Previous | Tutorial index | Next
Learn the most important special methods to customize class behavior.
Dunder methods (short for double underscore methods) are special methods that have two leading and two trailing underscores, like __init__, __str__, and __add__. They are also called magic methods because they enable your classes to integrate seamlessly with Pythonâs built-in operations and syntax.
len(obj), Python calls obj.__len__(). When you write obj1 + obj2, Python calls obj1.__add__(obj2).+ means for your class).We will cover the ones you will use most frequently:
__init__ â initializer (covered in Tutorial 2).__str__ â human-readable string representation.__repr__ â unambiguous representation, often for debugging.__len__ â length of the object.__eq__ â equality (==).__lt__, __le__, __gt__, __ge__ â comparison operators.__add__, __sub__, __mul__, etc. â arithmetic operators.__call__ â makes an object callable like a function.__str__ and __repr__: Representing Your ObjectsThese two methods control how your objects are converted to strings.
__str__str(obj) and print(obj).__str__ is not defined, Python falls back to __repr__.__repr__repr(obj) and in the interactive REPL when you just type the object name.__repr__; define __str__ only when you want a different userâfriendly display.class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"Person('{self.name}', {self.age})"
def __str__(self):
return f"{self.name} (age {self.age})"
p = Person("Alice", 30)
print(p) # Alice (age 30) -> __str__
print(repr(p)) # Person('Alice', 30) -> __repr__
In the REPL, typing p would show Person('Alice', 30).
When to define both: If the __repr__ is already clear and user-friendly, you can omit __str__. Many library classes define __repr__ only.
__len__: Supporting the len() FunctionTo make your object work with len(), define __len__ to return an integer.
class ShoppingCart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
cart = ShoppingCart()
cart.add("apple")
cart.add("banana")
print(len(cart)) # 2
__eq__, __lt__, etc.: Comparison Operators__eq__ defines equality (==).__lt__ defines less than (<).__le__ defines less than or equal to (<=).__gt__ defines greater than (>).__ge__ defines greater than or equal to (>=).If you define __eq__ and __lt__, Python can sometimes infer others (but it's safer to define them explicitly or use functools.total_ordering).
Book Class with Equality and Orderingclass Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __eq__(self, other):
if not isinstance(other, Book):
return NotImplemented
return self.pages == other.pages
def __lt__(self, other):
if not isinstance(other, Book):
return NotImplemented
return self.pages < other.pages
def __repr__(self):
return f"Book('{self.title}', '{self.author}', {self.pages})"
b1 = Book("1984", "Orwell", 328)
b2 = Book("Brave New World", "Huxley", 268)
print(b1 == b2) # False
print(b1 < b2) # False (328 < 268 is False)
print(b2 < b1) # True
Important: If you do not handle the case where other is not the same type, return NotImplemented so Python can try swapping operands.
__add__ and Friends: Arithmetic Operators__add__ for +__sub__ for -__mul__ for *__truediv__ for /__floordiv__ for //__mod__ for %__pow__ for **Vector Class Supporting Additionclass Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2
print(v3) # Vector(6, 8)
__radd__)If the left operand does not support the operation, Python tries __radd__ on the right operand. This is less common but useful for mixing types.
__call__: Making Objects CallableWhen you define __call__, your object can be used like a function.
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
c = Counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
This is useful for creating function-like objects that maintain state (e.g., closures, decorators, or stateful callbacks).
| Dunder Method | Purpose | Example Use |
|---|---|---|
__contains__ |
Called by in operator |
if item in my_container: |
__getitem__ |
Indexing (obj[key]) |
value = my_list[0] |
__setitem__ |
Assignment by index | my_list[0] = 5 |
__delitem__ |
Deletion by index | del my_list[0] |
__iter__ |
Returns an iterator for for loops |
for item in my_obj: |
__next__ |
Gets the next item in an iterator | Used with __iter__ |
__hash__ |
Makes object hashable (for sets, dict keys) | hash(obj) |
__bool__ |
Called by bool() and if statements |
if obj: |
We will focus on the most common ones, but you should know these exist.
NotImplemented SingletonWhen implementing binary operators (like __add__), if the operation is not supported with the given operand, you should return the special value NotImplemented (not raise NotImplementedError). This allows Python to try the reflected operation on the other operand.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented # Try other's __radd__
def __radd__(self, other):
# If other is something that can be added to Vector
# For example, if other is a number, we might add it to both components
if isinstance(other, (int, float)):
return Vector(self.x + other, self.y + other)
return NotImplemented
v = Vector(1, 2)
result = v + 3 # uses __radd__ (since 3 does not have __add__ for Vector)
print(result) # Vector(4, 5)
| Best Practice | Explanation |
|---|---|
Always define __repr__ |
It helps with debugging and provides a clear representation. |
Define __str__ only if different from __repr__ |
If the unambiguous repr is user-friendly, skip __str__. |
Use NotImplemented for unsupported operands |
Do not raise an error; let Python try the other side. |
| Keep dunder methods focused | Each dunder should do one thing and do it well. |
| Be consistent with operator semantics | If you define __eq__, also consider defining __ne__ (or Python will infer it from __eq__ in many contexts, but explicit is sometimes better). |
Avoid side effects in __repr__ |
It should just return a string, not modify the object. |
| Overload only when it makes sense | Not every class should support + or <; only when the operation is intuitively meaningful. |
__add__, always check the type of other and return NotImplemented if unsupported.__str__ but not __repr__: the default __repr__ is unhelpful; always define __repr__.__call__ do too much: it should be used sparingly; if it does many things, consider separate methods.__hash__: if you define __eq__, you must define __hash__ only if the object is immutable; otherwise, set __hash__ = None to make it unhashable.Fraction ClassLet's build a simple Fraction class that demonstrates many dunder methods.
import math
class Fraction:
def __init__(self, numerator, denominator):
if denominator == 0:
raise ValueError("Denominator cannot be zero")
self.numer = numerator
self.denom = denominator
self._reduce()
def _reduce(self):
"""Simplify the fraction using GCD."""
g = math.gcd(self.numer, self.denom)
self.numer //= g
self.denom //= g
# String representations
def __repr__(self):
return f"Fraction({self.numer}, {self.denom})"
def __str__(self):
return f"{self.numer}/{self.denom}"
# Arithmetic
def __add__(self, other):
if not isinstance(other, Fraction):
return NotImplemented
new_numer = self.numer * other.denom + other.numer * self.denom
new_denom = self.denom * other.denom
return Fraction(new_numer, new_denom)
def __sub__(self, other):
if not isinstance(other, Fraction):
return NotImplemented
new_numer = self.numer * other.denom - other.numer * self.denom
new_denom = self.denom * other.denom
return Fraction(new_numer, new_denom)
def __mul__(self, other):
if not isinstance(other, Fraction):
return NotImplemented
return Fraction(self.numer * other.numer, self.denom * other.denom)
def __truediv__(self, other):
if not isinstance(other, Fraction):
return NotImplemented
return Fraction(self.numer * other.denom, self.denom * other.numer)
# Comparison
def __eq__(self, other):
if not isinstance(other, Fraction):
return NotImplemented
return self.numer == other.numer and self.denom == other.denom
def __lt__(self, other):
if not isinstance(other, Fraction):
return NotImplemented
return self.numer * other.denom < other.numer * self.denom
def __le__(self, other):
if not isinstance(other, Fraction):
return NotImplemented
return self < other or self == other
# Length? Not meaningful, but we could define __len__ as number of digits?
# We'll skip.
# Callable: convert to float
def __call__(self):
return self.numer / self.denom
# For use in set/dict (hashable if immutable)
def __hash__(self):
return hash((self.numer, self.denom))
# --- Using the class ---
f1 = Fraction(1, 2)
f2 = Fraction(2, 3)
print(f1) # 1/2
print(repr(f1)) # Fraction(1, 2)
print(f1 + f2) # 7/6
print(f1 * f2) # 1/3
print(f1 == f2) # False
print(f1 < f2) # True (1/2 < 2/3)
print(f1()) # 0.5 (as float)
Observations:
__repr__ gives a clear, recreatable representation.__str__ gives a user-friendly fraction display.__call__ allows converting to float with f1().Answer the following questions to check your understanding.
1. Which dunder method is called by print(obj)?
__repr____str____print____display__2. What is the best practice regarding __repr__ and __str__?
__str__; __repr__ is optional.__repr__; define __str__ if a different userâfriendly string is needed.__repr__ and never __str__.3. If __str__ is not defined, what does print(obj) use?
__repr____unicode__object.__str__4. Which dunder method is called when you write obj + other?
__plus____add____concat____sum__5. What should a binary operator (like __add__) return if the operation is not supported for the given operand type?
NoneFalseNotImplementedraise TypeError6. Consider the following code:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
What will happen if you try p1 + 5 where p1 is a Point?
other.x does not exist.__add__ for points.NotImplemented.7. (True/False) The __call__ method allows an object to be used as a function.
8. Which dunder method defines the behavior of the len() function?
__len____size____length____count__9. If you define __eq__ but not __ne__, what does obj1 != obj2 do?
not (obj1 == obj2).False.10. Which of the following is a valid reason to define __call__ on a class?
(Exercise text unchanged)
Part C: Reflection
In comments, explain why you chose to implement __repr__ and __str__ differently, and why you defined __lt__ and __eq__.
(Homework text unchanged)
Part 3: Reflection Questions
Answer these in a comment block at the top of your script:
__str__ method, how did you handle the signs and formatting? What were the trickiest cases?__len__ method relate to the degree of the polynomial? Why did we choose to return 0 for the zero polynomial?Polynomial to an integer? How could you support that (e.g., p + 5)? (Hint: look up __add__ with type checks and __radd__.)Polynomial class more Pythonic and easier to use compared to having methods like add(), sub(), etc.?Bonus Challenge (Optional):
Implement __sub__ and __neg__ such that -p works. Also implement __radd__ and __rmul__ so that 5 + p and 2 * p work. Test them.
Before moving to Tutorial 9 (Class as Decorator), ensure you can confidently say YES to the following:
Ready for the next tutorial? In Tutorial 9, you will learn how to use a class as a decorator â a powerful technique for creating reusable decorators with state.