Python Classes and Data Model
- Description:
classsyntax, instance/class/static methods, inheritance and MRO, dunder methods (__init__,__repr__,__eq__,__hash__, etc.),@property,@dataclass,__slots__, and abstract base classes - My Notion Note ID: K2A-D1-10
- Created: 2022-08-22
- Updated: 2026-05-11
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Class Definition
- 2.
self, Instance vs Class Attributes - 3.
@staticmethod,@classmethod - 4. Inheritance,
super(), and MRO - 5. The Data Model (Dunder Methods)
- 6.
@property - 7.
@dataclass - 8.
__slots__ - 9. Abstract Base Classes
1. Class Definition
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def distance_to(self, other: "Point") -> float:
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
p = Point(3, 4)
p.distance_to(Point(0, 0)) # 5.0
- No
public/privatekeywords, convention only (_leading_underscore) - No attribute declarations, attributes spring into existence on first assignment to
self
2. self, Instance vs Class Attributes
selfis the explicit first parameter of every instance method (C++this, made visible)inst.method(x)is sugar forClass.method(inst, x)
class Counter:
count = 0 # CLASS attribute, shared across all instances
def __init__(self) -> None:
self.value = 0 # INSTANCE attribute
def bump(self) -> None:
self.value += 1
Counter.count += 1 # mutate the class attribute
- Trap:
self.count = 1shadows the class attribute with an instance attribute; class attribute unchanged
3. @staticmethod, @classmethod
class Date:
def __init__(self, y, m, d):
self.y, self.m, self.d = y, m, d
@classmethod
def from_iso(cls, s: str) -> "Date":
y, m, d = map(int, s.split("-"))
return cls(y, m, d) # cls supports subclassing
@staticmethod
def is_leap(y: int) -> bool:
return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)
Date.from_iso("2026-05-11")
Date.is_leap(2024)
@classmethod, class as first arg; used for alternative constructors, class-level state@staticmethod, no implicit first arg; namespaced free function
4. Inheritance, super(), and MRO
class Animal:
def __init__(self, name): self.name = name
def speak(self): return "..."
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # forward to parent
self.breed = breed
def speak(self): return "Woof"
super()walks the Method Resolution Order (MRO), computed by C3 linearization- Single inheritance: it's just the parent. Multiple inheritance: the merged linear chain
class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...
D.__mro__ # (D, B, C, A, object)
- Resolves the diamond problem by default, C++ requires
virtualinheritance for the same effect
5. The Data Model (Dunder Methods)
- The "data model" is the contract between user classes and the language
- Dunder methods (
__name__) hook into operators, built-ins, and protocols
5.1 __init__, __new__, __del__
__init__(self, ...), initializer (NOT constructor). The object already exists; this fills it in.__new__(cls, ...), allocator. Rarely overridden; needed for immutable types and metaclass tricks.__del__(self), finalizer. Timing is not guaranteed (especially with cycles); don't rely on it for cleanup, usewith.
5.2 __repr__ vs __str__
__repr__, unambiguous; ideallyeval()-able. Used byrepr(x)and the REPL.__str__, readable, user-facing. Used bystr(x)andprint(). Defaults to__repr__if missing.
class Point:
def __init__(self, x, y): self.x, self.y = x, y
def __repr__(self): return f"Point({self.x!r}, {self.y!r})"
def __str__(self): return f"({self.x}, {self.y})"
- Rule: always define
__repr__; define__str__only when the user-facing form differs
5.3 __eq__ and __hash__
class Currency:
def __init__(self, code, amount):
self.code, self.amount = code, amount
def __eq__(self, other):
if not isinstance(other, Currency): return NotImplemented
return self.code == other.code and self.amount == other.amount
def __hash__(self):
return hash((self.code, self.amount))
Rules:
- Define
__eq__→ Python sets__hash__ = None(unhashable); re-define__hash__to make instances hashable a == bmust implyhash(a) == hash(b)- Hash must be immutable for the object's lifetime, never hash mutable state
5.4 Ordering and total_ordering
- Define
__lt__,__le__,__gt__,__ge__to enable ordering @functools.total_orderingfills the rest from__eq__+__lt__
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor): self.major, self.minor = major, minor
def __eq__(self, o): return (self.major, self.minor) == (o.major, o.minor)
def __lt__(self, o): return (self.major, self.minor) < (o.major, o.minor)
5.5 Container Protocols
| Dunder | Enables |
|---|---|
__len__ |
len(x) |
__getitem__ |
x[i], slicing, iteration fallback (with __len__) |
__setitem__ |
x[i] = v |
__delitem__ |
del x[i] |
__contains__ |
v in x |
__iter__ / __next__ |
for v in x |
__call__ |
x(...), instances become callable |
- A class with
__getitem__/__setitem__/__delitem__/__len__/__iter__quacks like a sequence - Add
keys→ quacks like a mapping - No formal interface, duck typing is the rule
6. @property
- Turns a method into an attribute-like accessor, no
()
class Circle:
def __init__(self, r): self._r = r
@property
def radius(self) -> float:
return self._r
@radius.setter
def radius(self, r: float) -> None:
if r < 0: raise ValueError("negative radius")
self._r = r
@property
def area(self) -> float:
return 3.14159 * self._r ** 2
c = Circle(5)
c.radius # 5 , no parentheses
c.radius = 10 # invokes setter
c.area # 314.159
- Unlike C++ getter/setter methods, you can rewrite a public attribute as a property later without breaking callers
- Start public; add
@propertyonly when validation or computation is needed
7. @dataclass
- PEP 557 (3.7+), generates
__init__,__repr__,__eq__from type-annotated class attributes
from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True) # slots since 3.10
class Point:
x: float
y: float
label: str = ""
@dataclass
class Inventory:
items: list[str] = field(default_factory=list) # avoid the mutable-default trap
Key options:
-
frozen=True, immutable, hashable -
slots=True, use__slots__, no__dict__ -
kw_only=True, force keyword args -
order=True, generate ordering -
For richer needs (validation, serialization):
pydanticorattrs
8. __slots__
- By default each instance has a
__dict__ __slots__replaces it with a fixed array, saves memory and prevents typo-attributes
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y): self.x, self.y = x, y
p = Point(1, 2)
p.z = 3 # AttributeError, z is not in __slots__
- Use it on classes instantiated by the millions, or to lock attribute names
- Subclassing a slotted class without re-declaring
__slots__brings__dict__back
9. Abstract Base Classes
abc.ABC+@abstractmethodenforce that subclasses implement specific methods
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def read(self, key: str) -> bytes: ...
@abstractmethod
def write(self, key: str, data: bytes) -> None: ...
class MemoryStorage(Storage):
def __init__(self): self._d = {}
def read(self, key): return self._d[key]
def write(self, key, data): self._d[key] = data
Storage() # TypeError: can't instantiate abstract class
- For typing without enforced inheritance,
Protocol(structural typing) is usually a better fit