Python documentation calls them special method names. The community nickname is dunder methods, short for double underscore methods, because names such as __eq__, __iter__, and __repr__ begin and end with __. The nickname is fun but forgettable. What matters is what they do: they are how Python maps ordinary syntax and built-ins onto user-defined types.
Think of dunder methods like electrical outlet shapes. A device with the right plug shape works in any socket, regardless of brand. __iter__ is the two-prong plug — any class that implements it fits the for loop socket. __len__ is the three-prong plug for len(). The language defines the socket shapes; your class provides the plugs.
Special methods are the protocol layer behind Python syntax.
a == bcan dispatch through__eq__len(x)uses__len__for item in xuses__iter__item in xprefers__contains__repr(x)uses__repr__with x:uses__enter__and__exit__
This is why custom classes can feel native in Python: the language does not need a separate syntax for "user-defined container" or "user-defined context manager." It asks whether the object implements the relevant protocol.
One exception matters immediately: is is not a special-method dispatch point. Identity comparison is direct.
# [CURRENT - 3.10-3.14] Works on Python 3.xclass UserId: def __init__(self, value): self.value = value def __eq__(self, other): if not isinstance(other, UserId): return NotImplemented return self.value == other.valuea = UserId(7)b = UserId(7)alias = aprint(a == b) # True -> __eq__print(a is b) # False -> different objectsprint(a is alias) # True -> same objectPython's object model is protocol-driven. Instead of asking every object to inherit from a single rigid container or comparable base class, Python asks whether the object supports the operation at hand.
That is why built-ins and syntax stay uniform:
- if an object supports iteration,
forcan use it - if an object supports truth testing,
if obj:can use it - if an object supports context management,
with obj:can use it
That is why implementing a dunder method is never cosmetic. You are defining part of the public semantics of the type.
Special method lookup bypasses the normal instance-attribute lookup (__getattribute__) in most cases. Instead, CPython looks up the method on the type, not the instance (Objects/typeobject.c, _Py_lookup_special). This is documented in the Python data model: special methods are looked up on the type, using the type's MRO (method resolution order) — not the object's __dict__.
For example, when Python sees len(obj), the len() built-in calls type(obj).__len__(obj). It does NOT call obj.__len__(). This distinction matters because it means special methods defined on the instance rather than the class are invisible to the interpreter's dispatch logic.
# [CURRENT - 3.10-3.14] Works on Python 3.xclass A: def __len__(self): return 42obj = A()print(len(obj)) # 42# Instance-level __len__ is NOT found by len()class B: passobj = B()obj.__len__ = lambda: 42try: print(len(obj)) # TypeErrorexcept TypeError as exc: print(exc)This type-level lookup is implemented via the tp_as_number, tp_as_sequence, tp_as_mapping, and similar slots in PyTypeObject. Each slot is a struct of function pointers. When you define __len__ in Python, CPython sets the sq_length slot in tp_as_sequence. When len() is called, CPython calls sq_length directly without going through __getattribute__.
The __eq__ dispatch path. When Python evaluates a == b:
- If
bis a subtype ofa's type, CPython may tryb.__eq__(a)first (the subtype rule) - Otherwise, try
a.__eq__(b) - If
a.__eq__returnsNotImplemented, tryb.__eq__(a)(reflected operation) - If both return
NotImplemented, CPython falls back to identity comparison (a is b)
This is why NotImplemented is important: it is the signal that the type does not know how to handle the comparison, and the interpreter should try the other operand rather than silently returning False.
The __hash__ / __eq__ relationship. When you define __eq__ in a class, Python automatically sets __hash__ = None unless you also define __hash__. This is enforced at the type level in type.__new__ (Objects/typeobject.c). If a class has __hash__ = None, hash(obj) raises TypeError, and the instance cannot be used as a dict key or set element.
The __iter__ fallback. CPython's iter() built-in (Python/bltinmodule.c) first tries __iter__ via tp_iter. If that slot is NULL (no __iter__ defined), CPython falls back to __getitem__ via sq_item, constructing a PySeqIter_Type iterator that calls __getitem__ with increasing integer indices until IndexError.
Python's protocol mechanism is duck typing at the syntax level. The language does not require formal interface declarations. If a class implements len and getitem, it is a sequence. If it implements enter and exit, it is a context manager. This design reduces ceremony compared with formal interface systems found in Java or Go.
The most important ones in ordinary production code are:
| Category | Method names | Used by | Main concern |
|---|---|---|---|
| Representation | repr | repr(obj), debugging, logs | Make object state inspectable |
| Equality / hashing | eq, hash | ==, dict keys, set elements | Keep equality and hash coherent |
| Truthiness / size | bool, len | if obj, bool(obj), len(obj) | Keep emptiness and truth rules predictable |
| Iteration / membership | iter, contains | for, in | Expose efficient traversal and lookup semantics |
| Construction | new, init | Class(...) | Separate allocation from initialization when necessary |
| Context management | enter, exit | with | Make resource cleanup deterministic |
If you implement only a few well, a custom type can already integrate naturally with the rest of Python.
When Python sees syntax or a built-in that maps to a protocol, it performs a method lookup following the data model rules for that operation.
For equality:
a == bis semantic comparison- Python may call
a.__eq__(b) - if that returns
NotImplemented, Python can try the reflected path on the other operand before deciding
For identity:
a is bis direct identity comparison- there is no
__is__ - user code cannot overload identity
That distinction is why the sentence "the is operator uses __eq__" is wrong. The correct statement is:
==may dispatch through__eq__isnever does
# [CURRENT - 3.10-3.14] Works on Python 3.xclass Token: def __eq__(self, other): print("__eq__ called") return Truex = Token()y = Token()print(x == y) # calls __eq__print(x is y) # direct identity checkFor truth testing:
- Python prefers
__bool__ - if
__bool__is absent, Python can use__len__
# [CURRENT - 3.10-3.14] Works on Python 3.xclass Batch: def __init__(self, rows): self.rows = list(rows) def __len__(self): return len(self.rows)print(bool(Batch([])))print(bool(Batch([1, 2])))For membership:
- Python prefers
__contains__ - if absent, Python can fall back to iteration
# [CURRENT - 3.10-3.14] Works on Python 3.xclass RoleSet: def __init__(self, roles): self._roles = set(roles) def __contains__(self, role): return role in self._rolesprint("admin" in RoleSet({"admin", "billing"}))For iteration:
forusesiter(obj)- that normally dispatches to
__iter__
# [CURRENT - 3.10-3.14] Works on Python 3.xclass Countdown: def __init__(self, start): self.start = start def __iter__(self): current = self.start while current > 0: yield current current -= 1print(list(Countdown(3)))__eq__ and __hash__ must be treated as one design surface.
If two objects compare equal, their hashes must also be equal. That rule matters because dictionaries and sets use hash lookup first and equality second. If you break the relationship, keys and set membership become unreliable.
# [CURRENT - 3.10-3.14] Works on Python 3.xclass UserId: def __init__(self, value): self.value = value def __eq__(self, other): if not isinstance(other, UserId): return NotImplemented return self.value == other.value def __hash__(self): return hash(self.value)This connects directly to the dict and set guides:
- dict keys must be hashable
- set elements must be hashable
- mutation of equality-relevant fields after insertion is dangerous
See and .
Dataclasses are relevant here because they can generate __eq__ and __hash__ behavior for you. That is useful, but it is still your contract. See .
__repr__ is one of the highest-value special methods because it directly affects debugging, logs, and interactive inspection.
Good __repr__ practice:
- make it unambiguous
- include the fields that explain state
- keep it developer-facing, not user-marketing-facing
# [CURRENT - 3.10-3.14] Works on Python 3.xclass Job: def __init__(self, name, priority): self.name = name self.priority = priority def __repr__(self): return f"Job(name={self.name!r}, priority={self.priority!r})"print(repr(Job("backup", 3)))If your type already maps cleanly onto dataclass-like field behavior, generated __repr__ may be enough. If not, write the representation deliberately.
- Implement only protocols the type can honor naturally.
If the object is not meaningfully iterable, do not force __iter__ into it just because the syntax looks elegant.
- Keep semantic contracts coherent.
__eq__and__hash__must agree__bool__and__len__should not fight each other__contains__should match what iteration conceptually exposes
- Return
NotImplementedfor unsupported binary operations.
This is especially important in __eq__ and other rich comparisons. It gives Python a chance to try the other operand's implementation instead of silently lying.
- Keep the fast-path methods cheap.
Truthiness, repr, and membership are often used in debugging, logging, and control flow. If they are unexpectedly expensive, they become hidden costs.
- Do not overload semantics for surprise value.
A class whose __bool__ means "has passed validation in the last hour" or whose __contains__ does a network lookup is a maintenance hazard.
- Prefer generated behavior when the type is plain data.
If the class is mostly a record, NamedTuple or @dataclass may be better than writing repetitive dunders by hand. See .
Special methods are foundational Python behavior and stable across Python 3. Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life.
Two version-sensitive notes around surrounding features:
- structural pattern matching is Python 3.10+ and uses protocol-driven behavior in some places
- dataclass options that affect generated special methods, such as
slots=True, have their own version context
The names and meanings of core dunder protocols such as __eq__, __len__, __iter__, and __enter__ are stable Python behavior. Exact lookup internals and bytecode details are interpreter implementation concerns.
Do not confuse the informal nickname with the API contract. The docs talk about special method names, and the meaning comes from the data model, not from the fact that the name has underscores.
Do not implement __hash__ on mutable equality-relevant state casually. If the object changes after becoming a dict key or set element, later lookups can behave incorrectly.
Do not rely on __repr__ for end-user formatting or machine-stable serialization.
Do not use is where you mean value equality.
# [CURRENT - 3.10-3.14] Works on Python 3.xname = "".join(["A", "n", "a"])target = "Ana"print(name == target)print(name is target)The biggest special-method bug pattern is semantic inconsistency, not syntax. A type that technically implements eq, hash, or iter but violates caller expectations is worse than a type that simply omits the protocol.
Use special methods to make domain types integrate with Python where the mapping is honest:
__repr__for observability and debugging__eq__/__hash__for value objects and stable keys__iter__/__contains__for container-like abstractions__enter__/__exit__for resource lifetimes
Avoid hand-writing every possible protocol on every class. The point is not maximum cleverness. The point is to give the type the small set of native behaviors that make its contract clearer.
For related content:
- equality and identity:
- hashing and key semantics:
- set element/hash rules:
- generated
__eq__/__repr__/__hash__: - iterables and one-shot behavior: