Dunder Methods and the Data Model

A dunder method (“double-underscore,” e.g. __len__, __add__, __iter__) is a method whose name the interpreter recognizes and calls on your behalf whenever a piece of syntax or a built-in needs the corresponding behavior. You never write obj.__len__() directly; you write len(obj) and the interpreter looks up and calls __len__ for you. The single most important — and most surprising — rule about this implicit invocation is that the lookup happens on the object’s type, not on the object’s instance dictionary, and it generally bypasses __getattribute__ as well: “For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not on the instance itself” (Data model, “Special method lookup”). This note owns the semantics of that invocation — how, when, and against what the interpreter resolves a dunder. The higher-level “everything is an object, dunders are the protocol” framing lives in The Python Data Model; the C-level wiring that connects each dunder name to a tp_*/nb_* slot lives in Type Slots and the Protocol Tables. Verified against the data-model reference for 3.14 and Objects/abstract.c/typeobject.c/object.c at the v3.14.5 tag.

Mental Model — Syntax Is Sugar Over a Type-Side Method Call

The cleanest way to think about dunders is as a desugaring contract. Almost every syntactic construct and many built-in functions are shorthand for a method call that the interpreter generates automatically. len(x) is shorthand for “call x’s __len__”; a + b is shorthand for “call a’s __add__ (and maybe b’s __radd__)”; for y in z is shorthand for “call z’s __iter__, then repeatedly call __next__.” The reference states the principle directly: “A class can implement certain operations that are invoked by special syntax (such as arithmetic operations or subscripting and slicing) by defining methods with special names. This is Python’s approach to operator overloading” (Data model, “Special method names”).

The crucial refinement — the thing that makes dunders different from ordinary methods — is where the interpreter looks. For an ordinary explicit call like obj.method(), Python consults the instance dictionary first, then the type and its MRO (Method Resolution Order; see Method Resolution Order and C3 Linearization), running the full __getattribute__ machinery. For an implicit dunder call, the interpreter shortcuts straight to the type: it does the equivalent of type(obj).__len__(obj), never obj.__len__(). The instance dictionary is not consulted at all.

graph TD
    SRC["len(obj)<br/>(syntax / builtin)"] --> SLOT["read Py_TYPE(obj)->tp_as_sequence->sq_length<br/>(a C function pointer on the TYPE)"]
    SLOT -->|"slot is slot_sq_length<br/>(Python-defined class)"| LOOKUP["_PyType_LookupRef(Py_TYPE(obj), '__len__')<br/>walks the TYPE's MRO — NOT obj.__dict__"]
    SLOT -->|"slot is a C function<br/>(built-in type)"| CFUNC["call C sq_length directly"]
    LOOKUP --> CALL["vectorcall __len__(obj)"]
    INST["obj.__len__ = lambda: 5<br/>(set on the INSTANCE)"] -.->|"ignored — never consulted"| SLOT

    style SLOT fill:#ffe8b3
    style INST fill:#ffd0d0
    style LOOKUP fill:#dde7ff

Diagram: the implicit-invocation path for len(obj). The interpreter reads a function pointer (sq_length) off the type object; for a Python-defined class that pointer is the generic wrapper slot_sq_length, which looks __len__ up in the type’s MRO via _PyType_LookupRef(Py_TYPE(obj), ...). A __len__ attribute set on the instance (red) is never on this path, which is exactly why obj.__len__ = lambda: 5 does nothing for len(obj). The insight: dunders are resolved through the type’s slot table, not through normal attribute access on the object.

The Implicit-Invocation Rule — What Calls What

Implicit invocation is pervasive. A non-exhaustive map of the desugaring, all of which the interpreter performs without you ever naming the dunder:

  • Built-in functions. len(x)__len__; iter(x)__iter__; next(it)__next__; repr(x)__repr__; str(x)__str__ (falling back to __repr__); hash(x)__hash__; abs(x)__abs__; bool(x)__bool__ (falling back to __len__, then to “always true”); format(x, spec)__format__; reversed(x)__reversed__; round(x)__round__. Each of these is documented as such in the built-in functions and data-model references.
  • Operators. a + b__add__/__radd__; a == b__eq__; a < b__lt__; a[i]__getitem__; -a__neg__; a @ b__matmul__. The full dispatch semantics for binary operators (reflected operands, NotImplemented, the right-operand-subclass rule) are the subject of the sibling note Operator Overloading and the Numeric Protocols.
  • Statements and syntax. x[i] = v__setitem__; del x[i]__delitem__; k in x__contains__; for y in z:__iter__ then __next__; with w as v:__enter__/__exit__; obj.attr__getattribute__ (then __getattr__ on failure); obj()__call__; async with/async for__aenter__/__aexit__/__aiter__/__anext__.
  • Construction and lifecycle. Creating an instance C(...) invokes the metaclass’s __call__, which calls C.__new__ then C.__init__ (detailed in Object Creation new init and alloc). Finalization calls __del__ (see Finalizers and the del Method).

A subtle but real part of the contract: setting a special method to None retracts the protocol. “Setting a special method to None indicates that the corresponding operation is not available. For example, if a class sets __iter__() to None, the class is not iterable, so calling iter() on its instances will raise a TypeError (without falling back to __getitem__())” (Data model). This lets a subclass un-inherit a behavior its parent provided — __hash__ = None is the canonical example (it makes instances unhashable).

The Special-Method-Lookup Rule — Type, Not Instance, and Why

This is the heart of the note. The reference gives the rule and a demonstration:

For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not on the instance itself. That behaviour is the reason why the following code raises an exception:

>>> class C:
...     pass
...
>>> c = C()
>>> c.__len__ = lambda: 5
>>> len(c)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type C has no len()

(Data model, “Special method lookup”)

Assigning c.__len__ = lambda: 5 puts a callable in the instance dictionary c.__dict__. But len(c) does not look there. It reads the sq_length slot off type(c) — which is NULL because C never defined __len__ in its class body — and so raises TypeError. The lambda in the instance dict is invisible to the len() machinery.

Why CPython does this — the two reasons

The reference gives both reasons, and they are worth quoting because each is a genuine design constraint:

Reason 1 — correctness. Every object inherits default dunders from object (__hash__, __repr__, __str__, __class__, …). If implicit lookup consulted the instance dict, a perfectly ordinary attribute could accidentally shadow a fundamental operation. The reference’s example:

The rationale behind this behaviour lies with a number of special methods such as __hash__() and __repr__() that are implemented by all objects to have a default behaviour. If the special method lookup did not bypass instance attributes, code like the following would not work as expected:

>>> 1 .__hash__() == hash(1)
True
>>> class C:
...     __hash__ = None
...
>>> hash(C())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'C'

(Data model, “Special method lookup”)

The __hash__ = None on the class is honored (instances are unhashable); the point is that hashing resolves through the type so that this retraction works predictably regardless of what is in any instance dict.

Reason 2 — speed. The reference: “In addition to bypassing any instance attributes in the interest of correctness, implicit special method lookup generally also bypasses the __getattribute__() method even of the object’s metaclass. This is done primarily for speed: the type slots are optimized for direct memory access by the interpreter, which would not be possible if attribute access were permitted to modify the normal procedure.” A built-in type’s __len__ is a C function pointer (sq_length) the interpreter reads with a single memory access. Routing every len() through __getattribute__ would forfeit that. The reference demonstrates that even a metaclass __getattribute__ is skipped for special-method lookup, while an explicit type(C).__getattribute__(C, '__len__') does invoke it — proving the two paths differ.

The mechanism in the source

The CPython side makes this concrete. When you define __len__ in a Python class body, the slot-fixup machinery (update_one_slot/fixup_slot_dispatchers, detailed in Type Slots and the Protocol Tables) installs the generic wrapper slot_sq_length into the type’s sq_length slot. That wrapper does the type-side lookup-and-call (Objects/typeobject.c, v3.14.5):

slot_sq_length(PyObject *self)
{
    PyObject* stack[1] = {self};
    PyObject *res = vectorcall_method(&_Py_ID(__len__), stack, 1);
    /* ... convert res to a non-negative Py_ssize_t ... */
}

vectorcall_method resolves __len__ against the type, not the instance. The general-purpose helper that all special-method lookups funnel through is _PyObject_LookupSpecial (Objects/abstract.c, v3.14.5):

PyObject *
_PyObject_LookupSpecial(PyObject *self, PyObject *attr)
{
    PyObject *res;
    res = _PyType_LookupRef(Py_TYPE(self), attr);   /* MRO of the TYPE — not self.__dict__ */
    if (res != NULL) {
        descrgetfunc f;
        if ((f = Py_TYPE(res)->tp_descr_get) != NULL) {
            Py_SETREF(res, f(res, self, (PyObject *)(Py_TYPE(self))));  /* bind as a method */
        }
    }
    return res;
}

Read line by line: _PyType_LookupRef(Py_TYPE(self), attr) walks the type’s MRO for the dunder name (it does not touch self.__dict__); if found and the result is a descriptor (has tp_descr_get — which a plain function is, so it becomes a bound method), it is bound to self. This single function — taking Py_TYPE(self) as its starting point — is the literal embodiment of “lookup on the type, not the instance.” The newer _PyObject_LookupSpecialMethod and lookup_maybe_method variants do the same _PyType_Lookup...(Py_TYPE(self), ...) while avoiding a temporary bound-method object on the hot path, but the type-side starting point is identical.

Slot ↔ Dunder Mirroring — A Brief Recap, Not a Re-Teach

Because the interpreter reads slots (not dunder names) on the hot path, there must be a binding that keeps a C slot and its Python dunder in sync. That binding is the full subject of Type Slots and the Protocol Tables, so this note only states the two facts a dunder-lookup discussion needs:

  • Dunder → slot. Defining __len__ in a class body causes update_one_slot to fill tp_as_sequence->sq_length (and tp_as_mapping->mp_length) with the generic wrapper slot_sq_length. Assigning C.__len__ = ... on an existing class re-runs update_slot from type.__setattr__, refilling the slot live across the class and its subclasses. (This is precisely why monkey-patching a dunder onto a class works but onto an instance does not — the class path updates the slot; the instance path writes into a dict nothing reads.)
  • Slot → dunder. For a C-implemented type, add_operators (run by PyType_Ready) installs a slot-wrapper descriptor in the type’s __dict__ so int.__add__ is introspectable — <slot wrapper '__add__' of 'int' objects>.

The slot_tp_*/slot_nb_*/slot_sq_* “wrappers” are the bridge in the dunder-→-slot direction: when the dunder is a Python function, the slot holds a generic wrapper (e.g. slot_sq_length, slot_nb_add, slot_tp_richcompare) that calls back into Python by name via the type. That callback is where the type-not-instance rule is mechanically enforced.

A Categorized Tour of the Dunder Families

The data model groups dunders by the protocol they implement. A self-contained survey (each links to the note that treats it in depth):

Construction and lifecycle. __new__ (static method, allocates the instance), __init__ (initializes it), __del__ (finalizer), __init_subclass__ and __set_name__ (class-creation hooks). See Object Creation new init and alloc and Metaclasses.

Representation and conversion. __repr__ (unambiguous, developer-facing — the repr() and interactive-echo target), __str__ (readable, the str()/print() target, defaulting to __repr__), __bytes__ (bytes()), __format__ (format() and f-strings), __bool__ (truthiness, defaulting to __len__ then to true — see Truthiness and Falsy Values), __hash__ (hash()).

Attribute access. __getattribute__ (called for every attribute access), __getattr__ (called only when normal lookup fails), __setattr__, __delattr__, and __dir__. Descriptor objects add __get__/__set__/__delete__ (see Descriptors). These are the machinery behind obj.attr, detailed in Attribute Lookup Mechanics.

Comparison and ordering. The six “rich comparison” dunders __lt__, __le__, __eq__, __ne__, __gt__, __ge__. “There are no swapped-argument versions of these methods … rather, __lt__() and __gt__() are each other’s reflection, __le__() and __ge__() are each other’s reflection, and __eq__() and __ne__() are their own reflection” (Data model). The dispatch and __eq__/__hash__ consistency rules are treated in Operator Overloading and the Numeric Protocols.

Container protocols. __len__, __getitem__, __setitem__, __delitem__, __contains__, __iter__, __next__, __reversed__, __length_hint__. Covered in The Sequence and Mapping Protocols and The Iterator Protocol. A notable fallback: a class with __getitem__ but no __iter__ is still iterable via the legacy sequence-iteration protocol (the interpreter feeds it integer indices 0, 1, 2, … until IndexError). The source for this is slot_tp_iter, which falls back to PySeqIter_New(self) when __iter__ is absent but has_dunder_getitem(self) is true.

Numeric protocols. __add__/__radd__/__iadd__ and the rest of the arithmetic, bitwise, and unary operators, plus __index__, __int__, __float__, __complex__. The full reflected/in-place dispatch is the sibling Operator Overloading and the Numeric Protocols.

Callable and context-manager. __call__ makes an instance behave like a function (obj()type(obj).__call__(obj)). __enter__/__exit__ implement the with statement (see The Context Manager Protocol); __aenter__/__aexit__/__await__/__aiter__/__anext__ are their async counterparts (see Coroutines and the async await Protocol).

Failure Modes and Common Misunderstandings

Setting a dunder on an instance does nothing. The canonical trap, already shown: obj.__len__ = lambda: 5 then len(obj) raises TypeError. The fix is to define the method in the class body, or type(obj).__len__ = ... to patch the class. This burns people who try to give a single object special behavior at runtime; the answer is to make a one-off subclass, not to mutate the instance.

__getattr__ does not catch implicit dunder lookups. A class with a catch-all __getattr__ that returns a callable for any name will not make len(obj) work, because implicit lookup bypasses __getattribute__/__getattr__ entirely — it reads the slot. Proxy objects (e.g. __getattr__-based wrappers) routinely fail to forward dunders for this reason, which is why frameworks that need transparent proxies (like unittest.mock or weakref.proxy) must explicitly define each dunder they want to forward.

Scope

The “implicit lookup reads the type slot and bypasses __getattribute__” rule holds for the operator/protocol dunders (__len__, __add__, __enter__, …) that have backing tp_*/nb_*/sq_*/mp_* slots. The data model itself states it with a hedge — implicit lookup “generally also bypasses the __getattribute__() method even of the object’s metaclass” — because attribute-access machinery dunders sit at the boundary (__class__ and __dict__ are exposed via the slot wrappers / tp_getset, and type(obj) reads ob_type directly, not a Python-level attribute). The note’s claim is correct for the protocol dunders this section is about; an exhaustive slotdefs[]-vs-data-model audit of every entry point is left out of scope.

Dunders are looked up freshly each operation (but cached). Reassigning C.__add__ takes effect immediately because type.__setattr__ re-runs the slot fixup and bumps the type’s version tag, invalidating the inline caches the specializing interpreter keeps. So there is no stale-cache hazard at the language level — but the type-version-tag bump is why heavily monkey-patched code can be slower (it keeps de-optimizing specialized opcodes).

Defining __eq__ silently drops __hash__. Because hashing and equality must agree, defining __eq__ without __hash__ sets __hash__ to None, making instances unhashable. This is a correctness guard (mutable-equality objects make bad dict keys), but it surprises people who add __eq__ to a value class and then can’t put it in a set. The detail belongs to Operator Overloading and the Numeric Protocols (the __eq__/__hash__ contract).

See Also