runtime
138 / 250
lesson 138 Data Models & Typing · advanced

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__.

You'll be able to
  • 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() and in with __add__, __len__ and __contains__
The idea

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) and str(x) call __str__: a friendly text for users, e.g. £17.99. Without it, Python falls back to __repr__.
  • a == b calls a.__eq__(b); a < b (and so sorted, min, max) calls __lt__; a + b calls __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.

example-1.py · Money that prints, compares and adds
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)
example-2.py · Sorting objects that can't compare
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.

example-3.py · A basket that answers len and in
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).

Review
  • __repr__ for developers (looks like code), __str__ for users (print uses it).
  • __eq__, __lt__, __add__ let your objects use ==, sorted/max and +; return NotImplemented for types you don't handle.
  • __len__ and __contains__ make len(x) and item in x work; a length of 0 is false.
  • Next: type hints, which document what every function and field expects.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Special methods: printing, comparing and operators — Data Models & Typing · Runtime