Python Dunder Methods — __init__, __str__, __call__ and Friends
Dunder (double-underscore) methods let your objects hook into Python’s built-in operations. They’re what makes len(obj), str(obj), obj[key], and with obj work.
class Demo:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Demo({self.name})"
def __repr__(self):
return f"Demo({self.name!r})"
def __len__(self):
return len(self.name)
def __getitem__(self, index):
return self.name[index]Construction & Representation
| Method | Triggers | Purpose |
|---|---|---|
__new__(cls, ...) |
cls(...) |
Allocate instance (rarely overridden) |
__init__(self, ...) |
After __new__ |
Initialize instance state |
__del__(self) |
del obj / GC |
Cleanup (not a destructor — use context managers) |
__repr__(self) |
repr(obj), REPL |
Unambiguous developer-readable string |
__str__(self) |
str(obj), print(obj) |
Readable user-facing string |
__bytes__(self) |
bytes(obj) |
Byte representation |
__format__(self, spec) |
f"{obj:spec}", format(obj) |
Custom formatting |
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"User({self.name!r}, {self.age!r})"
def __str__(self):
return self.name
u = User("Alice", 30)
repr(u) # User('Alice', 30)
str(u) # Alice
print(u) # AliceContainer & Sequence Methods
Make your object behave like a list, dict, or set.
class Deck:
def __init__(self, cards):
self._cards = list(cards)
def __len__(self):
return len(self._cards)
def __getitem__(self, pos):
return self._cards[pos]
def __setitem__(self, pos, val):
self._cards[pos] = val
def __delitem__(self, pos):
del self._cards[pos]
def __contains__(self, card):
return card in self._cards
def __iter__(self):
return iter(self._cards)
def __reversed__(self):
return reversed(self._cards)
def __add__(self, other):
return Deck(self._cards + other._cards)
deck = Deck(["A♠", "K♥", "Q♦"])
len(deck) # 3
deck[0] # A♠
"A♠" in deck # True
for c in deck: ... # iteration| Method | Description |
|---|---|
__len__ |
len(obj) |
__getitem__ |
obj[key], slicing, iteration fallback |
__setitem__ |
obj[key] = val |
__delitem__ |
del obj[key] |
__contains__ |
x in obj |
__iter__ |
for x in obj, iter(obj) |
__reversed__ |
reversed(obj) |
__missing__ |
Called by dict-like on missing key (d[key] when key not in dict subclass) |
__length_hint__ |
Hint for list(obj) optimization |
Numeric Operators
Override +, -, *, /, ==, etc.
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar):
return self * scalar
def __neg__(self):
return Vector(-self.x, -self.y)
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __bool__(self):
return self.x != 0 or self.y != 0
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v1 + v2 # Vector(4, 6)
3 * v1 # Vector(3, 6) ← __rmul__ handles this
bool(v1) # TrueForward vs Reflected vs In-place:
| Category | Examples | Called when |
|---|---|---|
| Forward | __add__, __mul__ |
a + b (type of a wins) |
| Reflected | __radd__, __rmul__ |
b + a but a doesn’t implement __add__ |
| In-place | __iadd__, __imul__ |
a += b (falls back to __add__ if absent) |
Full list: __add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__, __lshift__, __rshift__, __and__, __or__, __xor__, and their __r*__ / __i*__ variants.
Comparison Methods
class Priority:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __ne__(self, other):
return self.value != other.value
def __lt__(self, other):
return self.value < other.value
def __le__(self, other):
return self.value <= other.value
def __gt__(self, other):
return self.value > other.value
def __ge__(self, other):
return self.value >= other.value
def __hash__(self):
return hash(self.value)Friendly reminder: defining __eq__ without __hash__ makes the class unhashable (can’t be used in sets or as dict keys). Restore hashing by implementing both.
Context Managers — __enter__ / __exit__
Make your object work with with statements.
class File:
def __init__(self, path):
self.path = path
def __enter__(self):
self.fh = open(self.path)
return self.fh
def __exit__(self, exc_type, exc_val, exc_tb):
self.fh.close()
return False # don't suppress exceptions
with File("/tmp/data.txt") as f:
print(f.read())__enter__runs on entry, returns the bound variable (as x).__exit__runs on exit (even if an exception occurred). ReturnTrueto suppress the exception.
Callable Objects — __call__
Make an instance callable like a function. Useful for callable classes (e.g. decorators, factories).
class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x):
return x + self.n
add5 = Adder(5)
add5(10) # 15
callable(add5) # TrueAttribute Access
| Method | Triggers | Use |
|---|---|---|
__getattr__ |
obj.x only when normal lookup fails |
Fallback / computed attributes |
__getattribute__ |
Every obj.x access |
Intercept all access (careful — infinite recursion risk) |
__setattr__ |
obj.x = val |
Validation, logging |
__delattr__ |
del obj.x |
Cleanup on delete |
__dir__ |
dir(obj) |
Custom attribute listing |
class Validated:
def __setattr__(self, name, val):
if name == "age" and val < 0:
raise ValueError("age must be >= 0")
super().__setattr__(name, val)__slots__ — Memory Optimization
Tell Python not to use a per-instance __dict__, saving memory.
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(1, 2)
p.z = 3 # AttributeError: 'Point' has no attribute 'z'__slots__ disables __dict__ and __weakref__ by default. Add "__dict__" to __slots__ if you still need dynamic attributes.
Pickling — __getstate__ / __setstate__
Control how your object is serialized by pickle.
class Connection:
def __init__(self, url):
self.url = url
self.socket = open_connection(url)
def __getstate__(self):
state = self.__dict__.copy()
del state["socket"] # can't pickle sockets
return state
def __setstate__(self, state):
self.__dict__.update(state)
self.socket = open_connection(self.url)Cheat Sheet: Quick Reference
| Category | Methods |
|---|---|
| Construction | __new__, __init__, __del__ |
| Representation | __repr__, __str__, __bytes__, __format__ |
| Container | __len__, __getitem__, __setitem__, __delitem__, __contains__, __iter__, __reversed__, __missing__ |
| Numeric | __add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__, __lshift__, __rshift__, __and__, __or__, __xor__, plus __r*__ and __i*__ variants |
| Unary | __neg__, __pos__, __abs__, __invert__, __bool__, __int__, __float__, __complex__, __index__ |
| Comparison | __eq__, __ne__, __lt__, __le__, __gt__, __ge__, __hash__ |
| Context | __enter__, __exit__ |
| Callable | __call__ |
| Attribute | __getattr__, __getattribute__, __setattr__, __delattr__, __dir__ |
| Serialization | __getstate__, __setstate__, __reduce__ |
| Slots | __slots__ |
Golden rule: Implement dunders only when your type genuinely is that kind of thing. Don’t add __len__ just because you can — add it when len(obj) is a natural operation for your object.