Classes and Dataclasses
Model domain concepts with classes, enforce invariants, use dataclasses for value-oriented records, and avoid turning every dictionary into an object.
Before this lesson
Model cohesive objects
Use dataclasses appropriately
Protect valid state at construction
The short answer
Use a class when data and behavior form one concept with invariants. A dataclass removes record-like boilerplate but does not replace validation or design. Prefer simple functions and mappings when no meaningful object behavior exists.
Build the mental model
A class defines how instances are created and which attributes and methods they expose. self is the current instance. Methods should represent behavior belonging to the concept rather than serve as a folder for unrelated utility functions.
The central idea in this lesson is domain modeling. Read the rule, predict a concrete outcome, and then run the smallest example that can confirm or reject the prediction. This separates knowledge from familiarity with syntax.
Make the design choice explicit
@dataclass can generate initialization, representation, and equality for declared fields. frozen=True prevents normal reassignment and works well for value objects. Use post_init for validation while remembering that mutability of contained objects is separate.
from dataclasses import dataclass
@dataclass(frozen=True)
class Expense:
description: str
amount: float
def __post_init__(self):
if not self.description.strip() or self.amount <= 0:
raise ValueError("expense needs a description and positive amount")
expense = Expense("Notebook", 6.5)
print(f"{expense.description}: USD {expense.amount:.2f}")Expected output
Notebook: USD 6.50
Trace the example before running it. Identify each input, transformation, returned value, and side effect. Then change one boundary value and explain why the new behavior follows from the rule rather than memorizing the output.
Recognize failure modes
Avoid getters and setters that merely mirror public attributes. Protect a real invariant, not ceremony. Class-level mutable attributes are shared by all instances; use a dataclass default_factory for per-instance lists or dictionaries.
Use a professional practice loop
Turn the concept into a repeatable workflow: write down the expected behavior, implement one coherent change, run a representative example, and retain a regression check. If the result surprises you, capture the exact input and error before changing anything.
Review the program for names, boundaries, and hidden side effects. A solution is complete when another developer can understand its contract, reproduce its setup, and verify both the successful path and one meaningful failure path.
Quick knowledge check
Answer before you reveal.
01What does @dataclass not do automatically?
It does not choose good boundaries, enforce domain invariants, or make contained mutable values immutable.
02What four steps make the practice loop reliable?
Predict the behavior, make one coherent change, run it, and retain a check that detects regression.
Exercise
Practice challenge
Create a validated Book dataclass and a Library class whose methods lend and return books without allowing impossible state.
Requirements
- The successful path produces the documented result
- At least one boundary or invalid case is handled deliberately
- Calculation or domain logic is separated from console interaction
Optional extension: Add one automated regression check for the most important rule.
Open in Python compilerLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
Is domain modeling only important in large programs?
No. Small programs reveal the same rules with less noise, and learning the rule early prevents fragile habits from becoming architecture.
Should I memorize every API used here?
No. Memorize the mental model and how to verify behavior. Use documentation for exact names and parameters when needed.
How do I know the exercise is finished?
Meet every success criterion, test at least one boundary or failure case, and explain why the output follows from the code.