32 · Dunder methods and operator overloading
__add__, __len__, __getitem__, __call__ — the protocol that makes your objects work with Python's built-in syntax. Plus dataclasses for the modern shortcut.
**Dunder methods** (double underscore on both sides) are Python's hook system. `a + b` calls `a.__add__(b)`. `len(x)` calls `x.__len__()`. `for item in x` calls `x.__iter__()`. `x[i]` calls `x.__getitem__(i)`. `x(...)` calls `x.__call__(...)`. Implementing the right dunders makes your class behave like a built-in — readable, composable, and compatible with the whole Python ecosystem.
Without this:
Without dunders, `Vector(1, 2) + Vector(3, 4)` is a `TypeError` and `for sample in my_dataset:` fails. PyTorch tensors support `+`, `*`, and `@` (matmul) only because `torch.Tensor` implements `__add__`, `__mul__`, and `__matmul__`.
Python's data model is built on dunder methods — special methods with double underscores on both sides. When you write a + b, Python calls a.__add__(b). When you write len(x), Python calls x.__len__(). This protocol means that your custom classes can participate in any Python syntax without special compiler support — just implement the right dunders.
Key dunders to know:
__init__— initialise the instance__repr__— developer-facing string (repr(obj), printed in REPL)__str__— user-facing string (str(obj), used byprint)__eq__— equality (==); if defined without__hash__, the object becomes unhashable__hash__— allows the object to be used as a dict key or set element__len__—len(obj)__getitem__/__setitem__—obj[i]/obj[i] = v__iter__/__next__—for x in objand manual iteration__call__—obj(...)— makes an instance callable like a function__add__,__mul__,__matmul__—+,*,@
dataclasses.dataclass (Python 3.7+) auto-generates __init__, __repr__, and __eq__ from field annotations — eliminating boilerplate for simple data containers.
Python (in browser)
`__rmul__` handles the case where Python tries `3 * v1` and `int.__mul__(3, v1)` returns `NotImplemented` — Python then tries `v1.__rmul__(3)`. Without `__rmul__`, `3 * v1` raises `TypeError`. Returning `NotImplemented` (not raising) is the correct signal to Python to try the reflected operation.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
Python's `for` loop protocol checks for `__iter__` first, then falls back to calling `__getitem__(0)`, `__getitem__(1)`, ... until `IndexError`. Implementing just `__len__` and `__getitem__` is enough to make a class iterable and compatible with PyTorch's `DataLoader`, which expects exactly these two methods.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`@dataclass` reads the class-level type annotations and generates `__init__` (with defaults), `__repr__`, and `__eq__` at class creation time. No more writing the same boilerplate `self.x = x` for every field. Add `frozen=True` to make the dataclass immutable (and hashable).
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
What dunder method must a class implement to make its instances callable as functions — e.g. `result = my_obj(x)`?
Lesson mml-3 covers vectors and matrices — the mathematical objects that `+`, `*`, and `@` operate on. Operator overloading (`__add__`, `__mul__`, `__matmul__`) is precisely how NumPy and PyTorch make `a + b` and `A @ B` work on arrays and tensors — the same arithmetic syntax you use for Python scalars now applies to entire datasets.
- Dunder methods hook into Python's built-in syntax: `+` → `__add__`, `len()` → `__len__`, `obj[i]` → `__getitem__`, `for x in obj` → `__iter__`, `obj(...)` → `__call__`.
- Implementing `__len__` and `__getitem__` makes a class iterable and compatible with PyTorch's `DataLoader` — no `__iter__` required.
- `@dataclass` auto-generates `__init__`, `__repr__`, `__eq__` from field annotations. Use it for config objects, result containers, and any plain data class to eliminate boilerplate.
PyTorch tensors overload `+`, `*`, `-`, `/`, and `@` (matrix multiply) via `__add__`, `__mul__`, etc. — making `loss = predictions - targets` and `output = W @ x + b` readable. `nn.Module.__call__` wraps `forward` with gradient hooks, so `model(x)` does more than just `model.forward(x)`. PyTorch `Dataset` requires `__len__` and `__getitem__` — any class that implements them plugs into `DataLoader` with batching, shuffling, and multi-process loading for free. `@dataclass` is the standard for experiment config objects in ML codebases (Hydra, Lightning CLI, etc.).
If you remove it: Without `__len__` and `__getitem__`, PyTorch's `DataLoader` cannot batch your dataset. Without `__call__`, `model(x)` fails — you'd have to write `model.forward(x)` everywhere, missing all the gradient-hook infrastructure `nn.Module.__call__` provides.