Dunder Methods, Object Protocols & Operator Overloading

The Python Data Model is powered by Dunder (Double-Underscore) Methods. Rather than using hardcoded syntax or keywords, CPython maps operations (indexing a[i], iteration for x in a, context blocks with a, comparisons a == b) to C-level type slots (PyTypeObject).

This chapter details CPython’s PyTypeObject slot dispatch, operator overloading protocols, rich comparison dunders, container emulation (__getitem__, __len__), and the functools.total_ordering decorator.


1. CPython Slot Architecture (PyTypeObject)

In CPython’s C source code, every type object (PyTypeObject) contains a struct filled with function pointers called C-Slots:

CPython PyTypeObject C-Slot Dispatch:

Python Operation         CPython C-Slot Pointer           Dunder Fallback
-----------------------------------------------------------------------------
str(obj)           -->   tp_repr / tp_str           -->   __str__ / __repr__
len(obj)           -->   sq_length / mp_length      -->   __len__
obj[key]           -->   mp_subscript / sq_item     -->   __getitem__
iter(obj)          -->   tp_iter                    -->   __iter__
hash(obj)          -->   tp_hash                    -->   __hash__

When you execute len(obj), CPython skips string method lookups in obj.__dict__ and directly invokes the mp_length or sq_length C function pointer stored in obj->ob_type->tp_as_sequence.


2. Rich Comparison Protocols (__eq__, __lt__, NotImplemented)

Comparison operators (==, !=, <, <=, >, >=) map to rich comparison dunder methods:

class Currency:
    def __init__(self, amount: float, code: str):
        self.amount = amount
        self.code = code

    def __eq__(self, other):
        if not isinstance(other, Currency):
            return NotImplemented  # Signals CPython to try other.__eq__(self)
        return self.amount == other.amount and self.code == other.code

    def __lt__(self, other):
        if not isinstance(other, Currency) or self.code != other.code:
            return NotImplemented
        return self.amount < other.amount

@functools.total_ordering Helper:

Implementing all 6 comparison dunders creates repetitive boilerplate. Decorating a class with @total_ordering requires defining only __eq__ and one ordering method (__lt__, __le__, __gt__, or __ge__); the decorator generates the remaining comparison methods automatically.


3. Container & Sequence Protocols (__getitem__, __len__, __contains__)

Implementing Python’s sequence and mapping protocols allows custom classes to seamlessly integrate with standard Python functions:

  • __len__(self): Returns element count.
  • __getitem__(self, key): Enables indexing (obj[0]) and slice evaluation (obj[1:5]).
  • __contains__(self, item): Enables in membership testing. If __contains__ is absent, CPython falls back to an $O(N)$ linear scan using __getitem__.
class CustomDeck:
    def __init__(self, cards: list[str]):
        self._cards = cards

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, index):
        return self._cards[index] # Enables indexing, slicing, AND iteration!

4. Production Trade-offs & Protocol Safety

  • NotImplemented vs NotImplementedError: Always return NotImplemented from comparison dunders when encountering incompatible types. Raising NotImplementedError crashes execution immediately and breaks Python’s fallback comparison mechanism (b.__eq__(a)).
  • __repr__ vs __str__: __str__ is for user-friendly display (print()); __repr__ is for unambiguous developer debugging (repr()). Rule of thumb: eval(repr(obj)) == obj where possible.
Display Options
Appearance
Text Size
100%