Dunder Methods
Dunder methods (double-underscore names, "dunders") are the hooks Python calls on your objects for its built-in operations: print calls __str__/__repr__, len calls __len__, + calls __add__, and so on. Defining them is how your classes become first-class citizens of the language - they can be printed, compared, hashed, iterated, and called without the caller knowing your type. This chapter covers the essential set, the conventions each one must follow, and the pairs that must be defined together (__eq__ and __hash__, __str__ and __repr__).
Identity and Presentation: __repr__ and __str__
class Money:
def __init__(self, amount, currency="USD"):
self.amount = amount
self.currency = currency
def __repr__(self):
# unambiguous, ideally re-parseable: the developer-facing string
return f"Money({self.amount!r}, {self.currency!r})"
def __str__(self):
# human-friendly: the display string
return f"{self.amount:.2f} {self.currency}"
m = Money(1234.5, "EUR")
print(repr(m)) # Money(1234.5, 'EUR') - shown in the REPL, in lists
print(str(m)) # 1234.50 EUR - shown by print()
print(m) # print uses __str__
print([m]) # a list shows __repr__ of its items
The rule: __repr__ is for developers - concise, unambiguous, ideally a valid expression that rebuilds the object (the !r conversion inside). __str__ is for humans - readable, formatted, localized if you like. If you define only __repr__, print falls back to it, so __repr__ is the one you should always write; add __str__ when the human form genuinely differs. The default __repr__ (<__main__.Money object at 0x...>) is a debugging smell in any object that will appear in logs or traces.
Equality and Hashing
class Vec2:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Vec2):
return NotImplemented # let the other side try, then default
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
a = Vec2(1, 2)
b = Vec2(1, 2)
print(a == b) # True
print(hash(a) == hash(b)) # True - equal objects hash equally
s = {a, b}
print(len(s)) # 1 - the set deduplicated them
The contract: if a == b, then hash(a) == hash(b) must hold - sets and dict keys rely on it. Define both together. Returning NotImplemented from __eq__ for unrelated types is the correct "I don't know" (Python then tries the reflected operation and falls back to identity); returning False there is a subtle bug that breaks symmetric comparisons. A mutable object that defines __eq__ and __hash__ must keep the hashed fields stable for as long as it sits in a set or dict - or set __hash__ = None to mark it unhashable and fail early. The convenience dataclasses (chapter 17) generates these for you from the field declarations.
The Container Protocol: __len__, __getitem__, __iter__
class Ruler:
"""A range-like object: Ruler(10) acts like the numbers 0..9."""
def __init__(self, n):
self.n = n
def __len__(self):
return self.n
def __getitem__(self, i):
if isinstance(i, slice):
return [self[i] for i in range(*i.indices(self.n))]
if not 0 <= i < self.n:
raise IndexError("out of range")
return i
def __iter__(self):
i = 0
while i < self.n:
yield i
i += 1
r = Ruler(5)
print(len(r)) # 5 - len() works
print(r[2]) # 2 - indexing works
print(r[1:4]) # [1, 2, 3] - slicing works
print(list(r)) # [0, 1, 2, 3, 4] - iteration works
for x in r:
pass # for-loops work
Three small methods make an object behave like a container: __len__ for len() and boolean truth, __getitem__ for indexing and slicing, and __iter__ (usually a generator) for for loops. With them, your class slots into the standard patterns - if not items:, for x in items:, items[1:3] - without the caller knowing it is not a list. __contains__ adds in support (defaulting to a scan via __iter__), and __bool__ overrides truth (defaulting to __len__ when present). This protocol is what makes "custom sequence" a one-afternoon exercise instead of a framework.
Calling and Context: __call__, __enter__/__exit__
class Counter:
def __init__(self, start=0):
self.calls = start
def __call__(self):
self.calls += 1
return self.calls
tick = Counter()
print(tick(), tick(), tick()) # 1 2 3 - the instance IS a callable
class Managed:
def __enter__(self):
print("open")
return self
def __exit__(self, exc_type, exc, tb):
print("close")
return False # do not suppress exceptions
with Managed() as m:
print("using", m)
__call__ makes an instance callable - the foundation of callback objects, functools.partial-like wrappers, and the "callable class" pattern where the instance carries state and the call does the work. __enter__/__exit__ are the context-manager pair (chapter 20 covers them fully): with calls __enter__ to set up, assigns its return to the as target, and calls __exit__ on the way out - returning True swallows the exception, False lets it propagate. Together they are the two dunders that turn a class into a behavior rather than a data bag.
The Essential Set
| Dunder | Trigger | Convention |
|---|---|---|
__repr__ | REPL, repr(), in containers | unambiguous, developer-facing |
__str__ | print(), str() | human-friendly; fall back to repr |
__eq__ / __hash__ | ==, sets, dict keys | define together; return NotImplemented |
__len__ / __getitem__ / __iter__ | len, index, for-loops | the container protocol |
__bool__ | truth testing | define with __len__ if they differ |
__call__ | instance() | stateful callable |
__enter__ / __exit__ | with | setup/teardown, exception policy |
__getattr__ | missing-attribute fallback | raise AttributeError, not Exception |
Summary
Dunder methods are the interface between your classes and Python's operators and built-ins: __repr__/__str__ for presentation (write __repr__ always, __str__ when the human form differs), __eq__/__hash__ together for value semantics and set/dict membership, the __len__/__getitem__/__iter__ trio for the container protocol, __call__ for callable instances, and __enter__/__exit__ for with. The conventions are as important as the names: return NotImplemented for unknown comparisons, keep hash consistent with equality, and raise AttributeError from __getattr__. Master the table above and your objects stop looking like foreign data and start behaving like the language's own types.