Lesson 2 of 9 · 20m
The Data Model: __dunder__ Methods That Make Classes Behave
len(), +, in, iteration — how Python's protocols let your own types behave like builtins.
Python's magic is not syntax — it is a set of protocols. len(x) works on lists, dicts, strings, and your own classes because each type implements __len__. x in y calls __contains__, x + y calls __add__, for i in y calls __iter__. Once you see that these "builtins" are just methods with double-underscore names, you can make your own types behave like the builtins — and other people's code can use yours without knowing what they are.
A real example: a read-only collection
class TagSet:
def __init__(self, tags):
self._tags = frozenset(tags)
def __contains__(self, tag):
return tag in self._tags
def __len__(self):
return len(self._tags)
def __iter__(self):
return iter(self._tags)Now len(tags), "python" in tags, and for tag in tags all work on your class — no documentation needed, because Python callers already know the protocol.
The ones you will use constantly
| Method | Gives you |
|---|---|
__len__ | len(obj) |
__contains__ | x in obj |
__iter__ | for x in obj |
__getitem__ | obj[i], obj[key] |
__eq__ | obj == other |
__repr__ | the string you see in REPL/errors |
__enter__/__exit__ | with obj: |
__repr__ is the one everyone forgets
A good __repr__ shows enough state to debug at a glance: User(id=41, name="traven"), not <User object at 0x7f2a...>. Write __repr__ on every class you write — it is the single highest-value dunder for your own sanity.
Try it yourself — the protocol idea, in JS
// JavaScript has the same idea with Symbol.iterator:
const tags = { values: ["python","web","sec"], [Symbol.iterator]: function*() { yield* this.values; } };
console.log([...tags]); // spreads because the object is iterableThe takeaway
You do not write __dunder__ methods to be clever — you write them so len(), in, for, and with work on your type exactly like they do on builtins. That is what makes Python code feel like Python.