Special methods: printing, comparing and operators
Methods with double-underscore names are how Python's own syntax reaches your objects: print uses __str__, == uses __eq__, sorted uses __lt__, + uses __add__, len and in use __len__ and __contains__.
- Write
__repr__for developers and__str__for users - Define
__eq__and__lt__so objects compare by value and can be sorted, returning NotImplemented for other types - Support
+,len()andinwith__add__,__len__and__contains__
Python's syntax calls methods with special names, so a class can join in:
repr(x), and printing a list of objects, call__repr__: aim for something that looks like the code to recreate the object, e.g.Money(1799, 'GBP').print(x)andstr(x)call__str__: a friendly text for users, e.g.£17.99. Without it, Python falls back to__repr__.a == bcallsa.__eq__(b);a < b(and sosorted,min,max) calls__lt__;a + bcalls__add__.
When the other value is a type your method doesn't handle, return the special value NotImplemented. Python then tries the other operand's method, and if that fails too, == falls back to identity (False) and + raises TypeError, which is what you want for money + "cake".
Run each example to see what it prints. Running examples doesn't count toward progress.
class Money: def __init__(self, cents, currency="GBP"): self.cents = cents self.currency = currency def __repr__(self): return f"Money({self.cents}, {self.currency!r})" def __str__(self): symbol = {"GBP": "£", "USD": "$", "EUR": "€"}[self.currency] return f"{symbol}{self.cents // 100}.{self.cents % 100:02d}" def __eq__(self, other): if not isinstance(other, Money): return NotImplemented return (self.cents, self.currency) == (other.cents, other.currency) def __add__(self, other): if not isinstance(other, Money) or other.currency != self.currency: return NotImplemented return Money(self.cents + other.cents, self.currency) price = Money(1799) shipping = Money(350) print(price + shipping) print(repr(price + shipping), [price, shipping]) print(price == Money(1799, "GBP"), price == Money(1799, "EUR"), price == 17.99)
class Version: def __init__(self, text): self.parts = tuple(int(part) for part in text.split(".")) releases = [Version("1.10.0"), Version("1.9.2"), Version("2.0.0")] print(len(releases)) print(sorted(releases))
Containers have their own hooks. len(x) calls __len__, which must return a whole number; item in x calls __contains__. A bonus: an object with __len__ is false in an if when its length is 0, just like an empty list.
class Basket: def __init__(self, customer_id): self.customer_id = customer_id self.quantities = {} def add_item(self, sku, quantity=1): self.quantities[sku] = self.quantities.get(sku, 0) + quantity def __len__(self): return sum(self.quantities.values()) def __contains__(self, sku): return sku in self.quantities def __repr__(self): return f"Basket({self.customer_id!r}, {len(self)} items)" basket = Basket("C00077") basket.add_item("SPO-11681", 2) basket.add_item("HOM-22847") print(len(basket), "SPO-11681" in basket, "TOY-54564" in basket) print(basket) if basket: print("basket is not empty") print(bool(Basket("C01338")))
When to use it: __repr__ on almost every class you write (it makes debugging far easier); __eq__ for value-like objects; ordering and arithmetic only where they have one obvious meaning. Dataclasses (see Data Models & Typing) write __repr__ and __eq__ for you, and @dataclass(order=True) adds the comparisons.
Mistakes to avoid: __str__ or __repr__ that print instead of returning text; raising or returning False for an unknown type instead of returning NotImplemented; and defining __eq__ on a mutable class you then put in a set (Python removes the default hash when __eq__ is defined, so it becomes unhashable).
__repr__for developers (looks like code),__str__for users (print uses it).__eq__,__lt__,__add__let your objects use==,sorted/maxand+; return NotImplemented for types you don't handle.__len__and__contains__makelen(x)anditem in xwork; a length of 0 is false.- Next: type hints, which document what every function and field expects.