The Data Model¶
Python's data model is the set of rules that define how objects behave. When you write a + b, Python calls a.__add__(b). When you use len(x), Python calls x.__len__(). When you do for item in x, Python calls iter(x) which calls x.__iter__().
Understanding the data model means you can make your own classes behave like built-in types. This is one of Python's most powerful design decisions.
Objects, Values, and Types¶
Every piece of data in Python is an object. Every object has:
- Identity: A unique identifier —
id(obj)returns it. In CPython, it's the memory address. Never changes during an object's lifetime. - Type: What kind of object it is —
type(obj)returns it. Determines what operations are supported. Never changes for an object. - Value: The actual data. May change (mutable) or not (immutable).
x = [1, 2, 3]
print(id(x)) # e.g. 140543763210560
print(type(x)) # <class 'list'>
x.append(4) # value changed (mutable)
print(id(x)) # SAME id — still the same object
The is vs == Distinction¶
iscompares identity — are these the same object?==compares value — do these objects have equal content?
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True — same value
print(a is b) # False — different objects
print(a is c) # True — same object (c is just another name for a)
Don't use is for value comparison
is is only for identity. Use == for value equality. The one exception: comparing to singletons like None, True, False — conventionally written as x is None, not x == None.
Special Methods (Dunder Methods)¶
Python uses double-underscore methods (dunders) to define how objects respond to operators and built-in functions. This is often called "operator overloading".
Numeric Operations¶
| Method | Triggered by | Notes |
|---|---|---|
__add__(self, other) | self + other | |
__sub__(self, other) | self - other | |
__mul__(self, other) | self * other | |
__truediv__(self, other) | self / other | |
__floordiv__(self, other) | self // other | |
__mod__(self, other) | self % other | |
__pow__(self, other) | self ** other | |
__neg__(self) | -self | Unary |
__abs__(self) | abs(self) |
Reflected versions: __radd__, __rsub__, etc. — called when the left operand doesn't support the operation.
Comparison¶
| Method | Triggered by |
|---|---|
__eq__(self, other) | self == other |
__ne__(self, other) | self != other |
__lt__(self, other) | self < other |
__le__(self, other) | self <= other |
__gt__(self, other) | self > other |
__ge__(self, other) | self >= other |
Container Protocol¶
| Method | Triggered by |
|---|---|
__len__(self) | len(self) |
__getitem__(self, key) | self[key] |
__setitem__(self, key, value) | self[key] = value |
__delitem__(self, key) | del self[key] |
__contains__(self, item) | item in self |
__iter__(self) | iter(self), for x in self |
__next__(self) | next(self) |
Object Lifecycle¶
| Method | Called when |
|---|---|
__new__(cls, ...) | Creating a new instance |
__init__(self, ...) | Initializing a new instance |
__del__(self) | Object is about to be garbage collected |
String Representation¶
| Method | Called by |
|---|---|
__repr__(self) | repr(obj), REPL display |
__str__(self) | str(obj), print(obj) |
__format__(self, spec) | format(obj, spec), f-strings |
__bytes__(self) | bytes(obj) |
Attribute Access¶
| Method | Called when |
|---|---|
__getattribute__(self, name) | Any attribute access obj.attr |
__getattr__(self, name) | Attribute not found via normal means |
__setattr__(self, name, value) | obj.attr = value |
__delattr__(self, name) | del obj.attr |
__dir__(self) | dir(obj) |
Context Manager Protocol¶
| Method | Called by |
|---|---|
__enter__(self) | Entering with block |
__exit__(self, exc_type, exc_val, exc_tb) | Leaving with block |
Example: A Complete Custom Class¶
Here's a Vector class that uses the data model to behave like a proper numeric type:
import math
class Vector:
"""2D vector with full operator support."""
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Vector({self.x!r}, {self.y!r})"
def __str__(self) -> str:
return f"({self.x}, {self.y})"
def __add__(self, other: "Vector") -> "Vector":
return Vector(self.x + other.x, self.y + other.y)
def __sub__(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 __rmul__(self, scalar: float) -> "Vector":
return self.__mul__(scalar)
def __abs__(self) -> float:
return math.sqrt(self.x**2 + self.y**2)
def __bool__(self) -> bool:
return self.x != 0 or self.y != 0
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return self.x == other.x and self.y == other.y
def __iter__(self):
yield self.x
yield self.y
def __len__(self) -> int:
return 2
def __getitem__(self, index: int) -> float:
return (self.x, self.y)[index]
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # (4, 6)
print(3 * v1) # (3, 6)
print(abs(v2)) # 5.0
print(bool(v1)) # True
print(list(v1)) # [1, 2]
print(v1[0]) # 1
Type Hierarchy¶
Python's built-in types form a hierarchy rooted at object:
object
├── NoneType (None)
├── bool (True, False)
├── int
│ └── bool (bool is a subclass of int!)
├── float
├── complex
├── str
├── bytes
├── bytearray
├── memoryview
├── list
├── tuple
├── range
├── set
├── frozenset
├── dict
├── type (metaclass — classes are instances of type)
├── function
├── method
├── module
└── ...