Skip to content
Lesson 3 of 9

Step 1 of 7 · Reading · ~3 min

Learn

Object-Oriented Programming

Special Methods and Dataclasses

By the end you can make your own class work with print, +, ==, sorted, set, len, in, for and with, and let @dataclass write the boilerplate.

You have been using dunder-powered syntax all along: len(x), x in y, for, with open(...), ==. Each calls a double-underscore method on the object; a namedtuple got its == and repr that way. Here you write them yourself.

Text: __repr__ and __str__

repr(x) is for developers and should be unambiguous. str(x) and print are for users and fall back to __repr__ when __str__ is missing.

python

Operators return new objects

a + b calls a.__add__(b). Return a new object, never mutate self. If a cannot handle b, return NotImplemented (a value, not an exception) so Python tries b.__radd__(a) before giving up.

python

sum([Money(100), Money(250)]) starts at 0 + Money(100), which is why __radd__ handles 0; the result is Money(350). Money(5) + "x" gives TypeError: unsupported operand type(s) for +: 'Money' and 'str'.

Equality and hashing

Defining __eq__ sets __hash__ to None:

python

The contract: equal objects must have equal hashes, so hash exactly the fields __eq__ compares. Break it and sets quietly miscount:

python

Ordering

Write __eq__ and __lt__, and @functools.total_ordering fills in <=, > and >=. Compare tuples of numbers, never strings: "10" < "9" is True, (10,) < (9,) is False.

Truthiness and containers

bool(x) uses __bool__, then __len__ (zero is falsy). len, [], in and for map to __len__, __getitem__, __contains__ and __iter__. Delegate to an inner list: __iter__ returns iter(self._items).

python

__call__ makes an instance callable: c(5) runs Counter.__call__(c, 5).

Context managers

with X() as s: calls __enter__ (its result is s), then always __exit__(exc_type, exc, tb), even when the body raises. A falsy return lets the exception continue; True swallows it.

python

@dataclass

For classes that mostly hold data, @dataclass generates __init__, __repr__ and __eq__ from annotated fields. Annotations declare fields but are not enforced.

python

tags: list = [] raises ValueError: mutable default ... use default_factory, the shared-default trap from functions. order=True adds ordering by field order. frozen=True blocks assignment (FrozenInstanceError) and makes instances hashable; plain @dataclass sets __hash__ to None, like your own __eq__.

Common mistakes

  • __eq__ with no __hash__: set(items) raises TypeError: unhashable type.
  • A __hash__ over other fields than __eq__: equal objects both stay in the set, so the count is too high.
  • Sorting by text: 1.10 lands before 1.9 because "1" == "1" and then "1" < "9".
  • __add__ that raises on a foreign type instead of returning NotImplemented, so __radd__ never gets a chance.

Your exercise

Implement Version so equality, hashing and ordering all use the parsed numbers; 1.0 and 1.0.0 must be one set member.

Up nextGenerators and yieldGenerators and Iterators

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…