Create your own
Lesson illustration

How Built-in Operations Dispatch to Special Methods

Welcome to the first module of the course. We begin with Python’s data model: the protocol layer that lets ordinary-looking syntax such as len(x), x[i], x + y, for x in items, and with resource: work across many unrelated types.

This lesson focuses on one deceptively important distinction: built-in operations often resemble calls such as obj.__len__(), but Python does not generally perform ordinary instance attribute lookup when it invokes a special method implicitly. Understanding that distinction will make later work with descriptors, attribute hooks, and metaclasses much less mysterious.

By the end, you should be able to trace a built-in operation to its special method and explain why putting a special method only on one instance does not reliably affect the corresponding syntax.


Special methods are protocol entry points

A special method is a method with a name such as __len__, __getitem__, or __add__. Python defines these names as protocol hooks: implementations of language-level operations.

For example:

Python operationMain special method
len(x)__len__
x[index]__getitem__
x + y__add__ (and sometimes __radd__)
repr(x)__repr__
bool(x)__bool__, with a possible __len__ fallback
for item in x__iter__, then __next__ on the iterator

The usual first approximation is useful:

Likewise:

The word roughly is doing essential work. It is not generally equivalent to:

That last expression is an explicit attribute access followed by a call. It uses the ordinary attribute-lookup machinery. len(x) is an implicit protocol invocation performed by the interpreter, with special lookup rules.

Consider a small domain-oriented type:

class JobBatch:
    def __init__(self, job_ids: list[str]) -> None:
        self.job_ids = list(job_ids)

    def __len__(self) -> int:
        return len(self.job_ids)


batch = JobBatch(["job-101", "job-102", "job-103"])

print(len(batch))                  # 3
print(batch.__len__())             # 3
print(type(batch).__len__(batch))  # 3

All three expressions currently produce 3, but they reach that result by different routes:

  1. batch.__len__() performs normal lookup for the attribute __len__ on batch, then calls what it finds.
  2. type(batch).__len__(batch) explicitly retrieves __len__ from the class and passes the instance yourself.
  3. len(batch) asks the interpreter to perform the length protocol. It looks for the relevant special method on the object’s type, following special-method lookup rules.

For everyday class design, that third route is the one that matters. If you want your object to support len, define __len__ in the class body.


Read the language rule, not just the approximation

The official data-model documentation distinguishes the friendly “roughly equivalent” explanation from the actual semantics, including the instance-namespace bypass.

3. Data model — Python 3.14.2 documentation

Read the official Python documentation’s introduction to special methods, then its section on special-method lookup. This is the authoritative account of why syntax and built-ins do not simply call an instance attribute.

In Section 3.3, read the opening explanation beginning with special-method syntax. Focus on the wording “roughly equivalent” in the __getitem__ example. Then read all of Section 3.3.13, “Special method lookup.” Start with the per-instance __len__ example, continue through the explanation of metaclass confusion, and finish with the __getattribute__ demonstration. In particular, study why attribute hooks are bypassed.

Keep two statements from that reading in mind:

  • Define special methods on the class, not by assigning them to a single instance.
  • An implicit operation such as len(x) can bypass both the instance dictionary and a custom __getattribute__ hook.

The experiment: explicit lookup versus implicit dispatch

Run the following as one file or in a REPL. Its purpose is to make the two lookup paths visibly diverge.

class JobBatch:
    def __init__(self, job_ids: list[str]) -> None:
        self.job_ids = list(job_ids)

    def __len__(self) -> int:
        return len(self.job_ids)


batch = JobBatch(["job-101", "job-102", "job-103"])

# A normal, per-instance attribute assignment:
batch.__len__ = lambda: 10_000

print(vars(batch))
print(batch.__len__())             # 10000
print(len(batch))                  # 3
print(type(batch).__len__(batch))  # 3

The instance dictionary now contains an attribute named __len__:

{
    "job_ids": ["job-101", "job-102", "job-103"],
    "__len__": <function ...>
}

So why does len(batch) still return 3?

batch.__len__() is ordinary attribute access. Python checks the normal attribute-lookup path and finds the __len__ function stored directly in batch’s namespace. Since a function stored directly on an instance is not automatically bound like a class-defined function, the zero-argument lambda works exactly as written.

len(batch), however, is not asking, “What value does this particular object expose under the name __len__?” It is asking, “How does this type of object implement the length protocol?” The answer comes from JobBatch.__len__, so the per-instance replacement is ignored.

You can confirm that changing the class implementation does affect the protocol:

def archived_batch_length(self: JobBatch) -> int:
    return 0


JobBatch.__len__ = archived_batch_length

print(batch.__len__())  # 10000: explicit instance lookup still finds the lambda
print(len(batch))       # 0: implicit protocol lookup sees the class method

Do not mutate special methods on live classes in production code like this; it makes behavior global and difficult to reason about. It is useful here because it isolates the rule:

Instance assignment affects normal attribute access. Class definition affects implicit special-method dispatch.

The same principle applies to special methods used by operators and built-ins. For example, an instance-level __getitem__ assignment is not the reliable way to make obj[key] work. Define __getitem__ on the class instead.


Trace a built-in operation systematically

When a built-in operation behaves unexpectedly, use this trace rather than guessing.

  1. Identify the protocol operation.
    For len(batch), the relevant hook is __len__. For batch[0], it is __getitem__.

  2. Identify the type.
    Inspect type(batch), not only vars(batch).

  3. Find the class-level implementation.
    Inspect the class and its base classes. For the current example, JobBatch.__len__ is the relevant implementation.

  4. Account for operation-specific rules.
    Some protocols have fallbacks or multiple participants. For example, addition may involve __add__ and then __radd__; iteration involves __iter__ and an iterator’s __next__. len(x) is comparatively direct: it needs a valid __len__ implementation.

  5. Check the protocol contract.
    A found method can still fail if it returns an invalid result.

For example, Python finds BrokenBatch.__len__, but rejects its result:

class BrokenBatch:
    def __len__(self) -> int:
        return 3.5  # type: ignore[return-value]


len(BrokenBatch())

This raises a TypeError: a length must be integer-like, not a floating-point number. Special methods are not arbitrary callbacks. Each one has a semantic and return-value contract set by its protocol.

This is a useful design constraint for the future job-service capstone. If a JobBatch advertises a length, callers should be able to trust it as a count of contained jobs, not as an estimate, a database query object, or a formatted string.


Ordinary attribute lookup is a different mechanism

The difference is easier to retain if you separate these two questions:

  • Explicit lookup: “What attribute named name does this object expose?”
  • Implicit protocol dispatch: “How does this object’s type implement operation ?”

The first question follows ordinary attribute rules. The second uses special-method lookup rules.

This flowchart depicts the normal resolution of an explicit attribute such as `instance.foobar`: data descriptors are considered before the instance dictionary, followed by class attributes, non-data descriptors, and finally `__getattr__`. It explains the route used by `batch.__len__()`, not the special-method route used by `len(batch)`.

You will study descriptors and each branch of this flowchart in Module 2. For now, the key point is that this ordinary machinery can produce a different answer from an implicit operation.

A custom __getattribute__ method makes the contrast concrete:

class ObservableLength:
    def __getattribute__(self, name: str):
        print(f"ordinary lookup: {name}")
        return super().__getattribute__(name)

    def __len__(self) -> int:
        return 4


value = ObservableLength()

print(value.__len__())
print(len(value))

The first call produces output similar to:

ordinary lookup: __len__
4

The second call produces:

4

There is no ordinary lookup: __len__ line for len(value). Python invokes the length protocol without routing the special-method lookup through the instance’s __getattribute__.

This does not mean special methods can never access instance state. The selected class method still receives self, so its body may freely use normal attributes:

class JobBatch:
    def __init__(self, job_ids: list[str]) -> None:
        self.job_ids = list(job_ids)

    def __len__(self) -> int:
        return len(self.job_ids)

The bypass concerns finding __len__ itself. Once Python has selected JobBatch.__len__, that method can access self.job_ids through the usual rules.


Why bypass the instance namespace?

At first glance, respecting batch.__len__ may seem more flexible. Python deliberately chooses correctness and predictable language semantics instead.

The important edge case involves classes themselves. A class object is also an object. For example, int is an instance of the metaclass type.

int.__hash__ is an attribute describing how to hash integer instances. It is not automatically the method for hashing the class object int itself. The class object’s hash behavior belongs to its type, type.

Conceptually:

hash(1)       # hashes an int instance
hash(int)     # hashes the class object int

type(1).__hash__(1)
type(int).__hash__(int)

If implicit special-method lookup behaved exactly like ordinary lookup on the object, Python could retrieve a special method from the wrong semantic level: a method intended for instances of a class rather than for the class object. The documentation calls this failure mode metaclass confusion.

By looking up implicit special methods through the object’s type rather than through the object’s ordinary instance namespace, Python preserves the distinction between:

  • an object and the behavior defined for its type;
  • a class object and behavior supplied by its metaclass.

A practical secondary benefit is speed. Built-ins such as len, operators, iteration, hashing, and representation occur constantly. CPython can optimize their well-defined type-level dispatch paths more aggressively than arbitrary, interceptable instance attribute access.

The trade-off is intentional: special methods are less dynamically interceptable than ordinary methods. If you need per-instance behavioral variation, prefer an explicit strategy object, callback attribute, or normal method. Do not rely on assigning a dunder method to one object and expecting operators or built-ins to honor it.


What “roughly equivalent” does and does not promise

Use the equivalence as a reading aid:

len(x)

is usefully understood as a request for the behavior implemented by:

type(x).__len__(x)

But avoid treating it as a literal source transformation. Python operations may have extra protocol rules:

  • a + b can try reflected behavior on b if the left-side implementation cannot handle the operation.
  • a += b can use an in-place special method before falling back to regular addition behavior.
  • Truth testing can use __bool__, then possibly fall back to __len__.
  • Iteration involves an iterable producing an iterator, then repeated retrieval of next values.

The stable design rule remains simple: implement a protocol by defining its special method on the class. Treat direct calls such as obj.__len__() as ordinary method calls for debugging or exploration, not as a perfect model of what Python syntax must do.


Takeaways

  • Python’s built-ins and operators are driven by special-method protocols such as __len__, __getitem__, and __add__.
  • len(x) is usefully approximated by type(x).__len__(x), not by x.__len__().
  • Explicit x.__len__() uses ordinary attribute lookup and can see an instance attribute named __len__.
  • Implicit special-method lookup typically bypasses the instance namespace and __getattribute__, so special methods should be defined on the class.
  • This behavior prevents metaclass confusion and gives the interpreter room to optimize common operations.

Next, we will move one stage earlier in an object’s lifetime: constructing an immutable value type and enforcing its invariants in __new__, before normal initialization takes place.

Can't find a good explanation? Sign up and we'll make it for you

Sign up