Course progress Course outline 24 of 24 lessons available
Python Foundations
Data and Collections
Building Reliable Programs
Modeling with Objects
Professional Python
Advanced Python
A dataclass is a printed record card
A normal class can spend many lines receiving fields, displaying them, and comparing them. @dataclass prints that routine record-card machinery for you.
- Name fields title and minutes
- Dataclass generates routine methods
- Record readable and comparable
from dataclasses import dataclass
@dataclass
class Reading:
title: str
minutes: int
first = Reading("Enums", 25)
second = Reading("Enums", 25)
print(first)
print(first == second)
print(first is second)
Reading(title='Enums', minutes=25)
True
False
By default, the decorator generates __init__(), a useful __repr__(), and field-by-field __eq__() for the same class. Equal values are still two objects, so is remains false. Field annotations also do not validate runtime arguments automatically; Chapter 16 explains that job split.
Give every card its own mutable pocket
Fields without defaults must come before fields with defaults because the generated initializer follows field order:
@dataclass
class Article:
title: str
minutes: int
completed: bool = False
Putting a required field after completed would raise TypeError when the class is created. Simple immutable defaults such as False are fine. A mutable list, dictionary, or set needs a factory:
- Aki's card needs a tag pocket
- Mina's card needs another pocket
- Factory makes a fresh list each time
- No sharing one card's tags stay private
default_factory prevents accidental shared mutable state.from dataclasses import dataclass, field
@dataclass
class Notebook:
owner: str
tags: list[str] = field(default_factory=list)
mine = Notebook("Aki")
yours = Notebook("Mina")
mine.tags.append("python")
print(mine.tags, yours.tags)
print(mine.tags is yours.tags)
['python'] []
False
Pass the factory itself—list, without ()—so the generated initializer calls it for every new instance.
Check the card after its fields arrive
The generated initializer calls __post_init__() after assigning fields. Use it for small validation, normalization, or derived fields:
from dataclasses import dataclass, field
@dataclass
class Session:
topic: str
minutes: int
label: str = field(init=False)
def __post_init__(self):
self.topic = self.topic.strip()
if not self.topic or self.minutes <= 0:
raise ValueError("topic and minutes must be valid")
self.label = f"{self.topic} ({self.minutes} min)"
print(Session(" Python ", 30).label)
Python (30 min)
init=False keeps label out of constructor arguments. Keep file access and big workflows out of __post_init__() so construction stays predictable.
@dataclass(frozen=True) blocks normal field reassignment. Use dataclasses.replace() to make a changed copy. But frozen is a shallow promise:
@dataclass(frozen=True)
class FrozenPocket:
tags: list[str] = field(default_factory=list)
pocket = FrozenPocket()
pocket.tags.append("still changes")
print(pocket.tags)
['still changes']
A frozen dataclass normally gets a hash when equality is generated, but hashing still fails if a compared field contains an unhashable list. Deeply stable values need immutable fields such as strings, numbers, tuples, and frozen sets. unsafe_hash=True is not a magic fix for changing data.
An Enum is a menu with fixed choices
An Enum replaces loose strings with known member objects.
- Menu LOW, NORMAL, HIGH
-
Member
Priority.HIGH -
Stored value
"high"
from enum import Enum
class Priority(Enum):
LOW = "low"
NORMAL = "normal"
HIGH = "high"
choice = Priority("high")
print(choice)
print(choice.name)
print(choice.value)
print(choice is Priority.HIGH)
print(choice == "high")
Priority.HIGH
HIGH
high
True
False
Members are singletons inside their Enum, so identity comparison with is is conventional. Priority("high") looks up by value; Priority["HIGH"] looks up by name. Unknown input raises ValueError or KeyError. Plain Enum members are not ordered with < merely because their values might be; use an explicit key or rank.
Tiny project: Prioritized Reading Queue
Create reading_queue.py. This standard-library program combines a frozen item dataclass, an Enum, validation, and a queue whose list comes from default_factory.
from dataclasses import dataclass, field
from enum import Enum
class Priority(Enum):
LOW = "low"
NORMAL = "normal"
HIGH = "high"
RANK = {Priority.LOW: 1, Priority.NORMAL: 2, Priority.HIGH: 3}
@dataclass(frozen=True)
class ReadingItem:
title: str
minutes: int
priority: Priority = Priority.NORMAL
def __post_init__(self):
clean = self.title.strip()
if not clean:
raise ValueError("title must not be blank")
if (
isinstance(self.minutes, bool)
or not isinstance(self.minutes, int)
or self.minutes <= 0
):
raise ValueError("minutes must be a positive integer")
if not isinstance(self.priority, Priority):
raise TypeError("priority must be a Priority")
object.__setattr__(self, "title", clean)
@dataclass
class ReadingQueue:
name: str
minute_limit: int = 45
items: list[ReadingItem] = field(default_factory=list)
def add(self, item):
self.items.append(item)
def plan(self):
ordered = sorted(
self.items,
key=lambda item: (-RANK[item.priority], item.minutes, item.title),
)
chosen = []
used = 0
for item in ordered:
if used + item.minutes <= self.minute_limit:
chosen.append(item)
used += item.minutes
return chosen
queue = ReadingQueue("Tonight")
for title, minutes, raw_priority in [
(" Dataclasses ", 25, "high"),
("Enum boundaries", 15, "normal"),
("Hashing notes", 10, "high"),
]:
queue.add(ReadingItem(title, minutes, Priority(raw_priority)))
plan = queue.plan()
print(f"{queue.name}:")
for number, item in enumerate(plan, start=1):
print(f"{number}. [{item.priority.name}] {item.title} - {item.minutes} min")
print(f"Total: {sum(item.minutes for item in plan)} min")
print(f"Unique items: {len(set(plan))}")
print(f"Fresh queue is empty: {ReadingQueue('Tomorrow').items == []}")
Run python3 reading_queue.py and check:
Tonight:
1. [HIGH] Hashing notes - 10 min
2. [HIGH] Dataclasses - 25 min
Total: 35 min
Unique items: 2
Fresh queue is empty: True
The frozen items are hashable because all compared fields are hashable. Boundary reminder: annotations do not enforce Priority; __post_init__() does. Also, a negative limit is outside this tiny queue’s contract and should be validated before accepting user input.
Three tiny missions
- Factory proof. Make two queues, add to one, and prove the other’s item list stays empty.
- Frozen copy. Use
dataclasses.replace()to make a completed version of an item without changing the original. - Enum boundary. Parse one valid and one unknown priority value, giving the unknown choice a clear error message.
You are ready for Chapter 16 when…
- you know what default
@dataclassgenerates and whatisstill means; - you put required fields before default fields;
- you use
default_factoryfor independent mutable defaults; - you validate or derive small values in
__post_init__(); - you can explain why frozen is shallow and hashing depends on every field;
- you distinguish an Enum member, its
name, and itsvalue; - you compare members by identity and parse outside text at a boundary;
- you can run the queue and see
Fresh queue is empty: True.
Next, you will add type hints and protocols that make these data and behavior contracts visible to static tools.