What’s one thing you learned? What’s still confusing?
Python's Data Model: Dunder Methods & Operator Overloading
Implement __repr__, __eq__, __hash__, __len__, __getitem__, __iter__, __enter__/__exit__, __call__.
Decorators, Generators & Context Managers
Decorators for timing/logging/caching, generators with yield, and context managers.
Python's Memory Model: Objects, References & Identity
Everything is an object: references, id(), integer caching, mutable vs immutable.
Interactive Labs for This Track
Loop Visualizer
You're a factory robot repeating the same task on an assembly line — watch how loops automate repetitive work
List Slicing
You have a playlist of 50 songs — grab just tracks 10 through 20 with a single slice expression
Sorting Algorithms
You're organizing a library of 10,000 books — which sorting method is fastest?
Ask questions, share insights
super() cooperatively, and build ABC-based interfaces. Part 2 is the data side of advanced OOP — @dataclass is the killer feature most Python developers underuse (every ML config and API response in a real codebase is a dataclass), Enum replaces magic strings everywhere (Stripe's entire API uses them), Protocol gives you duck typing with type-checker support, and __slots__ is the memory optimization that turns 400 MB into 200 MB at scale.@dataclass: Python's Best Feature (3.7+)@dataclass inspects your field type annotations and automatically generates __init__, __repr__, and __eq__. It eliminates the most repetitive boilerplate in Python.A @dataclass with frozen=True generates which extra dunder method that a plain @dataclass does not?
__hash__. Regular dataclasses define __eq__, which causes Python to set __hash__ = None. With frozen=True, the object is immutable, so equal objects will always have equal hashes -- Python generates __hash__ automatically.from dataclasses import dataclass, field
import math
# BEFORE dataclass: 30+ lines of boilerplate
class ModelConfigManual:
def __init__(self, name, hidden_size=768, num_layers=12,
dropout=0.1, learning_rate=1e-4):
self.name = name
self.hidden_size = hidden_size
self.num_layers = num_layers
self.dropout = dropout
self.learning_rate = learning_rate
def __repr__(self):
return (f"ModelConfig(name={self.name!r}, hidden_size={self.hidden_size}, "
f"num_layers={self.num_layers}, dropout={self.dropout}, "
f"learning_rate={self.learning_rate})")
def __eq__(self, other):
if not isinstance(other, ModelConfigManual):
return False
return self.__dict__ == other.__dict__
# AFTER dataclass: 10 lines, equivalent behavior + more features
@dataclass
class ModelConfig:
name: str
hidden_size: int = 768
num_layers: int = 12
dropout: float = 0.1
learning_rate: float = 1e-4
# field() for mutable defaults — REQUIRED for list/dict/set
layer_sizes: list[int] = field(default_factory=list)
def __post_init__(self) -> None:
"""Called automatically after __init__ -- for validation and computed fields."""
if self.dropout < 0 or self.dropout >= 1:
raise ValueError(f"dropout must be in [0, 1), got {self.dropout}")
if not self.layer_sizes:
self.layer_sizes = [self.hidden_size] * self.num_layers
@property
def num_parameters(self) -> int:
"""Rough parameter count estimate."""
return self.hidden_size * self.num_layers * 4
# __init__, __repr__, __eq__ all generated automatically
cfg = ModelConfig("BERT-base")
print(cfg)
# ModelConfig(name='BERT-base', hidden_size=768, num_layers=12, dropout=0.1, ...)
cfg2 = ModelConfig("BERT-base")
print(cfg == cfg2) # True -- __eq__ compares all fields
print(f"Parameters: {cfg.num_parameters:,}") # 37,748,736frozen=True: Immutable + Hashable@dataclass(frozen=True)
class DatasetSplit:
"""An immutable description of how data is split."""
train_ratio: float
val_ratio: float
test_ratio: float
random_seed: int = 42
def __post_init__(self) -> None:
total = self.train_ratio + self.val_ratio + self.test_ratio
if not math.isclose(total, 1.0, rel_tol=1e-6):
raise ValueError(f"Ratios must sum to 1.0, got {total}")
standard_split = DatasetSplit(0.8, 0.1, 0.1)
# Hashable because frozen=True — usable as dict keys / set elements
cache: dict[DatasetSplit, list] = {standard_split: []}
print(hash(standard_split)) # works
try:
standard_split.train_ratio = 0.7 # FrozenInstanceError!
except Exception as e:
print(f"Cannot mutate frozen dataclass: {e}")order=True: Sortable Records@dataclass(order=True)
class TrainingRun:
"""A recorded training run, sortable by accuracy then loss."""
# Fields used for ordering are evaluated left to right (like a tuple)
accuracy: float
loss: float
run_id: str = field(compare=False) # exclude from ordering
runs = [
TrainingRun(0.87, 0.45, "run-003"),
TrainingRun(0.92, 0.31, "run-001"),
TrainingRun(0.89, 0.38, "run-002"),
]
best_runs = sorted(runs, reverse=True)
for run in best_runs:
print(f" {run.run_id}: accuracy={run.accuracy:.2%}")
# run-001: 92.00%
# run-002: 89.00%
# run-003: 87.00%slots=True: Memory + Speed Win (3.10+)@dataclass(slots=True)
class Point:
"""A 2D point with the memory layout of a tuple."""
x: float
y: float
# Per-instance memory drops by ~40-50% vs the default dict-backed class.
# Attribute access is also slightly faster (array index vs dict lookup).
# Restriction: cannot add new attributes at runtime.| Approach | __init__ | __repr__ | __eq__ | Immutable | Hashable | Memory |
|---|---|---|---|---|---|---|
| Manual class | Manual | Manual | Manual | Via property | Manual | Normal |
namedtuple | Auto | Auto | Auto | Yes | Yes | Smaller |
@dataclass |
from enum import Enum, IntEnum, Flag, auto
class ModelType(Enum):
"""Type of ML model architecture."""
LINEAR = "linear"
TREE = "tree"
NEURAL_NETWORK = "neural_network"
TRANSFORMER = "transformer"
DIFFUSION = "diffusion"
# Access patterns
print(ModelType.TRANSFORMER) # ModelType.TRANSFORMER
print(ModelType.TRANSFORMER.name) # TRANSFORMER
print(ModelType.TRANSFORMER.value) # transformer
# Get enum member from value (raises ValueError if not found)
m = ModelType("transformer")
print(m is ModelType.TRANSFORMER) # True -- enums are singletons
# Iteration
for model_type in ModelType:
print(f" {model_type.name}: {model_type.value}")
# Use in type hints -- forces callers to use the enum
def create_model(model_type: ModelType, n_layers: int):
if model_type == ModelType.TRANSFORMER:
return f"Building Transformer with {n_layers} layers"
return f"Building {model_type.value} model"auto(): Let Python Assign Valuesclass Status(Enum):
PENDING = auto() # 1
TRAINING = auto() # 2
EVALUATING = auto() # 3
DONE = auto() # 4
FAILED = auto() # 5IntEnum: Enum That IS an Integerint:class ExitCode(IntEnum):
SUCCESS = 0
ERROR = 1
TIMEOUT = 124
OOM = 137
exit_code = ExitCode.SUCCESS
print(exit_code == 0) # True -- IntEnum compares equal to its int value
print(exit_code < ExitCode.ERROR) # True -- supports < > comparisonsFlag: Combinable Bit Flagsclass DataAugmentation(Flag):
NONE = 0
FLIP = auto() # 1
ROTATE = auto() # 2
CROP = auto() # 4
COLOR_JITTER = auto() # 8
ALL = FLIP | ROTATE | CROP | COLOR_JITTER # 15
# Combine flags with |
augmentations = DataAugmentation.FLIP | DataAugmentation.ROTATE
print(augmentations) # DataAugmentation.FLIP|ROTATE
print(DataAugmentation.FLIP in augmentations) # True
print(DataAugmentation.CROP in augmentations) # FalseProblem with "transformer" | How ModelType.TRANSFORMER fixes it |
|---|---|
Typo "transfomer" silently passes | ModelType.TRANSFOMER → AttributeError immediately |
| No IDE autocomplete | Full autocomplete in any IDE |
| Cannot iterate all valid values | list(ModelType) gives all members |
| Equality check is string comparison | Enum comparison uses is (identity) |
| No documentation | Enum class has docstring; members can too |
Protocol defines an interface through structure rather than inheritance. Any class that has the required methods satisfies the protocol -- even if it never heard of it. This is formalized duck typing.from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
"""Any object with a draw() method satisfies this protocol."""
def draw(self, canvas: str) -> str: ...
def bounding_box(self) -> tuple[float, float, float, float]: ...
# Neither of these classes inherits from Drawable
class Circle:
def __init__(self, cx: float, cy: float, r: float) -> None:
self.cx, self.cy, self.r = cx, cy, r
def draw(self, canvas: str) -> str:
return f"[{canvas}] Drawing circle at ({self.cx}, {self.cy}) r={self.r}"
def bounding_box(self) -> tuple[float, float, float, float]:
return (self.cx - self.r, self.cy - self.r,
self.cx + self.r, self.cy + self.r)
class SVGRect:
def __init__(self, x: float, y: float, w: float, h: float) -> None:
self.x, self.y, self.w, self.h = x, y, w, h
def draw(self, canvas: str) -> str:
return f"<rect x={self.x} y={self.y} width={self.w} height={self.h} />"
def bounding_box(self) -> tuple[float, float, float, float]:
return (self.x, self.y, self.x + self.w, self.y + self.h)
# render() accepts any Drawable -- no inheritance required
def render(obj: Drawable, canvas: str = "screen") -> None:
print(obj.draw(canvas))
print(f" Bounding box: {obj.bounding_box()}")
render(Circle(50, 50, 30)) # Works -- Circle satisfies Drawable structurally
render(SVGRect(10, 10, 80, 60)) # Works -- SVGRect satisfies Drawable structurally
# runtime_checkable allows isinstance checks
print(isinstance(Circle(0, 0, 1), Drawable)) # True
print(isinstance("not drawable", Drawable)) # False| ABC | Protocol | |
|---|---|---|
| Typing style | Nominal (explicit inheritance required) | Structural (method presence is enough) |
| Use when | You own all implementations | You want to type-check third-party objects |
| Enforcement | @abstractmethod at instantiation | Static type checker only (unless @runtime_checkable) |
| Example | PyTorch nn.Module | Python's typing.Sized, typing.Iterable |
__slots__: Memory Optimization__dict__ (a full hash map). For objects you create in the millions -- training samples, graph nodes, token representations -- this is expensive.import sys
class PointDict:
"""Normal class: each instance has a __dict__."""
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
class PointSlots:
"""Slots class: no __dict__, fixed attribute layout."""
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
# Memory comparison
pd = PointDict(1.0, 2.0)
ps = PointSlots(1.0, 2.0)
dict_size = sys.getsizeof(pd) + sys.getsizeof(pd.__dict__)
slot_size = sys.getsizeof(ps)
print(f"PointDict total size: {dict_size} bytes")
print(f"PointSlots total size: {slot_size} bytes")
print(f"Memory saving: {(1 - slot_size/dict_size)*100:.0f}%")
# At scale: 10 million points
n = 10_000_000
dict_total_mb = (n * dict_size) / 1_000_000
slot_total_mb = (n * slot_size) / 1_000_000
print(f"\n10M PointDict objects: {dict_total_mb:.0f} MB")
print(f"10M PointSlots objects: {slot_total_mb:.0f} MB")# Cannot add new attributes at runtime
ps2 = PointSlots(3.0, 4.0)
try:
ps2.z = 5.0 # AttributeError: 'PointSlots' object has no attribute 'z'
except AttributeError as e:
print(f"Caught: {e}")
# Inheritance with __slots__: child must also define __slots__
class Point3DSlots(PointSlots):
__slots__ = ("z",) # only NEW slots needed -- inherits x and y
def __init__(self, x: float, y: float, z: float) -> None:
super().__init__(x, y)
self.z = z
p3d = Point3DSlots(1, 2, 3)
print(p3d.x, p3d.y, p3d.z) # 1 2 3__slots____getstate__, or mixing with __weakref__.This pulls dataclass, Enum, and Protocol together into one realistic system:
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Protocol, runtime_checkable
# --- Enums for type-safe configuration ---
class TaskType(Enum):
CLASSIFICATION = auto()
REGRESSION = auto()
SEGMENTATION = auto()
class Optimizer(Enum):
SGD = "sgd"
ADAM = "adam"
ADAMW = "adamw"
# --- Dataclasses for configuration ---
@dataclass(frozen=True)
class TrainingConfig:
learning_rate: float = 1e-3
batch_size: int = 32
max_epochs: int = 100
optimizer: Optimizer = Optimizer.ADAM
weight_decay: float = 1e-4
def __post_init__(self) -> None:
if self.learning_rate <= 0:
raise ValueError(f"learning_rate must be > 0, got {self.learning_rate}")
if self.batch_size < 1:
raise ValueError(f"batch_size must be >= 1, got {self.batch_size}")
@dataclass
class TrainingResult:
model_name: str
task: TaskType
config: TrainingConfig
accuracy: float = 0.0
history: list[dict] = field(default_factory=list)
# --- Protocol for exportable models ---
@runtime_checkable
class Exportable(Protocol):
def export(self, path: str) -> None: ...
def load(self, path: str) -> None: ...
class LinearModel:
"""A class that satisfies Exportable structurally — no inheritance from Exportable."""
def export(self, path: str) -> None:
print(f"Exporting LinearModel weights to {path}")
def load(self, path: str) -> None:
print(f"Loading LinearModel weights from {path}")
def save_if_supported(model) -> None:
if isinstance(model, Exportable):
model.export("/tmp/model.pkl")
else:
print(f"{type(model).__name__} does not support export")
cfg = TrainingConfig(learning_rate=0.01, batch_size=64, optimizer=Optimizer.ADAMW)
print(cfg)
# TrainingConfig(learning_rate=0.01, batch_size=64, max_epochs=100,
# optimizer=<Optimizer.ADAMW: 'adamw'>, weight_decay=0.0001)
save_if_supported(LinearModel()) # Exporting LinearModel weights to /tmp/model.pkl
save_if_supported("not a model") # str does not support export@dataclass is the default way to write data-holding classes -- it generates __init__, __repr__, and __eq__ from field annotations; use frozen=True for hashable value objects, order=True for sortable records, and __post_init__ for validationfield(default_factory=...) -- Python raises a clear error on results: list = [], but the underlying lesson is the same as function-argument mutable defaults: each instance must get a fresh container"linear" when you can use ModelType.LINEAR@runtime_checkable for supportWhat does @dataclass(frozen=True) add that plain @dataclass does not?
| Auto |
| Auto |
| Auto |
frozen=True |
frozen=True |
| Normal |
@dataclass(slots=True) | Auto | Auto | Auto | Optional | Optional | Smaller |
self.results = self.results or []self.resultsfield(default_factory=...)isinstance()__slots__ trades flexibility for memory -- cuts per-instance memory by 40–50% by replacing __dict__ with a fixed array; reach for it only when you have millions of identical-structure objects and a profiled memory bottleneck