Why this matters
Why does len() work on lists, strings and dicts? Why can you write a + b for numbers but also for strings and lists, with different meanings? Python's built-in operations are not hard-wired to built-in types. Each one calls a method with a special name, such as __len__ or __add__. Define those methods on your own class and your objects work with len, +, ==, in, for, sorted and print just like built-in ones. This is called the data model, and it is what makes Python code read so naturally.
The idea: syntax becomes method calls
Every operator and many built-in functions translate into a call on the operand's type:
| You write | Python calls |
|---|---|
repr(x), REPL display | type(x).__repr__(x) |
str(x), print(x), f-string {x} | type(x).__str__(x) |
x == y | type(x).__eq__(x, y) |
x < y | type(x).__lt__(x, y) |
x + y | type(x).__add__(x, y) |
len(x) | type(x).__len__(x) |
item in x | type(x).__contains__(x, item) |
x[i] | type(x).__getitem__(x, i) |
for v in x | type(x).__iter__(x) |
bool(x), if x: | type(x).__bool__(x) (or __len__) |
They are nicknamed dunder methods (double underscore). You implement them; you rarely call them directly. Notice that Python looks them up on the type, not the instance: setting obj.__len__ = ... on one object does nothing for len(obj).
You can check this with a built-in:
>>> (3).__add__(4)
7
>>> "ab".__len__()
2
>>> [1, 2].__contains__(2)
True
repr and str
Without them, your objects print unhelpfully:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
print(Point(1, 2))
<__main__.Point object at 0x7f3a2c1b6e40>
That default comes from object.__repr__, the class every class ultimately inherits from. Add two methods:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
def __str__(self):
return f"({self.x}, {self.y})"
p = Point(1, 2)
print(p) # uses __str__
print(repr(p)) # uses __repr__
print([p, p]) # containers show their items' repr
(1, 2)
Point(1, 2)
[Point(1, 2), Point(1, 2)]
The convention:
__repr__is for programmers: unambiguous, ideally valid Python that would recreate the object. It is what you see in the REPL, in tracebacks and in debugger output. Always write one; it makes debugging far easier.__str__is for end users: readable. If you do not define it,str()falls back to__repr__.
The !r in an f-string applies repr() to the value, which is why {self.name!r} gives 'Ada' with quotes.
Equality: eq and NotImplemented
By default, == on your objects means identity (is): two separate Point(1, 2) objects are unequal. Define __eq__ to compare by value:
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
NotImplemented is a special built-in value (not the exception NotImplementedError). Returning it means "I do not know how to compare with that". Python then gives the other operand a chance (it tries other.__eq__(self)), and if that also declines, == falls back to identity and returns False. So Point(1, 2) == "hello" is simply False instead of crashing.
You get != for free: Python's default __ne__ inverts __eq__.
Note: Defining
__eq__sets__hash__toNone, making instances unhashable (they cannot be dict keys or set members). That is deliberate: objects that are equal must have equal hashes, and Python cannot guess yours. Lesson 5 (dataclasses) and the internals module come back to this.
Arithmetic: add and the reflected radd
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, k):
if not isinstance(k, (int, float)):
return NotImplemented
return Vector(self.x * k, self.y * k)
def __rmul__(self, k):
return self.__mul__(k)
v = Vector(1, 2) + Vector(10, 20)
print(v, v * 3, 3 * v)
Vector(11, 22) Vector(33, 66) Vector(33, 66)
The full dispatch for a + b:
1. r = type(a).__add__(a, b) if r is not NotImplemented: done
2. r = type(b).__radd__(b, a) if r is not NotImplemented: done
3. TypeError: unsupported operand type(s) for +: 'A' and 'B'
3 * v works because int.__mul__(3, v) returns NotImplemented (an int does not know vectors), so Python calls Vector.__rmul__(v, 3). Operators should return new objects and leave their operands unchanged; += uses __iadd__ if you define it, otherwise it falls back to __add__ and rebinds the name.
Ordering: lt and friends
sorted(), min(), max() and list.sort() only need <, i.e. __lt__:
class Task:
def __init__(self, priority, name):
self.priority, self.name = priority, name
def __repr__(self):
return f"Task({self.priority}, {self.name!r})"
def __lt__(self, other):
if not isinstance(other, Task):
return NotImplemented
return self.priority < other.priority
print(sorted([Task(3, "c"), Task(1, "a"), Task(2, "b")]))
[Task(1, 'a'), Task(2, 'b'), Task(3, 'c')]
For x > y, Python tries x.__gt__(y) and then the reflection y.__lt__(x), so defining __lt__ alone also gives you >. For the full set (<=, >=), define them or use the functools.total_ordering decorator, which fills in the rest from __eq__ and one ordering method.
Containers: len, contains, getitem, iter
class Deck:
def __init__(self, cards):
self._cards = list(cards)
def __len__(self):
return len(self._cards)
def __getitem__(self, i):
return self._cards[i]
def __contains__(self, card):
return card in self._cards
def __iter__(self):
return iter(self._cards)
d = Deck(["AS", "KH", "7C"])
print(len(d), d[0], d[-1], "KH" in d, d[1:])
for c in d:
print(c, end=" ")
print()
3 AS 7C True ['KH', '7C']
AS KH 7C
Slicing works because d[1:] passes a slice object to __getitem__, which the inner list understands. Python also has fallbacks: without __iter__, a for loop will call __getitem__(0), __getitem__(1), ... until IndexError; without __contains__, in iterates. Without __bool__, truthiness uses __len__ (zero length is false). Explicit methods are clearer and faster. The iterator protocol behind __iter__ gets a full lesson in the next module.
Common mistakes
- Returning a non-string from
__repr__/__str__:TypeError: __str__ returned non-string (type int). - Raising instead of returning
NotImplemented, or returningFalsefrom__eq__for foreign types: it blocks the other operand from handling the operation. - Writing
raise NotImplemented: that is the value, not an exception. You getTypeError: exceptions must derive from BaseException. For "subclass must implement this", raiseNotImplementedError. - Mutating
selfin__add__:a + bchangingasurprises everyone. Return a new object. - Objects disappearing from sets after defining
__eq__:TypeError: unhashable type: 'Point'. Define__hash__consistently, or use a frozen dataclass.
Recap
- Operators and built-ins call dunder methods on the operand's type:
len->__len__,+->__add__,==->__eq__,in->__contains__,for->__iter__. - Always define
__repr__(for programmers); add__str__for a user-facing form. - Return
NotImplementedfor operand types you do not handle; Python then tries the reflected method (__radd__, the other side's__eq__). sortedneeds only__lt__. Defining__eq__removes the default hash.
Going further: read "Data model", section 3.3 "Special method names" in the Python Language Reference, and look at functools.total_ordering.