The Python Data Model
Source: Fluent Python, 2nd Edition — Chapter 1 by Luciano Ramalho
What Is the Python Data Model?
The Python Data Model is the API that makes your objects work seamlessly with Python's built-in features — things like len(), for loops, in operator, and arithmetic operators.
The key idea: you implement special methods (also called dunder methods, short for "double underscore") on your own classes, and Python calls them automatically when you use built-in syntax.
Special methods look like __len__ or __getitem__ — two underscores on each side. Python calls these behind the scenes; you almost never call them directly.
A Pythonic Card Deck
The chapter opens with a FrenchDeck class that shows the power of implementing just two dunder methods: __len__ and __getitem__.
The Card Named Tuple
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
namedtuple creates a simple class whose instances are immutable. A card can be made like this:
beer_card = Card('7', 'diamonds')
print(beer_card) # Card(rank='7', suit='diamonds')
The FrenchDeck Class
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]
🧪 Try the code out!
What You Get for Free
By implementing __len__ and __getitem__, the deck automatically supports:
| Feature | How it works |
|---|---|
len(deck) | Calls deck.__len__() |
deck[0], deck[-1] | Calls deck.__getitem__(0) |
deck[12::13] | Slicing — Python passes a slice object to __getitem__ |
for card in deck | Iteration via __getitem__ |
Card('Q', 'hearts') in deck | in operator (uses iteration) |
random.choice(deck) | Works because it just calls deck[random_index] |
🧪 Iteration and slicing examples
from random import choice
deck = FrenchDeck()
# Picking a random card
print(choice(deck))
# Slicing: get all aces (every 13th card starting from index 12)
print(deck[12::13])
# Iterating in reverse (requires __getitem__ only, no __reversed__ needed)
for card in reversed(deck):
print(card)
break # just show the first one
Sorting the Deck
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
def spades_high(card):
rank_value = FrenchDeck.ranks.index(card.rank)
return rank_value * len(suit_values) + suit_values[card.suit]
for card in sorted(deck, key=spades_high):
print(card)
We never subclassed any "deck" class from a standard library. By implementing the data model protocol (__len__ + __getitem__), our FrenchDeck behaves like a proper Python sequence and gets iteration, slicing, random.choice, and sorted for free.
Mini quiz — the data model
The data model
1 / 2You should usually call which of these?
len(deck)deck.__len__()
Pick an answer to see if you are right.
Challenge: bag length
Write __len__ for Bag so len(bag) is how many words were stored.
Challenge: pick an item
Write __getitem__ so bag[i] returns the word at index i.
How Special Methods Are Used
Python calls dunder methods for you — your code (and users of your library) should call the built-in functions, not the dunder methods directly.
# Do this:
my_len = len(deck)
# Not this (though it works):
my_len = deck.__len__()
The only common exception is __init__ — you call super().__init__() in subclasses.
Emulating Numeric Types — The Vector Class
The second example creates a 2D Vector class to demonstrate several dunder methods at once.
Full Implementation
import math
class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return f'Vector({self.x!r}, {self.y!r})'
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
🧪 Try the Vector class
String Representation — __repr__ vs __str__
| Method | Called by | When used |
|---|---|---|
__repr__ | repr(), interactive shell, {obj!r} in f-strings | Developer-facing; should be unambiguous |
__str__ | str(), print(), {obj} in f-strings | User-facing; can be readable/informal |
class Vector:
def __repr__(self):
return f'Vector({self.x!r}, {self.y!r})'
# If __str__ is not defined, Python falls back to __repr__
If you implement only one, implement __repr__. Python will use it as a fallback for __str__ too.
Mini quiz — repr and str
repr and str
1 / 2print(obj) uses which method first if both exist?
print(obj)str(obj)repr(obj)
Pick an answer to see if you are right.
Arithmetic Operators
__add__ and __mul__ let your objects respond to + and *:
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
v = Vector(3, 4)
print(v + Vector(1, 0)) # Vector(4, 4)
print(v * 2) # Vector(6, 8)
Note that v * 2 works (Vector × scalar), but 2 * v would not without implementing __rmul__. Ramalho covers this in later chapters.
Challenge: add two vectors
Write __add__ so Vector(a, b) + Vector(c, d) has coordinates (a+c, b+d). Print x y.
Boolean Value — __bool__
Python calls __bool__ when an object is used in a boolean context (if obj:, while obj:, bool(obj)).
def __bool__(self):
return bool(abs(self)) # zero vector is falsy, any other is truthy
print(bool(Vector(0, 0))) # False — zero vector
print(bool(Vector(1, 0))) # True — non-zero vector
If __bool__ is not defined, Python calls __len__ instead. If that's also absent, the object is always truthy.
Challenge: zero is false
Write __bool__ so a Flag is False when self.on is 0, otherwise True. Print yes or no.
Overview of Special Methods
The book includes two tables listing all standard dunder methods. Here are the most important ones by category:
String/Bytes Representation
| Method | Purpose |
|---|---|
__repr__ | Developer string (used by repr()) |
__str__ | User string (used by str(), print()) |
__format__ | Used by format() and f-strings |
__bytes__ | Used by bytes() |
Collection-like Behavior
| Method | Purpose |
|---|---|
__len__ | len(obj) |
__getitem__ | obj[key], slicing, iteration |
__setitem__ | obj[key] = val |
__delitem__ | del obj[key] |
__contains__ | item in obj |
__iter__ | for item in obj |
__reversed__ | reversed(obj) |
Numeric Types
| Method | Purpose |
|---|---|
__add__ | obj + other |
__sub__ | obj - other |
__mul__ | obj * other |
__truediv__ | obj / other |
__floordiv__ | obj // other |
__mod__ | obj % other |
__abs__ | abs(obj) |
__bool__ | bool(obj) |
__neg__ | -obj |
Comparison
| Method | Purpose |
|---|---|
__eq__ | obj == other |
__ne__ | obj != other |
__lt__ | obj < other |
__le__ | obj <= other |
__gt__ | obj > other |
__ge__ | obj >= other |
Chapter 1 Summary
- The Python Data Model defines the interfaces for objects to work with Python's built-in syntax and functions.
- Dunder methods (
__len__,__getitem__,__add__, etc.) are called by Python automatically — use them to make your objects feel native. - Implement
__repr__at minimum for any custom class;__str__is optional and falls back to__repr__. - You get a lot for free: implementing
__len__+__getitem__gives your class iteration, slicing,in,reversed(),sorted(), andrandom.choice()without extra work. - Prefer built-ins: call
len(x)notx.__len__(). The interpreter often takes a faster path through built-ins.
Exercises
Write __sub__ so Vector(a, b) - Vector(c, d) is (a-c, b-d). Print x y.