Chapters
Python114 chapters

Classes and objectsChapter 69 of 114

Magic Methods

The dunder methods that make your class work with Python's own syntax.

Python calls them for you

Methods with two underscores either side are how the language talks to your object. You never call __len__ directly; you call len() and Python calls it:

Python
class Basket:
    def __init__(self, items):
        self.items = items

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

basket = Basket(["apple", "pear"])
print(len(basket))
print(bool(basket), bool(Basket([])))

Output

2
True False

bool() falls back to __len__ when there is no __bool__, so emptiness works for free.

Printing

Python
class Money:
    def __init__(self, amount):
        self.amount = amount

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

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

price = Money(4.5)
print(price)
print(repr(price))
print([price])

Output

4.50 EUR
Money(4.5)
[Money(4.5)]

Containers use __repr__, which is why a class with only __str__ still shows addresses inside a list.

Equality, and hashing with it

Python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))

a = Point(1, 2)
print(a == Point(1, 2))
print(len({Point(1, 2), Point(1, 2)}))

Output

True
1

Comparison and sorting

Define __lt__ and sorted() works. functools.total_ordering fills in the rest from that plus __eq__:

Python
from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, major, minor):
        self.pair = (major, minor)

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

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

    def __repr__(self):
        return f"v{self.pair[0]}.{self.pair[1]}"

versions = [Version(1, 4), Version(1, 2), Version(2, 0)]
print(sorted(versions))
print(Version(1, 2) <= Version(1, 4))
print(max(versions))

Output

[v1.2, v1.4, v2.0]
True
v2.0

Arithmetic

Python
class Money:
    def __init__(self, amount):
        self.amount = amount

    def __add__(self, other):
        return Money(self.amount + other.amount)

    def __mul__(self, count):
        return Money(self.amount * count)

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

print(Money(4.5) + Money(2.25))
print(Money(4.5) * 2)

Output

6.75
9.00

Overload where the meaning is obvious. + on two amounts of money reads well; + meaning "send an email" does not.

Indexing, containment, iteration

Python
class Playlist:
    def __init__(self, songs):
        self.songs = songs

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

    def __contains__(self, song):
        return song in self.songs

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

p = Playlist(["a", "b", "c"])
print(p[1])
print(p[0:2])
print("b" in p)
print([song for song in p])

Output

b
['a', 'b']
True
['a', 'b', 'c']

Slicing came free, because __getitem__ passes the slice straight to the list.

Context managers

__enter__ and __exit__ make your object usable with with:

Python
class Section:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print("start", self.name)
        return self

    def __exit__(self, exc_type, exc, tb):
        print("end", self.name)
        return False

with Section("build"):
    print("working")

Output

start build
working
end build

__exit__ runs even when the block raises. Returning False lets the exception continue on its way, which is nearly always what you want.

Callable objects

Python
class Adder:
    def __init__(self, amount):
        self.amount = amount

    def __call__(self, value):
        return value + self.amount

add_five = Adder(5)
print(add_five(10))
print(callable(add_five))

Output

15
True

The ones worth knowing

MethodCalled by
__init__building the object
__repr__ / __str__repr() / print()
__len__len(), and bool() as a fallback
__eq__ / __hash__==, sets, dict keys
__lt__<, sorted(), max()
__getitem__obj[i], and slicing
__contains__in
__iter__for, list()
__call__obj()
__enter__ / __exit__with

Test yourself

2 questions

What happens to your class in a set if you define __eq__ but not __hash__?

Show the answer

It stops working, because __hash__ becomes None — Define both, and make them agree: objects that compare equal must hash equal.

What does returning True from __exit__ do?

Show the answer

Swallows the exception raised in the block — Almost always wrong. Return False, or nothing, so the exception continues on its way.

Next chapter

Modules

Split code across files, and pull in what the standard library already has.