An order is a small record: id, items, total, paid. Writing __init__, __repr__, and __eq__ by hand is a page of boilerplate. What does @dataclass generate for free?
A dataclass is a regular class with generated methods for initialization, display, and equality. It is a good fit for small records such as points, users, and configuration values. Fields are declared with annotations, and defaults are written like normal assignments. You will also use default_factory, validate in __post_init__, and update frozen records with replace.
Declare fields with annotations
The @dataclass decorator reads the annotated class attributes and writes an __init__ for you. Creating Point(2, 3) assigns x and y in order. The generated __repr__ makes the object readable in the console.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
point = Point(2, 3)
print(point)A small record
Add a default value
A field with a default can be omitted at construction. Fields without defaults must come first in the class body. The repr shows both the field names and their current values.
from dataclasses import dataclass
@dataclass
class User:
name: str
active: bool = True
print(User("Ada"))Defaults in the field list
Compare and convert
Dataclasses generate __eq__ from their fields, so two objects with the same data compare equal. asdict returns a plain dictionary, which is convenient for JSON or test assertions. frozen=True prevents later attribute assignment.
from dataclasses import dataclass, asdict
@dataclass(frozen=True)
class Item:
name: str
price: float
item = Item("tea", 3.5)
print(asdict(item))
print(Item("tea", 3.5) == item)Equality and a plain mapping
Mutable defaults need a factory
A list default in a dataclass would be shared by every instance, so Python rejects it. field(default_factory=list) creates a new list per object. The same rule applies to dictionaries, sets, and other mutable values.
from dataclasses import dataclass, field
@dataclass
class Order:
drink: str
tags: list[str] = field(default_factory=list)
first = Order("latte")
second = Order("tea")
first.tags.append("gift")
print(first.tags)
print(second.tags)
print(first.tags is second.tags)One fresh list per instance
__post_init__ validates and derives
__post_init__ runs after the generated __init__. Use it to reject impossible values and compute fields that must stay consistent. It should validate the domain, not try to enforce type hints that Python never checked.
from dataclasses import dataclass
@dataclass
class Order:
price: float
quantity: int
total: float = 0.0
def __post_init__(self):
if self.price < 0 or self.quantity <= 0:
raise ValueError("invalid order")
self.total = round(self.price * self.quantity, 2)
for values in [(4.5, 2), (-1, 1)]:
try:
print(Order(*values))
except ValueError as error:
print(type(error).__name__, error)Validate, then derive the total
Update a frozen record with replace
frozen=True blocks attribute assignment, which makes a record safe to share. replace() returns a new instance with selected fields changed; the original stays intact. That pattern is useful for undo, history, and configuration snapshots.
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Order:
drink: str
price: float
original = Order("latte", 4.5)
updated = replace(original, price=5.0)
print(original)
print(updated)
print(original == updated)Change by copying, not mutating
Key points
- @dataclass generates __init__, __repr__, and __eq__.
- Fields are annotated class attributes.
- Fields with defaults must come after fields without defaults.
- asdict converts a dataclass instance to a dictionary.
- frozen=True makes an instance immutable.
- Use default_factory for mutable fields; validate in __post_init__; replace frozen records.
Common mistakes
- Putting a field without a default after a field with one.
- Expecting dataclass fields to validate types at runtime.
- Trying to assign to a frozen instance.
- Writing tags: list[str] = [] and creating one shared list for every instance.
Create a frozen Item dataclass with name and price, then print asdict(Item('tea', 3.5)).
Try it yourself
Change the values, predict the output, then run the code. The Python runtime is downloaded the first time you press Run, so the first run can take a few seconds.
Practice this lesson
12 questions with immediate explanations.