Kodewick
Lesson 28 of 31Contents

Special methods

Lesson 28 of 31 · Part 5 · Objects · about 20 minutes

Make a Lamp object and print it, and real Python shows something like <__main__.Lamp object at 0x7f3a2c1b9e50>: the class name and a memory address, different on every run. Compare two lamps that look the same with ==, and you get False. Your class doesn't know how to do those things yet. Special methods teach it.

Special methods have two underscores on each side, like __init__ from lesson 26. People call them "dunder" methods, for double underscore. You rarely call them yourself. Python calls them for you when you use built-in functions and operators. Here len() calls the class's __len__:

main.py
1class Crate:2    def __len__(self):3        print("__len__ was called")4        return 45 6crate = Crate()7print(len(crate))
Output
__len__ was called
4

len(x) runs x.__len__(), a + b runs a.__add__(b), and so on; the built-in types like str and int work the same way. So when your own class defines __len__ or __add__, len() and + work on its objects too.

__str__: how print shows it

__str__ returns the text that print(), str() and f-strings use for your object:

main.py
1class Lamp:2    def __init__(self, street, brightness):3        self.street = street4        self.brightness = brightness5 6    def __str__(self):7        return f"Lamp on {self.street} ({self.brightness}/10)"8 9mill = Lamp("Mill Road", 3)10print(mill)11print(f"Tonight: {mill}")12print(str(mill).upper())
Output
Lamp on Mill Road (3/10)
Tonight: Lamp on Mill Road (3/10)
LAMP ON MILL ROAD (3/10)

__str__ must return a string, not print it.

__repr__: the developer's view

__repr__ returns a more exact text, meant for programmers: ideally it looks like the code that would make the object. Python uses it where precision matters: inside lists and dictionaries, in the repr() function, and with {x!r} or {x=} in an f-string. If a class has only __repr__, print uses that too.

main.py
1class Lamp:2    def __init__(self, street, brightness):3        self.street = street4        self.brightness = brightness5 6    def __repr__(self):7        return f"Lamp({self.street!r}, {self.brightness})"8 9lamps = [Lamp("Mill Road", 3), Lamp("Market", 7)]10print(lamps)11print(lamps[0])
Output
[Lamp('Mill Road', 3), Lamp('Market', 7)]
Lamp('Mill Road', 3)

{self.street!r} puts the street in quotes, just as Python writes a string in code. Press Step and look at the variables panel: now each lamp is shown by its __repr__.

Guess first

What does this print?

1class Ore:2    def __init__(self, name):3        self.name = name4 5    def __str__(self):6        return self.name7 8    def __repr__(self):9        return f"Ore({self.name!r})"10 11gold = Ore("gold")12print(gold, [gold])

Tip: If you write only one of the two, write __repr__: it is used everywhere __str__ is missing.

__eq__: when two objects are equal

Without __eq__, == asks "are these the very same object?". Two separate objects with the same attributes are not equal. __eq__(self, other) decides for itself:

main.py
1class Plain:2    def __init__(self, name):3        self.name = name4 5class Ore:6    def __init__(self, name, weight):7        self.name = name8        self.weight = weight9 10    def __eq__(self, other):11        if not isinstance(other, Ore):12            return NotImplemented13        return self.name == other.name and self.weight == other.weight14 15print(Plain("iron") == Plain("iron"))16print(Ore("iron", 3) == Ore("iron", 3))17print(Ore("iron", 3) != Ore("gold", 3))18print(Ore("iron", 3) in [Ore("gold", 1), Ore("iron", 3)])19print(Ore("iron", 3) == "iron")
Output
False
True
True
True
False

!= and in use your __eq__ too. Returning NotImplemented for a different type means "I don't know how to compare with that"; for ==, Python then falls back to False instead of crashing.

__lt__ and sorting

__lt__ is "less than", the < operator. Once a class has it, sorted(), .sort(), min() and max() work on its objects without a key:

main.py
1class Cart:2    def __init__(self, name, load):3        self.name = name4        self.load = load5 6    def __lt__(self, other):7        return self.load < other.load8 9    def __repr__(self):10        return f"Cart({self.name!r}, {self.load})"11 12carts = [Cart("A", 7), Cart("B", 2), Cart("C", 5)]13print(sorted(carts))14print(min(carts), max(carts))15print(Cart("D", 1) < Cart("E", 9))
Output
[Cart('B', 2), Cart('C', 5), Cart('A', 7)]
Cart('B', 2) Cart('A', 7)
True

Python can also work out a > b from b < a, which is how max copes with only __lt__. The other comparisons have their own methods: __le__ (<=), __gt__ (>) and __ge__ (>=).

__len__, __getitem__ and __contains__

These three make your object act like a container: len(x), x[key] and item in x.

main.py
1class Warehouse:2    def __init__(self):3        self.stock = {}4 5    def store(self, ore, amount):6        self.stock[ore] = self.stock.get(ore, 0) + amount7 8    def __len__(self):9        return len(self.stock)10 11    def __getitem__(self, ore):12        return self.stock.get(ore, 0)13 14    def __contains__(self, ore):15        return self.stock.get(ore, 0) > 016 17depot = Warehouse()18depot.store("iron", 5)19depot.store("gold", 2)20depot.store("iron", 3)21print(len(depot), depot["iron"], depot["coal"])22print("gold" in depot, "coal" in depot)
Output
2 8 0
True False

len must return a whole number that is 0 or more. Inside __getitem__ you decide what a missing key means: here it is 0 rather than a KeyError.

Arithmetic operators

Each arithmetic operator has its own method. It gets the value on the right as other and usually returns a new object, leaving both sides unchanged:

OperatorMethod
a + ba.__add__(b)
a - ba.__sub__(b)
a * ba.__mul__(b)
a / ba.__truediv__(b)
a // ba.__floordiv__(b)
a % ba.__mod__(b)
main.py
1class Load:2    def __init__(self, iron, gold):3        self.iron = iron4        self.gold = gold5 6    def __add__(self, other):7        return Load(self.iron + other.iron, self.gold + other.gold)8 9    def __mul__(self, times):10        return Load(self.iron * times, self.gold * times)11 12    def __repr__(self):13        return f"Load(iron={self.iron}, gold={self.gold})"14 15morning = Load(3, 1)16evening = Load(2, 4)17print(morning + evening)18print(morning * 3)19print(morning)
Output
Load(iron=5, gold=5)
Load(iron=9, gold=3)
Load(iron=3, gold=1)

other doesn't have to be the same class: __mul__ here takes a plain number. Trying an operator that has no method gives a TypeError, such as "unsupported operand type(s) for -".

dataclass writes them for you

Many classes just hold a few values and need __init__, __repr__ and __eq__ that all look the same. The dataclasses module writes them for you. Put @dataclass on the line above the class (the @ is a decorator, lesson 30) and list the fields:

main.py
1from dataclasses import dataclass2 3@dataclass4class Ore:5    name: str6    weight: int = 17 8iron = Ore("iron", 3)9print(iron)10print(iron == Ore("iron", 3))11print(Ore("gold"))12print(iron.weight)
Output
Ore(name='iron', weight=3)
True
Ore(name='gold', weight=1)
3

Each field is a name with a type after the colon (type hints are lesson 31; here they tell dataclass what the fields are). A field with = has a default, and those must come after the ones without. For a list or dictionary, use field(default_factory=list), so that each object gets a list of its own (the same trap as mutable defaults in lesson 17). __post_init__ runs right after the generated __init__, for anything extra:

main.py
1from dataclasses import dataclass, field2 3@dataclass4class Cart:5    name: str6    ores: list = field(default_factory=list)7 8    def __post_init__(self):9        self.name = self.name.title()10 11a = Cart("north cart")12b = Cart("south cart")13a.ores.append("iron")14print(a)15print(b)
Output
Cart(name='North Cart', ores=['iron'])
Cart(name='South Cart', ores=[])

order=True adds <, <=, > and >=, comparing the fields in order, so sorted works. frozen=True makes the objects read-only:

main.py
1from dataclasses import dataclass2 3@dataclass(order=True, frozen=True)4class Spot:5    x: int6    y: int7 8spots = [Spot(2, 5), Spot(1, 9), Spot(2, 1)]9print(sorted(spots))10home = Spot(0, 0)11try:12    home.x = 413except Exception as e:14    print(f"Error: {e}")
Output
[Spot(x=1, y=9), Spot(x=2, y=1), Spot(x=2, y=5)]
Error: cannot assign to field 'x'

order=True compares x first and looks at y only when the x values are equal. Assigning to home.x raises a FrozenInstanceError (caught here with try, from lesson 21): a frozen object can't be changed after it's made. That suits values that should stay put, like a spot on the map.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

A printable house

Give House a __str__ method so that print(home) shows the line below.

It should print
Ada's house, 2 floors
exercise1.py
1class House:2    def __init__(self, owner, floors):3        self.owner = owner4        self.floors = floors5 6home = House("Ada", 2)7print(home)
Output
Press Run or Ctrl+Enter
Exercise 2

Sort the lamps

Give Lamp a __lt__ method that compares brightness, so that sorted(lamps) sorts from dimmest to brightest. Print the streets in that order.

It should print
Mill Road
Dock Lane
Market
exercise2.py
1class Lamp:2    def __init__(self, street, brightness):3        self.street = street4        self.brightness = brightness5 6lamps = [Lamp("Market", 7), Lamp("Mill Road", 3), Lamp("Dock Lane", 5)]7for lamp in sorted(lamps):8    print(lamp.street)
Output
Press Run or Ctrl+Enter
Exercise 3

Coins as a dataclass

Turn Coins into a dataclass with fields gold and silver (both int), and give it an __add__ that returns a new Coins with both fields added. Print the sum below.

It should print
Coins(gold=5, silver=15)
exercise3.py
1from dataclasses import dataclass2 3class Coins:4    pass5 6print(Coins(3, 10) + Coins(2, 5))
Output
Press Run or Ctrl+Enter

What you learned

  • Special methods (dunder methods) are called by Python for built-in functions and operators: len(x) runs x.__len__().
  • __str__ is for print and f-strings; __repr__ is the exact view used in lists and when __str__ is missing.
  • __eq__ decides ==; without it, == only matches the very same object.
  • __lt__ gives <, which is all sorted, min and max need.
  • __len__, __getitem__ and __contains__ give len(x), x[key] and in; __add__, __mul__ and friends give arithmetic.
  • @dataclass writes __init__, __repr__ and __eq__ from a list of fields; order=True adds comparisons, frozen=True makes objects read-only.