The protocol idea
Python’s built-in operations are all defined in terms of methods with double underscores around the name. len(x) calls x.__len__(). a + b calls a.__add__(b). for x in thing calls thing.__iter__(). Nothing is special-cased for the built-in types — they implement the same methods you can.
So "making an object work like a list" is not a metaphor. Implement the methods and every piece of code that expects a sequence will accept yours.
The two string methods
__repr__ is for the programmer: unambiguous, and ideally something you could paste back in. __str__ is for the reader. If you write only one, write __repr__ — str() falls back to it, and it is the one you see in a debugger, a list and a traceback.
class Money:
def __init__(self, amount, currency="USD"):
self.amount, self.currency = amount, currency
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})" # for you
def __str__(self):
return f"{self.amount:.2f} {self.currency}" # for the user
m = Money(12.5)
print(m) # 12.50 USD → __str__
m # Money(12.5, 'USD') → __repr__
[m] # [Money(12.5, 'USD')] — containers always use __repr__
Equality and hashing travel together
Defining __eq__ sets __hash__ to None, making the object unhashable — Python assumes that if you changed what equality means, the old hash is wrong. That is correct: the contract is that equal objects have equal hashes, and breaking it means a set or dict quietly loses things.
So: if the object is immutable, define both. If it is mutable, define __eq__ and leave it unhashable.
Returning NotImplemented (not False) from __eq__ lets Python try the other operand’s method before giving up.
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented # let the other side try
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y)) # equal objects MUST hash the same
Sizing, containing, iterating, indexing
__getitem__ alone is enough for for to work, because the old iteration protocol falls back to indexing from 0 — but define __iter__ when you can, it is clearer and faster. Without __bool__, truthiness falls back to __len__, which is usually what you want anyway.
class Deck:
def __init__(self, cards):
self._cards = list(cards)
def __len__(self): return len(self._cards) # len(deck)
def __getitem__(self, i): return self._cards[i] # deck[0], deck[1:3]
def __contains__(self, c): return c in self._cards # "ace" in deck
def __iter__(self): return iter(self._cards) # for card in deck
def __bool__(self): return len(self._cards) > 0 # if deck:
Operators and ordering
sorted, min and max need only __lt__. @total_ordering derives the other three comparisons from __eq__ and __lt__ — four methods you do not have to keep consistent by hand.
def __add__(self, other): ... # a + b
def __sub__(self, other): ... # a - b
def __mul__(self, n): ... # a * n
def __lt__(self, other): ... # a < b — and sorted() only needs this one
from functools import total_ordering
@total_ordering
class Version:
def __eq__(self, other): ...
def __lt__(self, other): ...
# <=, >, >= are filled in for you