Special Methods (Dunder)
Special methods (also called "dunder" methods for "double underscore") let you define
how your objects behave with Python's built-in operations. Implement __str__ for
print(), __eq__ for ==, __add__ for +,
and make your classes feel like native Python types.
What Are Dunder Methods?
Dunder methods have double underscores on both sides: __init__, __str__,
__add__. Python calls these automatically when you use operators, built-in functions,
or certain syntax on your objects. They're the hook into Python's data model.
String Representation: __str__ and __repr__
Control how your objects appear when printed or displayed. __str__ is for end users,
__repr__ is for developers and debugging.
Click Run to execute your code
The Golden Rule for __repr__
Make __repr__ return valid Python code that could recreate the object:
eval(repr(obj)) should give you an equivalent object. If you only implement one,
implement __repr__ - Python uses it as a fallback for __str__.
Comparison Methods
Make your objects comparable with ==, <, >, and enable
sorting. Use @total_ordering to only define __eq__ and __lt__.
Click Run to execute your code
__eq__ and __hash__ Are Linked
If you define __eq__, you should also define __hash__ (or set
__hash__ = None to make objects unhashable). Objects that compare equal must
have the same hash value - this is required for sets and dict keys.
Arithmetic Operators
Implement +, -, *, and other operators for your objects.
This is called operator overloading.
Click Run to execute your code
Regular vs Reverse vs In-place
__add__:a + b- called on the left operand__radd__:b + a- called if left doesn't support the operation__iadd__:a += b- in-place, should returnself
Container Methods
Make your objects behave like lists, dicts, or other containers. Implement __len__,
__getitem__, __iter__, and more.
Click Run to execute your code
Iterator vs Iterable
An iterable has __iter__ and returns an iterator.
An iterator has both __iter__ (returns self) and __next__.
For simple cases, __iter__ can return iter(self.data) and skip __next__.
Common Mistakes
1. Forgetting to return in __str__ / __repr__
# Wrong - prints instead of returning
class Person:
def __str__(self):
print(f"Person: {self.name}") # Returns None!
# Correct - return a string
class Person:
def __str__(self):
return f"Person: {self.name}"
2. Forgetting __hash__ when defining __eq__
# Problem - objects become unhashable
class Point:
def __eq__(self, other):
return self.x == other.x and self.y == other.y
# No __hash__!
p = Point(1, 2)
{p} # TypeError: unhashable type: 'Point'
# Solution 1 - implement __hash__
def __hash__(self):
return hash((self.x, self.y))
# Solution 2 - explicitly unhashable
__hash__ = None # Can't be in sets/dict keys
3. Not returning self from __iadd__
# Wrong - doesn't return self
class Counter:
def __iadd__(self, other):
self.value += other
# Missing return self!
c = Counter(5)
c += 3 # c becomes None!
# Correct - always return self
def __iadd__(self, other):
self.value += other
return self # Required!
4. Not handling different types in __eq__
# Wrong - crashes on type mismatch
class Point:
def __eq__(self, other):
return self.x == other.x # AttributeError if other isn't Point!
# Correct - check type or return NotImplemented
class Point:
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented # Let Python try other.__eq__
return self.x == other.x and self.y == other.y
5. Inconsistent __lt__ and __eq__
# Problem - sorting may give unexpected results
class Score:
def __lt__(self, other):
return self.value < other.value
def __eq__(self, other):
return self.name == other.name # Different criterion!
# Both should use the same comparison logic
# Or use @total_ordering and be explicit
Exercise: Fraction Class
Task: Create a Fraction class with dunder methods for arithmetic and comparison.
Requirements:
__str__: Return "numerator/denominator" (e.g., "3/4")__repr__: Return "Fraction(num, denom)"__eq__: Check equality (1/2 should equal 2/4)__lt__: Enable sorting__add__: Add fractions (a/b + c/d = (ad + cb) / bd)__mul__: Multiply fractions (a/b * c/d = ac / bd)
Click Run to execute your code
Show Solution
class Fraction:
def __init__(self, numerator, denominator):
# Simplify fraction
gcd = self._gcd(abs(numerator), abs(denominator))
self.num = numerator // gcd
self.denom = denominator // gcd
@staticmethod
def _gcd(a, b):
while b:
a, b = b, a % b
return a
def __str__(self):
return f"{self.num}/{self.denom}"
def __repr__(self):
return f"Fraction({self.num}, {self.denom})"
def __eq__(self, other):
# Works because fractions are simplified
return self.num == other.num and self.denom == other.denom
def __lt__(self, other):
# Compare by cross multiplication
return self.num * other.denom < other.num * self.denom
def __add__(self, other):
new_num = self.num * other.denom + other.num * self.denom
new_denom = self.denom * other.denom
return Fraction(new_num, new_denom)
def __mul__(self, other):
return Fraction(self.num * other.num, self.denom * other.denom)
# Test
f1 = Fraction(1, 2)
f2 = Fraction(1, 4)
f3 = Fraction(2, 4)
print(f"f1: {f1}, repr: {repr(f1)}")
print(f"f1 == f3: {f1 == f3}") # True
print(f"f1 + f2: {f1 + f2}") # 3/4
print(f"f1 * f2: {f1 * f2}") # 1/8
print(f"Sorted: {sorted([Fraction(3,4), f1, f2])}")
Summary
__str__for users,__repr__for developers (implement __repr__ first)__eq__+__hash__for equality and set/dict usage@total_ordering: Define__eq__+__lt__, get all comparisons__add__,__radd__,__iadd__for+, reverse, and in-place__len__,__getitem__,__iter__for container behavior- Return
NotImplemented(notraise NotImplementedError) from operators on type mismatch
What's Next?
Now that you know about dunder methods, learn about Properties - using
@property decorator to create getters and setters that look like attribute access,
providing cleaner syntax than explicit getter/setter methods.
Enjoying these tutorials?