Dunders (__add__, __eq__, __getitem__, …) plug a class into language protocols. Define them in the class body; the VM calls them when the corresponding operator, builtin, or syntax form runs.
class V:
def __init__(self, n):
self.n = n
def __add__(self, o):
return V(self.n + o.n)
def __eq__(self, o):
return self.n == o.n
print((V(3) + V(4)).n)
print(V(3) == V(3))7
TrueDunders are looked up on the class chain (instance dict skipped). Subclasses inherit and may override, operator overloading composes with single-level inheritance. Monomorphic sites (same class for both operands) promote through the IC after 4 hits and bypass lookup entirely.
Arithmetic
| Operator | Forward | Reflected |
|---|---|---|
a + b | __add__ | __radd__ |
a - b | __sub__ | __rsub__ |
a * b | __mul__ | __rmul__ |
a / b | __truediv__ | __rtruediv__ |
a // b | __floordiv__ | __rfloordiv__ |
a % b | __mod__ | __rmod__ |
a ** b | __pow__ | __rpow__ |
-a | __neg__ | , |
Returning NotImplemented from the forward op tells the VM to try the reflected op on the other operand. Both NotImplemented (or neither defined) -> TypeError.
Subclass-first: when type(b) is a strict subclass of type(a), b.__radd__ runs before a.__add__, lets a subclass override an inherited reflected op without touching the base.
class Money:
def __init__(self, n): self.n = n
def __add__(self, o):
return Money(self.n + (o.n if isinstance(o, Money) else o))
def __radd__(self, o):
return Money(o + self.n)
print((Money(10) + Money(5)).n)
print((3 + Money(7)).n)15
10Comparison
| Operator | Forward | Reflected |
|---|---|---|
a == b | __eq__ | __eq__ |
a != b | __eq__ | __eq__ |
a < b | __lt__ | __gt__ |
a <= b | __le__ | __ge__ |
a > b | __gt__ | __lt__ |
a >= b | __ge__ | __le__ |
!= falls back to not __eq__ when __ne__ is absent. Results coerce to bool, __lt__ returning 'A.lt' yields True, not the string.
Truth and length
bool(x) (and any boolean context) consults:
__bool__if defined -> cast to bool.__len__if defined ->Falsewhen length is 0, elseTrue.- Default
True.
len(x) calls __len__ directly; must return a non-negative int.
class Empty:
def __bool__(self):
return False
class Container:
def __init__(self, n): self.n = n
def __len__(self):
return self.n
print(bool(Empty()))
print(bool(Container(0)), bool(Container(3)))
print(len(Container(5)))False
False True
5Indexing and containment
| Form | Dunder | Arguments |
|---|---|---|
obj[i] | __getitem__ | (self, i) |
obj[i] = v | __setitem__ | (self, i, value) |
del obj[i] | __delitem__ | (self, i) |
v in obj | __contains__ | (self, value) |
Slices pass as a slice object: obj[1:3] calls __getitem__(self, slice(1, 3, None)).
Absent __contains__: v in obj falls back to iterating obj with __eq__.
Iteration
| Method | Role |
|---|---|
__iter__ | Returns an iterator (often self). |
__next__ | Returns the next item, or raises StopIteration to end the loop. |
class Up:
def __init__(self, stop):
self.i = 0
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.i >= self.stop:
raise StopIteration
self.i += 1
return self.i
print(list(Up(3)))[1, 2, 3]for loops, list(x), and tuple(x) all honour the protocol.
Callable
__call__ makes instances invocable.
class Double:
def __call__(self, x):
return x * 2
d = Double()
print(d(7))
print(callable(d))14
TrueHashing
hash(x) calls __hash__; must return int (masked to INT_MAX).
Eq/hash invariant: a class defining __eq__ without __hash__ is unhashable, hash(x) and {x: 1} raise TypeError. Prevents inconsistent dict keys.
class K:
def __init__(self, n): self.n = n
def __hash__(self):
return self.n
def __eq__(self, o):
return self.n == o.n
k = K(5)
print(hash(k))
print({k: 'found'}[k]) # same instance reference looks up reliably5
foundBuilt-in dict/set still compare instance keys by identity (Val bits); user __hash__ is returned by hash() but doesn’t change containment in built-in containers. Use the same instance reference to look up reliably.
Representation
| Function / form | Dunder | Fallback |
|---|---|---|
repr(x) | __repr__ | <ClassName instance> |
str(x), print(x) | __str__ | __repr__, then default |
f"{x}" (no spec) | __str__ | same as str(x) |
f"{x:spec}" | __format__ | built-in format spec engine |
f"{x!r}" | __repr__ | , |
__format__(spec) receives the spec string and must return str.
Attribute access fallback
__getattr__(self, name) runs only when normal lookup (instance dict -> class chain) misses. Receives the name as a string; returns the value or raises AttributeError to surface a real miss.
class Proxy:
def __getattr__(self, name):
return f"computed:{name}"
p = Proxy()
print(p.anything)
print(p.foo)computed:anything
computed:fooExisting attributes bypass __getattr__; only misses trigger it.
Context managers
with cm() as x: invokes __enter__; its return binds to as. On exit, __exit__(exc_type, exc_value, traceback) runs, (None, None, None) for normal exit, live exception info on raise. Truthy return suppresses; falsy propagates.
class Suppress:
def __enter__(self):
return self
def __exit__(self, t, v, tb):
return True # swallow whatever raised
with Suppress():
raise ValueError("boom")
print("after")afterMultiple managers (with a(), b() as x:) nest LIFO, b enters last, exits first. Each has its own implicit handler, so inner suppression still lets outer managers run their normal __exit__(None, None, None).
If __exit__ itself raises, the new exception replaces the original.
What’s not dispatched
Parsed for compatibility but never invoked on user classes:
__init_subclass__,__set_name__, descriptors (__get__/__set__/__delete__)__new__, VM constructs the instance;__init__runs user logic- Augmented-assignment dunders (
__iadd__, …),a += bdesugars toa = a + b, so__add__covers it - Async dunders (
__aenter__/__aexit__/__aiter__/__anext__),async with/async foruse the sync paths
For class basics (constructors, inheritance, properties), see Classes.