Learning Goals
3 minBy the end of this lesson you can:
- Define
__add__(self, other)soa + bworks. - Define
__len__(self)solen(obj)works. - Define
__contains__(self, item)sox in objworks. - Get a taste of
__iter__forfor x in obj:.
Warm-Up · The Arithmetic Operators
5 minEvery Python operator is secretly a dunder call:
a + b a.__add__(b) a - b a.__sub__(b) a * b a.__mul__(b) a / b a.__truediv__(b) a // b a.__floordiv__(b) a % b a.__mod__(b) a ** b a.__pow__(b) len(x) x.__len__() bool(x) x.__bool__() x in y y.__contains__(x) for x in y y.__iter__()
You've been using these on built-in types (1 + 2, len("hi")) for two levels. Today you teach them to your classes.
Python operators are a uniform interface. a + b works on numbers, strings, lists — and on your custom class once you define __add__. The whole language is dunders all the way down.
New Concept · Arithmetic, Length, Membership
14 min__add__ — the + operator
class Vector: def __init__(self, x, y): self.x = x; self.y = y def __add__(self, other): return Vector(self.x + other.x, self.y + other.y) def __repr__(self): return f"Vector({self.x}, {self.y})" print(Vector(1, 2) + Vector(3, 4)) # → Vector(4, 6)
Returns a new Vector — never modifies self. That's the "immutable + returns new" convention. Same way 1 + 2 doesn't change 1; it gives back 3.
The full arithmetic family
class Vector: def __init__(self, x, y): self.x = x; self.y = y def __add__(self, other): return Vector(self.x + other.x, self.y + other.y) def __sub__(self, other): return Vector(self.x - other.x, self.y - other.y) def __mul__(self, scalar): return Vector(self.x * scalar, self.y * scalar)
Notice __mul__ takes a number, not another Vector — there are two reasonable interpretations of vector multiplication, and scalar multiplication is the simple one.
__len__ — the len() builtin
class Inventory: def __init__(self): self.items = [] def add(self, item): self.items.append(item) def __len__(self): return len(self.items) inv = Inventory() inv.add("sword"); inv.add("potion") print(len(inv)) # → 2
__len__ must return a non-negative integer. Once defined, bool(inv) also works — an inventory with zero items is falsy.
__contains__ — the in operator
class Inventory: # ... existing ... def __contains__(self, item): return item in self.items inv = Inventory() inv.add("sword") print("sword" in inv) # → True print("dragon" in inv) # → False
__iter__ — the for loop
Make your class iterable by returning an iterator from __iter__. The easiest way: delegate to an existing iterable.
class Inventory: # ... existing ... def __iter__(self): return iter(self.items) inv = Inventory() inv.add("sword"); inv.add("potion"); inv.add("scroll") for item in inv: print(" ", item) # sword # potion # scroll
Now Inventory behaves like a list: len, in, for x in all work. Plus list(inv), sum(inv), max(inv), comprehensions — anything that iterates.
The reverse · __radd__
If you do 3 + my_vector instead of my_vector + 3, Python first asks int.__add__(3, my_vector). If that returns NotImplemented, Python tries my_vector.__radd__(3) — the "reflected" add.
class Vector: # ... existing ... def __radd__(self, other): # other is the left operand. e.g. sum([v1, v2, v3]) starts at 0 if other == 0: return self return self.__add__(other)
The if other == 0 trick lets sum([v1, v2]) work — sum starts from 0 and tries to add a Vector. Without __radd__, you'd get TypeError: unsupported operand type(s) for +: 'int' and 'Vector'.
Worked Example · Money Class
12 minA practical use case: a Money class that prevents accidental currency mixing. Save as money.py:
# money.py — Money with arithmetic, comparison, length-like ops from functools import total_ordering @total_ordering class Money: def __init__(self, amount, currency="RM"): self.amount = round(float(amount), 2) self.currency = currency def __repr__(self): return f"Money({self.amount}, {self.currency!r})" def __str__(self): return f"{self.currency} {self.amount:.2f}" def _check(self, other): if self.currency != other.currency: raise ValueError( f"Currency mismatch: {self.currency} vs {other.currency}") def __add__(self, other): self._check(other) return Money(self.amount + other.amount, self.currency) def __sub__(self, other): self._check(other) return Money(self.amount - other.amount, self.currency) def __mul__(self, n): return Money(self.amount * n, self.currency) def __radd__(self, other): if other == 0: return self raise TypeError("Can only add Money to Money or 0") def __eq__(self, other): if not isinstance(other, Money): return NotImplemented return (self.amount, self.currency) == (other.amount, other.currency) def __lt__(self, other): self._check(other) return self.amount < other.amount def __hash__(self): return hash((self.amount, self.currency)) a = Money(8, "RM") b = Money(3.50, "RM") print(a + b) # → RM 11.50 print(b * 3) # → RM 10.50 print(a > b) # → True # sum() works because of __radd__ basket = [Money(5), Money(3), Money(2.50)] print("Total:", sum(basket)) # → RM 10.50 # Currency safety gbp = Money(10, "GBP") try: a + gbp except ValueError as e: print(f"Refused: {e}")
Output
RM 11.50 RM 10.50 True Total: RM 10.50 Refused: Currency mismatch: RM vs GBP
Read the diff
Money supports +, -, * (by a number), comparison, and is sum-able. Currency mismatch raises an error — exactly the kind of invariant from PY-L3-10. The __radd__ exists solely to make sum([...]) work. _check is a private helper used by every arithmetic operator that requires matching currencies.
Try It Yourself
13 minAdd a dot method to Vector that returns self.x * other.x + self.y * other.y. Test on Vector(1, 2).dot(Vector(3, 4)).
Hint
def dot(self, other): return self.x * other.x + self.y * other.y print(Vector(1, 2).dot(Vector(3, 4))) # → 11
This is a method, not a dunder — there's no standard · operator in Python for dot products.
Build Inventory from the concept section. Define __len__, __contains__ and __iter__. Test all three.
Hint
class Inventory: def __init__(self): self._items = [] def add(self, item): self._items.append(item) def __len__(self): return len(self._items) def __contains__(self, item): return item in self._items def __iter__(self): return iter(self._items) inv = Inventory() for item in ["sword", "potion", "scroll"]: inv.add(item) print(len(inv)) # 3 print("scroll" in inv) # True for x in inv: print(x) # sword, potion, scroll print(list(inv)) # works because of __iter__ print(sorted(inv)) # works too
Add __add__ so inv_a + inv_b returns a new Inventory with items from both.
Hint
def __add__(self, other): if not isinstance(other, Inventory): return NotImplemented combined = Inventory() combined._items = self._items + other._items return combined a = Inventory(); a.add("sword") b = Inventory(); b.add("potion"); b.add("scroll") c = a + b print(list(c)) # ['sword', 'potion', 'scroll'] print(len(c)) # 3
Returns a fresh Inventory — never modifies self or other. That's the same convention as [1, 2] + [3, 4] for lists.
Mini-Challenge · Polynomial Class
8 minBuild poly.py. A Polynomial class that stores coefficients in a list — index = power. So Polynomial([3, 1, 2]) represents 3 + x + 2x².
Define:
__init__(coeffs)__repr__— print as e.g.3 + x + 2x²__add__— add two polynomials (pad shorter with zeros)__call__(x)— evaluate the polynomial atx__len__— return the degree + 1 (number of coefficients)
Test: p = Polynomial([3, 1, 2]); print(p(2)) should give 3 + 2 + 8 = 13.
Show one possible solution
# poly.py — Polynomial class class Polynomial: def __init__(self, coeffs): self.coeffs = coeffs def __repr__(self): terms = [] for i, c in enumerate(self.coeffs): if c == 0: continue if i == 0: terms.append(str(c)) elif i == 1: terms.append(f"{c}x" if c != 1 else "x") else: terms.append(f"{c}x^{i}" if c != 1 else f"x^{i}") return " + ".join(terms) if terms else "0" def __add__(self, other): n = max(len(self.coeffs), len(other.coeffs)) result = [0] * n for i, c in enumerate(self.coeffs): result[i] += c for i, c in enumerate(other.coeffs): result[i] += c return Polynomial(result) def __call__(self, x): return sum(c * (x ** i) for i, c in enumerate(self.coeffs)) def __len__(self): return len(self.coeffs) p = Polynomial([3, 1, 2]) # 3 + x + 2x² q = Polynomial([1, 0, 0, 5]) # 1 + 5x³ print(p) # 3 + x + 2x^2 print(q) # 1 + 5x^3 print(p + q) # 4 + x + 2x^2 + 5x^3 print(p(2)) # 3 + 2 + 8 = 13 print(len(p)) # 3
Non-negotiables: 4 dunders + __init__, accurate evaluation, sensible repr. __call__ is a special dunder that lets your instance be called like a function — p(2). We'll meet it more in PY-L3-26's iterators.
Recap
3 minOperator dunders make your class feel native. __add__ wires up +; __len__ wires up len(); __contains__ wires up in; __iter__ wires up for. __radd__ handles the reverse direction so sum([a, b]) works. Arithmetic dunders should return a new instance — never mutate self or other. The whole Python language is built on this pattern: define the right dunder and your class plugs into the rest.
Vocabulary Card
- __add__ / __sub__ / __mul__
- Wire up arithmetic operators. Return a new instance.
- __len__
- Wire up
len(). Must return a non-negative integer. Also affects truthiness. - __contains__
- Wire up the
inoperator. - __iter__
- Wire up
for x in obj:. Easiest:return iter(self._items). - __radd__
- Right-hand-side add. Needed for
sum([...])to work on your class.
Homework
4 minBuild playlist.py. A Playlist class wrapping a list of Songs. Define:
__init__— takes optional starting list.__len__— number of songs.__contains__— does a given song exist?__iter__— iterate songs.__add__— combine two playlists.__repr__— meaningful debug print.
Test: build two playlists, combine, loop through, check membership, get length.
Sample · playlist.py
class Playlist: def __init__(self, songs=None): self.songs = list(songs) if songs else [] def __len__(self): return len(self.songs) def __contains__(self, song): return song in self.songs def __iter__(self): return iter(self.songs) def __add__(self, other): if not isinstance(other, Playlist): return NotImplemented return Playlist(self.songs + other.songs) def __repr__(self): return f"Playlist({self.songs})" morning = Playlist(["Levitating", "Cupid"]) evening = Playlist(["Bohemian Rhapsody", "Dynamite"]) day = morning + evening print(len(day)) # 4 print("Cupid" in day) # True for song in day: print(" ", song) print(day) # Playlist([...])
Non-negotiables: 5 dunders making Playlist behave like a Pythonic collection. list(songs) if songs else [] avoids the mutable-default trap from PY-L3-04.