Special Methods

Special methods — also called "dunder" methods because they're surrounded by double underscores — let your classes work with Python's built-in syntax: print(), ==, +, len(), in, indexing, for loops and with blocks. They're how built-in types do it, too.

Representation

PY repr.py
class Money:
    def __init__(self, amount, currency="USD"):
        self.amount = amount
        self.currency = currency

    def __repr__(self):
        return f"Money({self.amount!r}, {self.currency!r})"

    def __str__(self):
        return f"{self.amount:,.2f} {self.currency}"

    def __format__(self, spec):
        return format(self.amount, spec or ",.2f") + " " + self.currency

m = Money(1234.5)
print(m)
print(repr(m))
print(f"Total: {m:.0f}")
Output
1,234.50 USD
Money(1234.5, 'USD')
Total: 1234 USD

Comparison and equality

PY compare.py
from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, text):
        self.parts = tuple(int(p) for p in text.split("."))

    def __eq__(self, other):
        return self.parts == other.parts

    def __lt__(self, other):
        return self.parts < other.parts

    def __hash__(self):
        return hash(self.parts)

    def __repr__(self):
        return ".".join(map(str, self.parts))

versions = [Version("1.10.0"), Version("1.2.3"), Version("1.9")]
print(sorted(versions))
print(Version("2.0") > Version("1.99"), Version("1.0") == Version("1.0"))
print(len({Version("1.0"), Version("1.0")}))
Output
[1.2.3, 1.9, 1.10.0]
True True
1

Without __eq__, two objects are only equal if they're the same object. @total_ordering fills in <=, > and >= from __eq__ and __lt__. If you define __eq__, also define __hash__ so objects can go in sets and dictionaries.

Arithmetic operators

PY arithmetic.py
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    __rmul__ = __mul__

    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

a = Vector(1, 2)
b = Vector(3, 4)
print(a + b)
print(a * 3, 3 * a)
print(abs(b))
Output
Vector(4, 6)
Vector(3, 6) Vector(3, 6)
5.0

__rmul__ handles the reversed form 3 * a, where the left operand is a number that doesn't know about vectors.

Containers: len, in, indexing, iteration

PY container.py
class Deck:
    ranks = "A23456789TJQK"

    def __init__(self):
        self.cards = [r + s for s in "♠♥" for r in self.ranks]

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

    def __getitem__(self, index):
        return self.cards[index]

    def __contains__(self, card):
        return card in self.cards

    def __iter__(self):
        return iter(self.cards)

deck = Deck()
print(len(deck))
print(deck[0], deck[-1], deck[2:5])
print("Q♥" in deck, "Q♦" in deck)
print(sum(1 for card in deck if card[0] in "JQK"))
Output
26
A♠ K♥ ['3♠', '4♠', '5♠']
True False
6

With __getitem__ supporting slices, the deck behaves much like a list. Python even makes it iterable automatically, though an explicit __iter__ is clearer.

Truthiness and calling

PY bool_call.py
class Inbox:
    def __init__(self, messages):
        self.messages = messages

    def __bool__(self):
        return bool(self.messages)

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, value):
        return value * self.factor

print("New mail!" if Inbox(["hi"]) else "Empty", "|", "New mail!" if Inbox([]) else "Empty")
triple = Multiplier(3)
print(triple(14))
Output
New mail! | Empty
42

Context managers: __enter__ and __exit__

Objects used in a with statement set something up on entry and are guaranteed to clean up on exit — even if an error occurs:

PY context.py
import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        print("timer started")
        return self

    def __exit__(self, exc_type, exc, tb):
        elapsed = time.perf_counter() - self.start
        print(f"timer stopped ({'fast' if elapsed < 1 else 'slow'})")
        return False

with Timer() as t:
    total = sum(range(100_000))
print(total)
Output
timer started
timer stopped (fast)
4999950000

Files, locks and database connections all work this way. Returning False from __exit__ lets any exception continue propagating. The contextlib.contextmanager decorator offers a shorter way to write simple context managers using a generator.

Common special methods

MethodCalled by
__init__Cls(...)
__repr__, __str__, __format__repr(), str()/print(), f-strings
__eq__, __lt__, __hash__==, <, sets and dict keys
__add__, __sub__, __mul__…+, -, *…
__len__, __getitem__, __contains__, __iter__len(), obj[i], in, for
__bool__, __call__if obj:, obj(...)
__enter__, __exit__with

Try it yourself

Try it yourself
Console output