Skip to main content

Chapter 10a - 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.

note

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!
import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

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]

deck = FrenchDeck()
print(len(deck)) # 52
print(deck[0]) # Card(rank='2', suit='spades')
print(deck[-1]) # Card(rank='A', suit='hearts')

What You Get for Free​

By implementing __len__ and __getitem__, the deck automatically supports:

FeatureHow 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 deckIteration via __getitem__
Card('Q', 'hearts') in deckin 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)
Key Insight

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.


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
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)

v1 = Vector(2, 4)
v2 = Vector(2, 1)

print(v1 + v2) # Vector(4, 5)
print(abs(v1)) # 4.47...
print(v1 * 3) # Vector(6, 12)
print(bool(Vector(0, 0))) # False
print(bool(v1)) # True

String Representation β€” __repr__ vs __str__​

MethodCalled byWhen used
__repr__repr(), interactive shell, {obj!r} in f-stringsDeveloper-facing; should be unambiguous
__str__str(), print(), {obj} in f-stringsUser-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__
note

If you implement only one, implement __repr__. Python will use it as a fallback for __str__ too.


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

Note that v * 2 works (Vector Γ— scalar), but 2 * v would not without implementing __rmul__. Ramalho covers this in later chapters.


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.


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​

MethodPurpose
__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​

MethodPurpose
__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​

MethodPurpose
__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​

MethodPurpose
__eq__obj == other
__ne__obj != other
__lt__obj < other
__le__obj <= other
__gt__obj > other
__ge__obj >= other

Chapter 1 Summary​

Key Takeaways
  1. The Python Data Model defines the interfaces for objects to work with Python's built-in syntax and functions.
  2. Dunder methods (__len__, __getitem__, __add__, etc.) are called by Python automatically β€” use them to make your objects feel native.
  3. Implement __repr__ at minimum for any custom class; __str__ is optional and falls back to __repr__.
  4. You get a lot for free: implementing __len__ + __getitem__ gives your class iteration, slicing, in, reversed(), sorted(), and random.choice() without extra work.
  5. Prefer built-ins: call len(x) not x.__len__(). The interpreter often takes a faster path through built-ins.
πŸ”¨Exercise: Extend the Vector class
  • Add __sub__ so v1 - v2 works.
  • Add __eq__ so two vectors with the same coordinates are equal.
  • Add __rmul__ so 3 * v also works (not just v * 3).
  • Add __neg__ so -v returns a vector pointing the opposite direction.
# Starter code
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):
return Vector(self.x + other.x, self.y + other.y)

def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)

# TODO: implement __sub__, __eq__, __rmul__, __neg__
πŸ“’ Solution
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):
return Vector(self.x + other.x, self.y + other.y)

def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)

def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)

def __rmul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)

def __neg__(self):
return Vector(-self.x, -self.y)

def __eq__(self, other):
return self.x == other.x and self.y == other.y

v = Vector(3, 4)
print(v - Vector(1, 1)) # Vector(2, 3)
print(v == Vector(3, 4)) # True
print(3 * v) # Vector(9, 12)
print(-v) # Vector(-3, -4)