Most Python developers have used __init__ and maybe __str__. But Python's data model goes much deeper — and once you understand it, you stop fighting the language and start working with it.
What the Data Model Actually Is
When you write len(my_list), Python doesn't call some method on a list object. It calls list.__len__(my_list). When you write a + b, Python calls a.__add__(b). When you iterate with for x in obj, Python calls obj.__iter__().
Every operator, built-in function, and language construct maps to a dunder method. The data model is just the full list of these hooks — and implementing them on your own classes makes them behave like first-class Python citizens.
A Practical Example
Say you're building a Vector class:
class Vector:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
Without any dunder methods, this class is barely useful:
v1 = Vector(2, 3)
v2 = Vector(1, 4)
v1 + v2 # TypeError: unsupported operand type(s) for +
len(v1) # TypeError: object of type 'Vector' has no len()
print(v1) # <__main__.Vector object at 0x10f3a2b50>
Now add the hooks:
import math
class Vector:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Vector({self.x}, {self.y})"
def __add__(self, other: "Vector") -> "Vector":
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar: float) -> "Vector":
return Vector(self.x * scalar, self.y * scalar)
def __abs__(self) -> float:
return math.sqrt(self.x ** 2 + self.y ** 2)
def __bool__(self) -> bool:
return bool(abs(self))
def __len__(self) -> int:
return 2 # a 2D vector always has 2 components
Now it behaves naturally:
v1 + v2 # Vector(3, 7)
v1 * 3 # Vector(6, 9)
abs(v1) # 3.605...
bool(Vector(0, 0)) # False
len(v1) # 2
Same class. Completely different experience.
The Most Useful Dunders You're Probably Not Using
__getitem__ and __setitem__ — make your object subscriptable:
def __getitem__(self, index):
if index == 0: return self.x
if index == 1: return self.y
raise IndexError("Vector index out of range")
# now you can do:
v[0] # 2.0
x, y = v # unpacking works too, via __getitem__
__contains__ — enables the in operator:
def __contains__(self, value):
return value in (self.x, self.y)
# 2.0 in v → True
__call__ — makes instances callable like functions:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
double(5) # 10
double(21) # 42
This is how decorators, transformers, and callable middleware objects work under the hood.
__repr__ vs __str__
These are easy to confuse:
__repr__is for developers — unambiguous, ideally valid Python to recreate the object__str__is for end users — readable, human-friendly
import datetime
d = datetime.date(2025, 1, 20)
repr(d) # datetime.date(2025, 1, 20) ← you could eval() this
str(d) # '2025-01-20' ← readable
If you only implement one, implement __repr__. Python falls back to it when __str__ is missing.
Context Managers: __enter__ and __exit__
The with statement is just syntax sugar over these two methods:
class Timer:
def __enter__(self):
import time
self.start = time.perf_counter()
return self
def __exit__(self, *args):
self.elapsed = time.perf_counter() - self.start
with Timer() as t:
expensive_operation()
print(f"Took {t.elapsed:.3f}s")
No need to import contextlib for simple cases — just implement the protocol directly.
Why This Matters
The real insight is that Python has no privileged types. A list is not special — it just implements __len__, __getitem__, __iter__, and friends. Your classes can do the exact same thing.
This is what people mean when they say Python is a "protocol-based" language. There's no inheritance required, no interface to declare. If your object has __iter__ and __next__, it's an iterator. If it has __enter__ and __exit__, it's a context manager. Duck typing, formalized.
Understanding the data model doesn't just make you a better Python developer — it changes how you design APIs. Instead of asking "what methods should this class have?", you start asking "what protocols should this class speak?"
The full reference is in the Python docs — worth reading once end-to-end. Most of it will only click once you've needed it in practice.