Skip to content

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

MethodGives 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 iterable

The 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.