Special methods

Make your own objects work with the language itself: printing, comparing, length, iteration and the operators.

15of 24
≈ 13Min read
5Parts

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.

py
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.

py
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.

py
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.

py
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

Practice