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 dataclassfrom 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))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 dataclassfrom 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 Falsewith 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: strclass 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.