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.

:)
TABLE OF CONTENTS
2.7Dunder methods and Python protocols

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

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.

Core answer

Special methods are the protocol layer behind Python syntax.

  • a == b can dispatch through __eq__
  • len(x) uses __len__
  • for item in x uses __iter__
  • item in x prefers __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.x
class UserId:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if not isinstance(other, UserId):
return NotImplemented
return self.value == other.value
a = UserId(7)
b = UserId(7)
alias = a
print(a == b) # True -> __eq__
print(a is b) # False -> different objects
print(a is alias) # True -> same object
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 they exist

Python'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, for can 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.

CPython internals

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.x
class A:
def __len__(self):
return 42
obj = A()
print(len(obj)) # 42
# Instance-level __len__ is NOT found by len()
class B:
pass
obj = B()
obj.__len__ = lambda: 42
try:
print(len(obj)) # TypeError
except 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:

  1. If b is a subtype of a's type, CPython may try b.__eq__(a) first (the subtype rule)
  2. Otherwise, try a.__eq__(b)
  3. If a.__eq__ returns NotImplemented, try b.__eq__(a) (reflected operation)
  4. 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.

Most used special methods

The most important ones in ordinary production code are:

CategoryMethod namesUsed byMain concern
Representationreprrepr(obj), debugging, logsMake object state inspectable
Equality / hashingeq, hash==, dict keys, set elementsKeep equality and hash coherent
Truthiness / sizebool, lenif obj, bool(obj), len(obj)Keep emptiness and truth rules predictable
Iteration / membershipiter, containsfor, inExpose efficient traversal and lookup semantics
Constructionnew, initClass(...)Separate allocation from initialization when necessary
Context managemententer, exitwithMake resource cleanup deterministic

If you implement only a few well, a custom type can already integrate naturally with the rest of Python.

How dispatch actually works

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 == b is 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 b is 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__
  • is never does
# [CURRENT - 3.10-3.14] Works on Python 3.x
class Token:
def __eq__(self, other):
print("__eq__ called")
return True
x = Token()
y = Token()
print(x == y) # calls __eq__
print(x is y) # direct identity check

For truth testing:

  • Python prefers __bool__
  • if __bool__ is absent, Python can use __len__
# [CURRENT - 3.10-3.14] Works on Python 3.x
class 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.x
class RoleSet:
def __init__(self, roles):
self._roles = set(roles)
def __contains__(self, role):
return role in self._roles
print("admin" in RoleSet({"admin", "billing"}))

For iteration:

  • for uses iter(obj)
  • that normally dispatches to __iter__
# [CURRENT - 3.10-3.14] Works on Python 3.x
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
current = self.start
while current > 0:
yield current
current -= 1
print(list(Countdown(3)))
Equality, hashing, and container semantics

__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.x
class 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 .

Representation and debugging

__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.x
class 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.

Best practices for implementing dunder methods
  1. 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.

  1. Keep semantic contracts coherent.
  • __eq__ and __hash__ must agree
  • __bool__ and __len__ should not fight each other
  • __contains__ should match what iteration conceptually exposes
  1. Return NotImplemented for 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.

  1. 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.

  1. 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.

  1. 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 .

Version context

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.

Edge cases and gotchas

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.x
name = "".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.

Production usage

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:
Further depth
  • Data model: special method names
  • Data model: object.__hash__
  • Built-in functions: repr
  • Built-in functions: len
  • Iterator types
  • Context manager types
  • CPython source: Objects/typeobject.c
  • CPython source: Python/bltinmodule.c
  • Python data model: special method names
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