Advanced · Lesson 56 of 94

The data model: dunder methods

Watch this lesson

View full playlist ↗

Read the lesson

What you'll learn

Concept

Every operator and every built-in in Python is a method call underneath. a + b is a.__add__(b). len(x) is x.__len__(). x[i] is x.__getitem__(i). print(obj) calls obj.__str__(), and obj == other calls obj.__eq__(other). These double-underscore methods, the data model, are the contract between your classes and the language. Implement the right ones and your objects sort, print, compare, iterate, and add like the built-ins do. You have already met __init__, __repr__, __iter__, __enter__, and __exit__.

The data model: dunder methods: a + b → a.__add__(b) → len(x) → x.__len__() → x[i → x.__getitem__(i) → a == b → a.__eq__(b) → print(x) → x.__str__() → falls back to __repr__
The data model: dunder methods · Figure 1 · Open for full size

Two methods control how an object appears. __repr__ is for developers: unambiguous, ideally the expression that recreates the object, shown in the REPL, in lists, and in tracebacks. __str__ is for users: friendly text, used by print and f-strings. If you define only __repr__, print falls back to it, so a class with a good __repr__ is never embarrassing. Define __str__ only when the user-facing form should differ.

You want Implement Notes
readable in REPL and logs __repr__ make it look like the constructor call
friendly print __str__ optional; falls back to __repr__
== by value __eq__ return NotImplemented for foreign types
use as a dictionary key / in a set __hash__ must agree with __eq__; only for immutable objects
<, sorting, min, max __lt__ (+ functools.total_ordering) total_ordering fills in the rest
len(x), x[i], in __len__, __getitem__, __contains__ __getitem__ alone also enables iteration
+, *, - __add__, __mul__, __sub__ (+ __radd__ etc.) return a new object; NotImplemented if unsupported
bool(x), if x: __bool__ (or __len__) default is always truthy

Ordering needs __lt__ at minimum, and functools.total_ordering derives the other comparisons from __lt__ and __eq__. For containers, __len__ and __getitem__ are enough for len(), indexing, slicing if you handle slices, in, and even iteration, because Python falls back to indexing from zero until IndexError. Arithmetic dunders return new objects and never mutate; the reflected forms like __radd__ handle 3 + obj when the left operand does not know your type, and returning NotImplemented from an unsupported combination gives a clean TypeError instead of a wrong answer.

Equality and hashing travel together. By default two instances are equal only if they are the same object. Define __eq__ to compare by value, and return NotImplemented when the other object is a type you do not understand, so Python can try the reverse comparison. Once you define __eq__, Python removes the default __hash__, because equal objects must hash equally; define __hash__ too, built from the same fields, if the object is immutable and you want it usable as a dictionary key or set member. Mutable objects should stay unhashable.

The data model: dunder methods: define __eq__ by value → define __hash__ from the same fields → usable as a dictionary key → leave __hash__ = None → unhashable, which is correct
The data model: dunder methods · Figure 2 · Open for full size

Code

__repr__ versus __str__, and what each context uses. The list shows reprs; print and the f-string show the friendly form.

# repr_str.py
class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

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

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


price = Money(12.5, "EUR")
print(price)
print(f"total: {price}")
print(repr(price))
print([price, Money(3, "USD")])
Output
12.50 EUR
total: 12.50 EUR
Money(12.5, 'EUR')
[Money(12.5, 'EUR'), Money(3, 'USD')]

Value equality and hashing for an immutable object, with NotImplemented for foreign types. Equal money is one set member; a mutable version stays unhashable.

# eq_hash.py
class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

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

    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return (self.amount, self.currency) == (other.amount, other.currency)

    def __hash__(self):
        return hash((self.amount, self.currency))


a, b = Money(5, "EUR"), Money(5, "EUR")
print(a == b, a is b, a == "5 EUR")
print(len({a, b, Money(7, "EUR")}))
print({a: "coffee"}[b])


class MutablePoint:
    def __init__(self, x):
        self.x = x

    def __eq__(self, other):
        return isinstance(other, MutablePoint) and self.x == other.x


print(MutablePoint(1) == MutablePoint(1))
{MutablePoint(1)}
Output
True False False
2
coffee
True
Traceback (most recent call last):
  File "eq_hash.py", line 33, in <module>
    {MutablePoint(1)}
TypeError: cannot use 'MutablePoint' as a set element (unhashable type: 'MutablePoint')

Ordering with total_ordering, plus arithmetic that returns new objects and refuses mismatched currencies cleanly.

# ordering_math.py
from functools import total_ordering


@total_ordering
class Money:
    def __init__(self, amount, currency):
        self.amount, self.currency = amount, currency

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

    def __eq__(self, other):
        return isinstance(other, Money) and (self.amount, self.currency) == (other.amount, other.currency)

    def __lt__(self, other):
        if not isinstance(other, Money) or other.currency != self.currency:
            return NotImplemented
        return self.amount < other.amount

    def __add__(self, other):
        if not isinstance(other, Money) or other.currency != self.currency:
            return NotImplemented
        return Money(self.amount + other.amount, self.currency)

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

    __rmul__ = __mul__


wallet = [Money(5, "EUR"), Money(1, "EUR"), Money(3, "EUR")]
print(sorted(wallet), max(wallet))
print(Money(1, "EUR") <= Money(1, "EUR"), Money(2, "EUR") > Money(1, "EUR"))
print(Money(2, "EUR") + Money(3, "EUR"), 3 * Money(2, "EUR"))
print(Money(1, "EUR") + Money(1, "USD"))
Output
[Money(1, 'EUR'), Money(3, 'EUR'), Money(5, 'EUR')] Money(5, 'EUR')
True True
Money(5, 'EUR') Money(6, 'EUR')
Traceback (most recent call last):
  File "ordering_math.py", line 35, in <module>
    print(Money(1, "EUR") + Money(1, "USD"))
          ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for +: 'Money' and 'Money'

A container class: __len__, __getitem__ with slice support, __contains__, and __bool__, which together give len, indexing, slicing, in, iteration, and truthiness.

# container.py
class Deck:
    def __init__(self, cards):
        self._cards = list(cards)

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

    def __getitem__(self, index):
        result = self._cards[index]
        return Deck(result) if isinstance(index, slice) else result

    def __contains__(self, card):
        return card in self._cards

    def __repr__(self):
        return f"Deck({self._cards})"


deck = Deck(["A", "K", "Q", "J", "10"])
print(len(deck), deck[0], deck[-1])
print(deck[1:3])
print("Q" in deck, "2" in deck)
print([card.lower() for card in deck])
print(bool(deck), bool(Deck([])))
Output
5 A 10
Deck(['K', 'Q'])
True False
['a', 'k', 'q', 'j', '10']
True False

Try it yourself

Create vector.py with an immutable Vector(x, y) class. Give it a __repr__, value __eq__ and __hash__, __add__ and __sub__ between vectors, __mul__ and __rmul__ by a number, __abs__ for the length, and __bool__ that is False for the zero vector. Sort a list of vectors by length using key=abs, put duplicates in a set to de-duplicate them, and check that adding a vector to a string raises a clean TypeError.

Common mistakes

What's next

Your classes can speak the language's own vocabulary. Next you will stop writing that boilerplate by hand: dataclasses generate it, and Enum names your fixed choices.

Course overview