Dunder (double-underscore) methods are Python's protocol for customizing how your objects integrate with the language. Implement __len__ and len(obj) works. Implement __iter__ and for x in obj works. Implement __eq__ and == works. Python calls these methods behind familiar syntax; giving your class the right ones makes it feel “native”.
The most common dunders fall into three groups. Representation: __str__ (for str() and print), __repr__ (for the debugger and the REPL). Comparison: __eq__, __lt__, __hash__. Container-like: __len__, __iter__, __getitem__, __contains__. Start with these; skip the exotic ones until you need them.
Contracts matter. If you implement __eq__, you should also implement __hash__ (or set it to None to mark the object unhashable). If you implement __lt__, consider functools.total_ordering to fill in the other comparison operators from that one. If you implement __iter__, also implement __len__ when the length is known.
Go light on dunders. Too much magic makes a class hard to read: if + does something completely unrelated to addition, you surprise readers. A good rule: implement a dunder only when the class really behaves like the concept the operator or built-in represents.
Representation and comparison
__repr__ should return a string that, ideally, is valid Python to recreate the object: Point(3, 4). It is what the REPL prints. __str__ is the human-friendly version used by print; if you only define one, pick __repr__.
__eq__ + __hash__ usually move together. If your class is immutable, return the hash of a tuple of the identifying fields. If it is mutable, either forgo hashing (set __hash__ = None) or base the hash on an immutable identity.
Container protocols and with statements
Implement __len__, __getitem__, __iter__, and __contains__ to make your class feel like a sequence. Dictionaries use __getitem__ for lookup; iteration uses __iter__; for loops work from either.
For the with statement, implement __enter__ and __exit__ on the class (or decorate a generator with contextlib.contextmanager). Most resources you build should consider being a context manager.
A starter set of dunder methods.
| Tool | Purpose |
|---|---|
__repr__method | Unambiguous developer-facing text. |
__str__method | Human-friendly text (fallback: __repr__). |
__eq__, __hash__methods | Value-based equality and hashing. |
__len__method | Makes len(obj) work. |
__iter__, __next__methods | Custom iteration protocol. |
__getitem__method | Indexing: obj[key]. |
__contains__method | Membership: x in obj. |
@total_orderingdecorator | Fills in comparison operators from one. |
Customizing Object Behavior code example
The script defines a small Deck class that supports len, iteration, indexing, containment, and sorting.
# Lesson: Customizing Object Behavior
from dataclasses import dataclass
from functools import total_ordering
@total_ordering
@dataclass(frozen=True)
class Card:
rank: int # 2..14 (14 = Ace)
suit: str # one of "cdhs"
def __lt__(self, other: "Card") -> bool:
return (self.rank, self.suit) < (other.rank, other.suit)
def __str__(self) -> str:
names = {11: "J", 12: "Q", 13: "K", 14: "A"}
rank = names.get(self.rank, str(self.rank))
return f"{rank}{self.suit}"
class Deck:
def __init__(self):
self._cards = [Card(r, s) for s in "cdhs" for r in range(2, 15)]
def __len__(self) -> int:
return len(self._cards)
def __iter__(self):
return iter(self._cards)
def __getitem__(self, index):
return self._cards[index]
def __contains__(self, card: Card) -> bool:
return card in self._cards
def __repr__(self) -> str:
return f"Deck({len(self)} cards)"
deck = Deck()
print(deck) # __repr__
print("len:", len(deck)) # __len__
print("first 3:", [str(c) for c in deck[:3]]) # __getitem__
print("Ace of spades?", Card(14, "s") in deck) # __contains__
sorted_cards = sorted(deck)
print("top 3:", [str(c) for c in sorted_cards[-3:]]) # __lt__ via @total_ordering
# Iterate with for
counts = {s: 0 for s in "cdhs"}
for card in deck:
counts[card.suit] += 1
print("by suit:", counts)
Observe how Python drives each dunder:
1) `print(deck)` calls `__repr__` because no `__str__` is defined.
2) `len(deck)`, `deck[:3]`, and `card in deck` each go through one dunder.
3) `sorted(deck)` uses `__lt__`; `@total_ordering` fills the rest in.
4) `for card in deck` uses `__iter__`.
Implement a small vector class with + and ==.
from dataclasses import dataclass
@dataclass(frozen=True)
class Vec2:
x: float
y: float
def __add__(self, other: "Vec2") -> "Vec2":
return Vec2(self.x + other.x, self.y + other.y)
def __abs__(self) -> float:
return (self.x * self.x + self.y * self.y) ** 0.5
print(Vec2(1, 2) + Vec2(3, 4)) # Vec2(x=4, y=6)
print(abs(Vec2(3, 4))) # 5.0
print(Vec2(1, 2) == Vec2(1, 2)) # True
Verify a handful of dunder contracts.
class A:
def __init__(self, xs): self.xs = xs
def __len__(self): return len(self.xs)
def __iter__(self): return iter(self.xs)
def __contains__(self, x): return x in self.xs
a = A([1, 2, 3])
assert len(a) == 3
assert list(a) == [1, 2, 3]
assert 2 in a and 99 not in a
Running prints:
Deck(52 cards)
len: 52
first 3: ['2c', '3c', '4c']
Ace of spades? True
top 3: ['Ah', 'As', 'Ad']
by suit: {'c': 13, 'd': 13, 'h': 13, 's': 13}