Special Methods: Making Objects Behave Like Built-ins
Dunder methods let your class work with print, len, +, ==, in, iteration and with. They are how Python asks an object to participate in ordinary syntax.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
What a dunder method is
Names surrounded by double underscores are hooks Python calls for you. You almost never call them directly; you use the syntax and Python calls the method.
| You write | Python calls |
|---|---|
len(obj) | obj.__len__() |
obj[key] | obj.__getitem__(key) |
a + b | a.__add__(b) |
a == b | a.__eq__(b) |
str(obj), print(obj) | obj.__str__() |
x in obj | obj.__contains__(x) |
for x in obj | obj.__iter__() |
obj(...) | obj.__call__(...) |
with obj: | obj.__enter__() / __exit__() |
__str__ and __repr__
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
b = Book("Dune", "Frank Herbert")
print(b) # <__main__.Book object at 0x7f...> - uselessclass Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __repr__(self):
"""Unambiguous, aimed at a developer."""
return f"Book(title={self.title!r}, author={self.author!r})"
def __str__(self):
"""Readable, aimed at a user."""
return f"{self.title} by {self.author}"
b = Book("Dune", "Frank Herbert")
print(b) # Dune by Frank Herbert - uses __str__
print(str(b)) # the same
print(repr(b)) # Book(title='Dune', ...) - uses __repr__
print([b]) # inside a container, __repr__ is used
print(f"{b}") # __str__
print(f"{b!r}") # __repr__If you write only one, write__repr__. Python falls back to it when__str__is missing, and it is what appears in lists, dictionaries, the REPL and debugger output - which is exactly where you need it most.
Length, indexing and membership
class Playlist:
def __init__(self, name):
self.name = name
self.tracks = []
def add(self, track):
self.tracks.append(track)
return self
def __len__(self):
return len(self.tracks)
def __getitem__(self, index):
return self.tracks[index] # slices work too, for free
def __setitem__(self, index, value):
self.tracks[index] = value
def __delitem__(self, index):
del self.tracks[index]
def __contains__(self, track):
return track in self.tracks
def __iter__(self):
return iter(self.tracks)
def __repr__(self):
return f"Playlist({self.name!r}, {len(self)} tracks)"
p = Playlist("Focus")
p.add("Track A").add("Track B").add("Track C")
print(len(p)) # 3
print(p[0]) # Track A
print(p[-1]) # Track C
print(p[0:2]) # ['Track A', 'Track B']
print("Track B" in p) # True
for track in p:
print(" -", track)
print(bool(p)) # True - falls back to __len__
print(p) # Playlist('Focus', 3 tracks)Six small methods made the class work with len, indexing, slicing, in, iteration and truthiness. None of that syntax knows anything about Playlist.
Comparison
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch=0):
self.parts = (major, minor, patch)
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self.parts == other.parts
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self.parts < other.parts
def __hash__(self):
return hash(self.parts)
def __repr__(self):
return "v" + ".".join(str(p) for p in self.parts)
versions = [Version(1, 2), Version(1, 10), Version(0, 9, 3)]
print(sorted(versions)) # [v0.9.3, v1.2.0, v1.10.0]
print(max(versions))
print(Version(1, 2) == Version(1, 2, 0)) # True
print(Version(2, 0) > Version(1, 99)) # True
print(len({Version(1, 0), Version(1, 0)})) # 1Returning NotImplemented for an unknown type is the correct habit: Python then tries the reflected operation on the other object before giving up, and comparisons with unrelated types fall back to sensible defaults.
Arithmetic
class Money:
def __init__(self, amount, currency="INR"):
self.amount = amount
self.currency = currency
def _check(self, other):
if not isinstance(other, Money):
return NotImplemented
if other.currency != self.currency:
raise ValueError("currency mismatch")
return other
def __add__(self, other):
other = self._check(other)
if other is NotImplemented:
return NotImplemented
return Money(self.amount + other.amount, self.currency)
def __sub__(self, other):
other = self._check(other)
if other is NotImplemented:
return NotImplemented
return Money(self.amount - other.amount, self.currency)
def __mul__(self, factor):
if not isinstance(factor, (int, float)):
return NotImplemented
return Money(self.amount * factor, self.currency)
__rmul__ = __mul__ # so 3 * money also works
def __neg__(self):
return Money(-self.amount, self.currency)
def __eq__(self, other):
return (isinstance(other, Money)
and (self.amount, self.currency) == (other.amount, other.currency))
def __hash__(self):
return hash((self.amount, self.currency))
def __str__(self):
return f"{self.currency} {self.amount:,.2f}"
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
a = Money(1500)
b = Money(250.50)
print(a + b) # INR 1,750.50
print(a - b)
print(a * 3)
print(3 * a) # works because of __rmul__
print(-a)
print(sum([a, b], Money(0)))| Operator | Method | Reflected |
|---|---|---|
+ | __add__ | __radd__ |
- | __sub__ | __rsub__ |
* | __mul__ | __rmul__ |
/ | __truediv__ | __rtruediv__ |
// | __floordiv__ | __rfloordiv__ |
% | __mod__ | __rmod__ |
** | __pow__ | __rpow__ |
+= | __iadd__ | - |
Truthiness
class Basket:
def __init__(self, items=None):
self.items = items or []
def __len__(self):
return len(self.items)
print(bool(Basket())) # False - via __len__
print(bool(Basket(["pen"]))) # True
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __bool__(self):
return self.celsius > 0 # explicit rule
print(bool(Temperature(-5))) # False
print(bool(Temperature(20))) # TruePython tries __bool__ first, then __len__, and treats the object as true if neither exists.
Callable objects and attribute hooks
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, n):
return n * self.factor
triple = Multiplier(3)
print(triple(7)) # 21
print(list(map(triple, [1, 2, 3]))) # [3, 6, 9]class Config:
def __init__(self, data):
self._data = data
def __getattr__(self, name):
"""Called ONLY when normal lookup fails."""
try:
return self._data[name]
except KeyError:
raise AttributeError(f"no setting named {name!r}") from None
config = Config({"theme": "dark", "size": 14})
print(config.theme) # dark
# print(config.missing) # AttributeError: no setting named 'missing'A complete small class
class Vector:
def __init__(self, *components):
self.components = tuple(components)
def __repr__(self):
return f"Vector{self.components}"
def __len__(self):
return len(self.components)
def __getitem__(self, index):
return self.components[index]
def __iter__(self):
return iter(self.components)
def __eq__(self, other):
return isinstance(other, Vector) and self.components == other.components
def __hash__(self):
return hash(self.components)
def __add__(self, other):
if len(self) != len(other):
raise ValueError("vectors must be the same length")
return Vector(*(a + b for a, b in zip(self, other)))
def __mul__(self, scalar):
return Vector(*(c * scalar for c in self.components))
__rmul__ = __mul__
def __abs__(self):
return sum(c * c for c in self.components) ** 0.5
v = Vector(3, 4)
w = Vector(1, 2)
print(v + w) # Vector(4, 6)
print(v * 2) # Vector(6, 8)
print(abs(v)) # 5.0
print(len(v), v[0]) # 2 3
print(list(v)) # [3, 4]
print(v == Vector(3, 4)) # TrueCommon mistakes
- Writing only
__str__, so lists and debugger output stay unreadable. - Defining
__eq__without__hash__, making the object unusable in sets. - Raising
TypeErrorfrom an arithmetic dunder instead of returningNotImplemented. - Making
__eq__or__hash__depend on attributes that change. - Calling
obj.__len__()directly instead oflen(obj). - Implementing
__getattr__in a way that recurses infinitely by touching a missing attribute.
Best practices
- Give every class a
__repr__that shows how to rebuild it. - Define
__eq__and__hash__together, over immutable attributes. - Return
NotImplementedfor types you do not handle. - Implement only the dunders your class genuinely needs.
- Keep dunder methods fast and free of side effects; they are called implicitly.
Practice
- Add
__repr__and__str__to a class and show where each one appears. - Write a
Matrixclass supporting+,*by a scalar and indexing by row and column. - Make a class usable in a set by implementing
__eq__and__hash__. - Write a class that supports
in,lenand iteration over its internal data. - Implement
__call__so instances can be passed tosorted(key=...).
Conclusion
Dunder methods are how a class joins in with Python's own syntax. Start with __repr__, add __eq__ and __hash__ when identity by value matters, and add the rest only when the syntax genuinely belongs to your object.