Dunder methods and Python protocols

How __eq__, __iter__, __repr__, __len__, and friends map syntax to behavior

Special methods are Python's protocol API. You write `__eq__`, `__iter__`, `__repr__`, and the language calls them automatically when you use `==`, `for`, or `repr()`. CPython looks up these methods on the type, not on the instance, using the MRO (method resolution order). This bypasses the normal `__getattribute__` lookup, so special methods defined on individual instances are invisible to the interpreter. When `__eq__` returns `NotImplemented`, Python tries the reflected operation on the other operand. `__hash__` and `__eq__` must be consistent: equal objects must hash equally. `__bool__` falls back to `__len__` if `__bool__` is not defined. `__iter__` falls back to `__getitem__` with sequential integer indices. <a href="/language-identity-equality">See how __eq__ drives the == operator</a>. <a href="/dict-hash-tables">Learn why hashability matters for dict keys</a>.

Understand.
Visualize.
Master.

Python in Depth

An interactive engineering reference for Python internals

Quick note

Implement only what your type can honour.

:)
Python version

Targets Python 3.10–3.14. Python 3.9 and below are End-of-Life.

TABLE OF CONTENTS
2.7Dunder methods and Python protocols

How __eq__, __iter__, __repr__, __len__, and friends map syntax to behavior

Special methods are not decoration around a class. They are the protocol boundary through which Python syntax and built-ins ask a type to behave.

Core answer

Implement only the protocols your type can honor coherently. Return NotImplemented for unsupported binary comparisons, keep equality and hashing aligned, and make resource lifetime explicit with context-manager hooks when the object owns cleanup.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass
from collections.abc import Iterator
@dataclass(frozen=True, slots=True)
class Batch:
batch_id: str
rows: tuple[str, ...]
def __len__(self) -> int:
return len(self.rows)
def __iter__(self) -> Iterator[str]:
return iter(self.rows)
payload = Batch("B-7", ("accepted", "settled"))
print(len(payload))
print(list(payload))
Map Python Syntax to Special Methods

Switch between common Python operations and see whether Python dispatches through a special method, which method it prefers, what fallback exists, and what a production-grade implementation should guarantee.

Why this design exists

Python favors protocols over inheritance-heavy operator hierarchies. for, len, with, repr, containment, comparison, and arithmetic can work with user-defined objects without adding parallel syntax for custom types. The names are special because the data model assigns them semantics.

That also explains why identity is outside the system. There is no __is__; pointer identity must stay reliable even when equality is domain-defined.

Mechanics and CPython internals

Special method lookup usually happens on the type, not on an instance attribute you attach later. CPython maps many operations through type slots in PyTypeObject, so len(obj) reaches a sequence length slot rather than performing ordinary attribute lookup for every call. Rich comparison has reflected and subtype-aware dispatch, and the NotImplemented sentinel gives the other operand a chance to participate.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass
from types import TracebackType
@dataclass(slots=True)
class Lease:
name: str
open: bool = False
def __enter__(self) -> "Lease":
self.open = True
return self
def __exit__(self, exc_type: type[BaseException] | None, exc: BaseException | None,
tb: TracebackType | None) -> bool:
self.open = False
return False
with Lease("export") as lease:
print(lease.open)
print(lease.open)

Complexity and tradeoffs

The syntax may be cheap-looking even when the method is not. __contains__ over a set-backed object can be average O(1); an iteration fallback can be O(n). __repr__, truthiness, and membership appear in control flow and diagnostics, so surprising I/O or network work inside them is an API hazard.

Idiomatic patterns and refactoring

Prefer generated behavior for plain records and hand-written hooks for domain-specific protocol semantics.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class RegionKey:
tenant: str
region: str
class LooseRegion:
def __init__(self, tenant: str, region: str) -> None:
self.tenant = tenant
self.region = region
def __eq__(self, other: object) -> bool:
if not isinstance(other, LooseRegion):
return NotImplemented
return (self.tenant, self.region) == (other.tenant, other.region)
print({RegionKey("TEN-1", "BR"): "ready"})
print(LooseRegion("TEN-1", "BR") == LooseRegion("TEN-1", "BR"))

Common mistakes and edge cases

Do not assign obj.__len__ = ... and expect len(obj) to see it. Do not define equality over mutable key state and then expect dict membership to survive mutation. Do not use __repr__ as end-user formatting or serialization.

When to use / When NOT to use

Use special methods when a Python protocol is the clearest public contract of the type: containers, stable value objects, callable adapters, and resource owners are common examples.

Do not add methods because syntax looks clever. If callers cannot predict the protocol semantics, expose a named method instead.

Further reading

  • Official docs: special method names
  • Official docs: context managers
  • Official docs: object hashing
  • CPython source: Objects/typeobject.c
  • CPython source: built-ins dispatch
BOARD NOTESContext
WHY NO BENCHMARK?

This topic is better taught with structure, semantics, and cross-references than with a synthetic chart.

Implement only what your type can honour.

RELATED GUIDES
NEXT CHECKS
Contribute