Lesson 19: Dataclasses & Modern OOP
Write less boilerplate with @dataclass, frozen data, and professional OOP best practices.
Dataclasses — Boilerplate Be Gone
Writing a simple data class by hand means writing __init__, __repr__, and __eq__ — dozens of lines of boring code. The @dataclass decorator auto-generates all of it from your type-annotated fields:
from dataclasses import dataclass
@dataclass
class Course:
title: str
lessons: int
tags: list = field(default_factory=list)
python = Course("Python", 25, ["python", "ai"])
print(python) # Course(title='Python', lessons=25, tags=['python', 'ai'])
print(python == Course("Python", 25, ["python", "ai"])) # True — free __eq__
Dataclasses (Python 3.7+) have become the default way to define data containers in modern Python. If a class is mostly "hold these fields and let me compare/print them", a dataclass is the professional choice.
What You'll Learn in This Lesson
- Define dataclasses with type-annotated fields
- Use
field()anddefault_factoryfor mutable defaults - Freeze data with
frozen=True - Add ordering with
order=True
The Free Boilerplate
| Without @dataclass | With @dataclass |
|---|---|
__init__ written by hand |
Auto-generated |
__repr__ written by hand |
Auto-generated |
__eq__ written by hand |
Auto-generated |
| Type hints optional | Type hints required |
What used to take 30 lines takes 10 — and is impossible to get wrong.
The "before" picture — what the decorator saves you from writing:
# Without dataclass — 15+ lines of ceremony
class Course:
def __init__(self, title, lessons, tags):
self.title = title
self.lessons = lessons
self.tags = tags
def __repr__(self):
return f"Course(title={self.title!r}, lessons={self.lessons!r}, tags={self.tags!r})"
def __eq__(self, other):
if not isinstance(other, Course):
return NotImplemented
return (self.title, self.lessons, self.tags) == (other.title, other.lessons, other.tags)
The @dataclass decorator generates exactly this — correctly, every time.
field() and default_factory — Safe Mutable Defaults
The classic Python trap is def f(items=[]) — a shared mutable default. Dataclasses solve it with field(default_factory=...):
from dataclasses import dataclass, field
@dataclass
class Student:
name: str
tags: list = field(default_factory=list) # NEW list per instance
s1 = Student("Amol")
s1.tags.append("python")
print(s1.tags) # ['python']
print(Student("Riya").tags) # [] — separate list!
Never write
tags: list = []— every instance would share one list.
Why field(default_factory=...) and not just = []? The default value is evaluated once at class definition. default_factory is a function that runs per instance — each object gets its own fresh container.
Frozen Data — Immutable by Design
@dataclass(frozen=True) makes instances immutable — perfect for configuration and constants:
@dataclass(frozen=True)
class Config:
version: str = "1.0"
debug: bool = False
cfg = Config()
print(cfg.version) # 1.0
# cfg.debug = True # TypeError: frozen instance
Immutable data is safer: it cannot be accidentally changed, and instances can be hashed and used as dict keys.
Modifying a frozen dataclass — the sanctioned way:
from dataclasses import replace
cfg2 = replace(cfg, debug=True) # returns a NEW Config with debug=True
print(cfg2) # Config(version='1.0', debug=True)
print(cfg.debug) # False — original untouched
order=True — Sorting for Free
@dataclass(order=True) adds __lt__, __le__, __gt__, __ge__ — your objects become sortable:
@dataclass(order=True)
class Product:
price: float
name: str
items = [Product(300, "Monitor"), Product(25, "Mouse")]
print(sorted(items)) # cheapest first
Field ordering matters with order=True: objects compare by fields left to right. Put the primary sort key first (price before name above).
Dataclasses vs NamedTuples vs Plain Classes
| Tool | When to use |
|---|---|
| NamedTuple | Tiny immutable records, no methods |
| @dataclass | Data containers with optional methods |
| Plain class | Objects with significant behavior |
Dataclasses with methods are fully supported — a dataclass can have regular methods alongside the auto-generated boilerplate:
@dataclass
class Course:
title: str
lessons: int
def summary(self): # a normal method
return f"{self.title} — {self.lessons} lessons"
Real-World Dataclass Uses
- DTOs (Data Transfer Objects): API request/response payloads with type hints.
- Configuration objects: frozen settings loaded once.
- Database models: table rows as typed records.
- Value objects: coordinates, money, ranges with
__eq__/order. - JSON serialization: combined with
dataclasses.asdict()for quickjson.dumps.
Common Mistakes to Avoid
- Mistake:
tags: list = []as a default — Fix:field(default_factory=list). - Mistake: Expecting to modify a frozen dataclass — Fix: use
dataclasses.replace(obj, field=value)for a modified copy. - Mistake: Using dataclasses for objects with complex behavior — Fix: use a regular class.
- Mistake: Forgetting type annotations — Fix: dataclass fields need annotations:
name: str. - Mistake: Relying on field order for
order=Truecomparisons without checking which field sorts first — Fix: put the primary sort key first.
Professional Tips & Tricks
- Use
field(default_factory=list)for mutable defaults — never[]directly (shared trap). - Reach for dataclasses for DTOs, API payloads, and config objects.
- Combine dataclasses with type hints for self-documenting code that linters can verify.
- Use
frozen=Truefor config and constants;replace()for safe "changes". - Use
dataclasses.asdict()for quick conversion to plain dicts for JSON.
Key Takeaways
@dataclassauto-generates__init__,__repr__, and__eq__.field(default_factory=list)gives each instance its own mutable default.frozen=Truemakes immutable, hashable data.order=Trueenables sorting.- Use dataclasses for data containers; plain classes for behavioral objects.
replace()modifies frozen instances by returning a new copy.
Next up: Module 6 — files, errors & professional Python.
from dataclasses import dataclass, field
@dataclass
class Course:
title: str
lessons: int
tags: list = field(default_factory=list)
def summary(self):
return f"{self.title} — {self.lessons} lessons"
@dataclass(frozen=True)
class Config:
version: str = "1.0"
debug: bool = False
# Free __init__, __repr__, __eq__
python = Course("Python", 25, ["python", "ai"])
print(python)
print(python.summary())
print("Equal:", python == Course("Python", 25, ["python", "ai"]))
cfg = Config()
print("Config version:", cfg.version)
# cfg.debug = True # TypeError: frozen instanceLesson Code (Python)
from dataclasses import dataclass, field
@dataclass
class Course:
title: str
lessons: int
tags: list = field(default_factory=list)
def summary(self):
return f"{self.title} — {self.lessons} lessons"
@dataclass(frozen=True)
class Config:
version: str = "1.0"
debug: bool = False
# Free __init__, __repr__, __eq__
python = Course("Python", 25, ["python", "ai"])
print(python)
print(python.summary())
print("Equal:", python == Course("Python", 25, ["python", "ai"]))
cfg = Config()
print("Config version:", cfg.version)
# cfg.debug = True # TypeError: frozen instanceConsole Output
Course(title='Python', lessons=25, tags=['python', 'ai'])
Python — 25 lessons
Equal: True
Config version: 1.0Code Visualization Tips
- Picture a dataclass as a form with labeled fields — the framework fills in the boring parts (init, repr, eq).
- Visualize frozen=True as a sealed box: read anytime, change never.
- default_factory makes a NEW list per instance — visualize each object getting its own empty box.
Professional Tips & Tricks
- Use field(default_factory=list) for mutable defaults — never [] directly (shared trap).
- Reach for dataclasses for DTOs, API payloads, and config objects.
- Combine dataclasses with type hints for self-documenting code that linters can verify.
Python Code Judge & Practice Arena
LeetCode StyleRun real Python 3.12 WebAssembly code directly in your browser against automated test suites.
Problem 1: Product Dataclass 1
Test Your Knowledge
Instant feedbackQuick Check: Lesson 19: Dataclasses, Field Configuration, Immutability & Validation
Up next · Continue learning
File Handling & Context Managers
Read and write files safely with the with statement, work with paths, and handle CSV data.