Dunder / Magic Methods
Double-underscore methods let your classes participate in Python's syntax - operators, context managers, iteration, and string formatting. Implement them deliberately; defaults are not always correct for value objects.
Recipe
class Vector:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Vector({self.x}, {self.y})"
def __add__(self, other: "Vector") -> "Vector":
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)When to reach for this:
- Value types with natural
+,==, or ordering - Custom containers implementing
len,getitem - Resource types needing
withcleanup - Debugging-friendly
__repr__on domain objects
Working Example
class Shelf:
def __init__(self, items: list[str]) -> None:
self._items = list(items)
def __len__(self) -> int:
return len(self._items)
def __getitem__(self, index: int) -> str:
return self._items[index]
def __contains__(self, item: object) -> bool:
return item in self._items
def __repr__(self) -> str:
return f"Shelf({self._items!r})"
class Money:
__slots__ = ("amount", "currency")
def __init__(self, amount: int, currency: str) -> None:
self.amount = amount
self.currency = currency
def __eq__(self, other: object) -> bool:
if not isinstance(other, Money):
return NotImplemented
return self.amount == other.amount and self.currency == other.currency
def __hash__(self) -> int:
return hash((self.amount, self.currency))
if __name__ == "__main__":
s = Shelf(["a", "b"])
print(len(s), "a" in s, s[0])
cache = {Money(100, "USD"): "ok"}
print(cache[Money(100, "USD")])What this demonstrates:
- Container protocol via
__len__,__getitem__,__contains__ - Value equality with
__eq__returningNotImplementedfor unknown types - Matching
__hash__enables use as dict key __repr__shows developer-oriented reconstruction hint
Deep Dive
How It Works
- Data model - Python calls dunder methods for operators and builtins.
__init__vs__new__-__new__creates instance;__init__initializes. Rarely override__new__.- Rich comparisons -
__lt__etc. enablesortedand@total_ordering. - Hash contract - Equal objects must have equal hashes; hash must not change while in dict/set.
- Iterator protocol -
__iter__+__next__or generator methods.
Common Dunders
| Method | Triggered by |
|---|---|
__repr__ | repr(), interactive display |
__str__ | str(), print() |
__eq__ | == |
__hash__ | hash(), dict/set key |
__enter__/__exit__ | with statement |
Python Notes
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major: int, minor: int) -> None:
self.major = major
self.minor = minor
def __eq__(self, other: object) -> bool:
...
def __lt__(self, other: "Version") -> bool:
return (self.major, self.minor) < (other.major, other.minor)Gotchas
- eq without hash - Makes instances unhashable. Fix: Add consistent
__hash__or usefrozendataclass. - Mutable objects as keys with hash - Hash must not change after insert. Fix: Immutable value objects only.
- repr returning user secrets - Leaks tokens in logs. Fix: Redact sensitive fields.
- Returning NotImplemented - Wrong type should return
NotImplemented, not False, for==. - Overloading everything - Surprising operator behavior. Fix: Implement only natural operations.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
@dataclass | Boilerplate repr/eq | Custom operator semantics |
NamedTuple | Immutable simple records | Need mutability |
attrs library | Rich validation ecosystem | Stdlib-only constraint |
| functions | Single operation on data | Operators improve readability |
FAQs
repr vs str?
__repr__ for developers and logs - unambiguous. __str__ for end-user messages. Fallback chain: str → repr.
When implement __hash__?
When instances should be dict keys or set members and equality is value-based with immutable fields.
What is NotImplemented?
Return from rich comparison when other type unknown - Python tries reversed operation on other operand.
Do I need __iter__ and __next__?
Implement both for iterator objects. Iterable classes only need __iter__ yielding iterator.
Can dataclass generate dunders?
Yes - eq, order, frozen, repr flags control generated methods.
__bool__ vs __len__?
__bool__ defines truthiness. __len__ returning 0 makes falsy unless __bool__ overrides.
__slots__ interaction?
Slots restrict attributes; some dunder attrs must be in __slots__ list if used.
Operator overloading readability?
Great for math vectors, money, paths. Poor for unrelated metaphors (+ on unrelated types).
__call__ use case?
Makes instances callable - functor pattern, decorator objects, stateful callbacks.
collections.abc helpers?
Subclass MutableMapping etc. to inherit required method checklist and doc guidance.
Related
- dataclasses - generated dunders
- Properties & Descriptors - attribute access
- Immutability & Hashability - hash rules
- Context Managers - enter/exit
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.