Skip to main content

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!

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.


Mini quiz — the data model​

Quiz

The data model

1 / 2

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

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.


Mini quiz — repr and str​

Quiz

repr and str

1 / 2

print(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

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​

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.

Exercises​

Activity: Subtract vectors

Write __sub__ so Vector(a, b) - Vector(c, d) is (a-c, b-d). Print x y.