The Python data model is the set of protocols that connect language syntax to method calls on objects. When you write len(items), a + b, or for x in container, Python is calling a specific special method behind the scenes on your object.
This consistent mapping between syntax and methods is the heart of Python's design. Your custom collection can work with the len function, your custom number type can work with the + and * operators, and your custom object can decide what str(obj) and repr(obj) return.
The article on special methods in Python covers individual methods in detail. This article explains the data model itself: what protocols are, how the mapping works, and why the system is designed this way.
How a built-in function becomes a method call
Every built-in operation follows the same pattern. Python calls a special method on the object. That method returns a value the built-in function or syntax then uses.
You can see this by calling both the built-in function and the special method it delegates to. The results are identical.
items = [10, 20, 30]
length = len(items)
length_via_method = items.__len__()
print(length)
print(length_via_method)The built-in len function delegates to __len__ on the list object. Both call paths produce the same result, confirming the direct mapping between function and method.
3
3Always use the built-in len rather than calling the dunder method directly. The built-in adds type checking and handles edge cases, and the special method is only the implementation hook, not the public API.
The same pattern applies to str(obj) and repr(obj). Implementing __str__ and its repr counterpart on your own classes lets you control how your objects display.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point({self.x}, {self.y})"
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
p = Point(3, 4)
print(str(p))
print(repr(p))The str function calls that method for a human-readable coordinate format. The repr function calls its own method to produce a debugging-focused representation with the class name and constructor arguments.
Point(3, 4)
Point(3, 4)Protocols: the duck-typing foundation
A protocol is an informal interface defined by a set of magic methods. Any object that implements those methods qualifies for the associated operations. You do not need to inherit from a specific base class or register with a framework.
The iteration protocol is the clearest example. An object is iterable if it implements __iter__ and returns an iterator, and it is an iterator if it also implements a next method that returns items or raises StopIteration.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for num in Countdown(3):
print(num)The Countdown class does not inherit from any special base class. It simply implements the two required methods, and Python's for loop accepts it.
3
2
1The for loop calls that method to get an iterator, then repeatedly calls its next method until StopIteration signals the end. The article on Python iterables and iterators covers this protocol in depth.
The container protocol
Any object that implements __len__ and __getitem__ works with the len function, indexing syntax, slicing, and membership testing. You do not need to inherit from list or any collection class.
class Weekdays:
_days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
def __len__(self):
return len(self._days)
def __getitem__(self, index):
return self._days[index]
days = Weekdays()
print(len(days))
print(days[0])
print(days[1:4])
print("Fri" in days)Python finds the length and indexing methods shown above whenever you call len(), index, or slice the object. The in operator works because Python falls back to iterating and comparing when a dedicated membership method is missing.
7
Mon
['Tue', 'Wed', 'Thu']
TrueAdding a dedicated membership method gives you more efficient and flexible control than the iterate-and-compare fallback, especially for large or computed collections.
The numeric protocol
Arithmetic operators each map to a magic method: the + operator calls __add__, and the * operator calls a similar multiplication method. Implementing these methods lets your objects work with operator syntax.
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})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)The + operator calls v1.add(v2), which creates and returns a new Vector rather than modifying either of the original two vectors.
Vector(4, 6)If the left operand does not know how to handle the right operand's type, Python tries the reflected operation on the right operand as a fallback. This two-way negotiation lets operators work with types you may not know about at design time.
The comparison protocol
Comparison operators each map to their own magic method, and implementing all of them lets your objects sort, rank, and compare like built-in types:
| Operator | Method |
|---|---|
| == | __eq__ |
| < | __lt__ |
| <= | __le__ |
| > | __gt__ |
| >= | __ge__ |
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.name == other.name and self.age == other.age
def __lt__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.age < other.age
alice = Person("Alice", 30)
bob = Person("Bob", 25)
print(alice == bob)
print(alice < bob)Alice and Bob are different people, so equality returns False. Alice is not younger than Bob, so less-than also returns False.
False
FalseReturning NotImplemented from a comparison method tells Python to try the reflected operation on the other operand. This is important for interoperability between different types.
The callable protocol
Any object that implements __call__ can be called like a function. This lets you create objects that maintain state between calls while being invoked with familiar function syntax.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(10))
print(triple(10))
print(callable(double))A Multiplier instance remembers its factor and applies it when called. The built-in callable function reports that instances of this class can be invoked, which is useful when your code needs to check that behaviour before calling.
20
30
TrueThe context manager protocol
Objects that implement __enter__ and __exit__ work with the with statement. This is how file handles, locks, and database connections provide automatic cleanup after a block completes.
class ManagedResource:
def __enter__(self):
print("Acquiring resource")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Releasing resource")
return False
with ManagedResource() as r:
print("Using resource")The enter method runs when the block starts, and the exit method runs when the block ends, whether normally or with an exception.
Acquiring resource
Using resource
Releasing resourceThe return value of the enter method is bound to the variable after as. Returning False from the exit method lets exceptions propagate normally, while returning True suppresses them from reaching the caller. The article on Python context managers covers this in more detail.
How Python decides which method to call
When Python encounters len(obj), it does not directly call obj.len(). The built-in function first checks that the object's type defines that method through its type slot mechanism, and if it does not, Python raises a TypeError.
For binary operators like the + operator, Python looks up the add method on the type of the left operand rather than the instance directly, because looking it up on the instance would bypass the class and its metaclass. This design keeps the data model consistent and fast.
If a.add(b) returns NotImplemented, Python tries b.radd(a) as a fallback. If neither side handles the operation, Python raises TypeError, and this two-way negotiation lets operators work across types.
Why the data model matters for everyday code
You interact with the data model constantly, even if you never write a dunder method yourself. When you use the len function, the str function, or the in operator, Python calls magic methods behind the scenes.
The for loop, the with statement, the + operator, and the == operator all work the same way. Understanding this mapping helps you debug unexpected behaviour.
When you write a class, the data model gives you a clear checklist: a collection-like class should implement length and indexing, a comparable class should implement equality, and a resource wrapper should implement the enter and exit methods.
The earlier article on everything being an object in Python showed that functions, classes, and modules are all objects. The data model is what gives each of those objects its behaviour, since a function works with call syntax and a module exposes attributes only because their types define the matching special methods.
Rune AI
Key Insights
- The Python data model maps language syntax and built-in functions to special method calls on objects.
- Magic methods (dunder methods) have double-underscore names and are called automatically by the interpreter.
- Implementing
__len__and__getitem__is enough to make an object work with len(), indexing, and for loops. - The data model uses protocols: any object that implements the right methods qualifies for the corresponding operation.
- You should never call magic methods directly; rely on the built-in function or syntax that triggers them.
Frequently Asked Questions
What is the Python data model?
Why are they called magic methods?
Conclusion
The Python data model is what makes Python feel consistent. Every built-in operation, from len() to + to for loops, is a method call in disguise. When you implement the right magic methods on your own classes, your objects gain the same syntactic support that built-in types have. The next article covers the most important special methods one by one with practical examples.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.