Special methods
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__:
1class Crate:2 def __len__(self):3 print("__len__ was called")4 return 45 6crate = Crate()7print(len(crate))
__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:
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())
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.
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])
[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__.
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])
print uses __str__ for gold itself, giving gold. A list shows its items with __repr__, giving [Ore('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:
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")
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:
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))
[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.
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)
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:
| Operator | Method |
|---|---|
a + b | a.__add__(b) |
a - b | a.__sub__(b) |
a * b | a.__mul__(b) |
a / b | a.__truediv__(b) |
a // b | a.__floordiv__(b) |
a % b | a.__mod__(b) |
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)
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:
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)
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:
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)
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:
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}")
[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.
A printable house
✓Give House a __str__ method so that print(home) shows the line below.
Ada's house, 2 floors
1class House:2 def __init__(self, owner, floors):3 self.owner = owner4 self.floors = floors5 6home = House("Ada", 2)7print(home)
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.
Mill Road Dock Lane Market
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)
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.
Coins(gold=5, silver=15)
1from dataclasses import dataclass2 3class Coins:4 pass5 6print(Coins(3, 10) + Coins(2, 5))
What you learned
- Special methods (dunder methods) are called by Python for built-in functions and operators:
len(x)runsx.__len__(). __str__is forprintand 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 allsorted,minandmaxneed.__len__,__getitem__and__contains__givelen(x),x[key]andin;__add__,__mul__and friends give arithmetic.@dataclasswrites__init__,__repr__and__eq__from a list of fields;order=Trueadds comparisons,frozen=Truemakes objects read-only.