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): Enablesinmembership 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
NotImplementedvsNotImplementedError: Always returnNotImplementedfrom comparison dunders when encountering incompatible types. RaisingNotImplementedErrorcrashes 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)) == objwhere possible.