Frontend Labs
⋯

16. The data model: making your objects feel built in

Python's syntax is a set of hooks. len(x) calls x.__len__(), x[i] calls x.__getitem__(i), a + b calls a.__add__(b), with x: calls __enter__/__exit__. These are the dunder (double underscore) methods. Implement them and your objects work with every built-in function and syntax, which is how NumPy arrays support + and @, and how PyTorch tensors support slicing.

16.1 Real-world example: a 2-D vector

import math
from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Vec:
    x: float
    y: float

    def __add__(self, other: "Vec") -> "Vec":
        return Vec(self.x + other.x, self.y + other.y)

    def __mul__(self, k: float) -> "Vec":           # v * 3
        return Vec(self.x * k, self.y * k)

    __rmul__ = __mul__                              # 3 * v

    def __matmul__(self, other: "Vec") -> float:    # v @ w: dot product
        return self.x * other.x + self.y * other.y

    def __abs__(self) -> float:                     # abs(v)
        return math.hypot(self.x, self.y)

    def __bool__(self) -> bool:                     # if v:
        return bool(self.x or self.y)

    def __iter__(self):                             # x, y = v
        yield self.x
        yield self.y

v, w = Vec(3, 4), Vec(1, 0)
print(v + w, 2 * v, v @ w, abs(v), bool(Vec(0, 0)))
x, y = v
print(x, y)

16.2 Containers: __len__, __getitem__, __contains__

class Dataset:
    """A tiny map-style dataset, the same shape torch.utils.data.Dataset expects."""

    def __init__(self, rows: list[tuple[str, int]]) -> None:
        self._rows = rows

    def __len__(self) -> int:
        return len(self._rows)

    def __getitem__(self, i: int) -> tuple[str, int]:
        return self._rows[i]             # also makes the object iterable and sliceable via the list

ds = Dataset([("refund please", 1), ("love it", 0), ("broken on arrival", 1)])
print(len(ds), ds[0], ds[-1])
for text, label in ds:                   # iteration falls back to __getitem__(0, 1, 2...)
    print(label, text)

That is the entire contract of a PyTorch map-style dataset (Ch 58): two methods. The DataLoader does not care what class you are, only that len() and indexing work. This is duck typing: "if it walks like a duck and quacks like a duck, it is a duck."

16.3 Equality and hashing

If you define __eq__, define __hash__ consistently: equal objects must have equal hashes, or dicts and sets will silently misbehave. A mutable object should not be hashable at all (its hash would change while it sits in a set). @dataclass(frozen=True) generates both correctly; @dataclass with eq=True and not frozen sets __hash__ = None, making instances unhashable, which is the safe choice.

16.4 __repr__ versus __str__

repr(x) is for developers: unambiguous, ideally valid Python (Vec(x=3, y=4)). str(x) is for end users (print, f-strings). If you implement only one, implement __repr__. Your logs and debugger will thank you.

16.5 Context managers as classes

import threading

class Timeout:
    """Raise TimeoutError in the main thread's next check if the block runs too long."""

    def __init__(self, seconds: float) -> None:
        self.seconds = seconds
        self.expired = threading.Event()

    def __enter__(self) -> "Timeout":
        self._timer = threading.Timer(self.seconds, self.expired.set)
        self._timer.start()
        return self

    def __exit__(self, exc_type, exc, tb) -> bool:
        self._timer.cancel()
        return False                  # False: do not swallow exceptions from the block

with Timeout(0.5) as t:
    total = sum(range(100_000))
    print("expired?", t.expired.is_set())

__exit__ receives the exception (if any). Returning True suppresses it, which is almost never what you want.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36