Lesson 11 · 11 of 20
Dataclasses · Python
Python dataclasses with @dataclass: free __init__, __repr__ and __eq__, field(default_factory), __post_init__, frozen and order.

Many classes only hold data. With the `@dataclass` decorator you just declare the fields with their types (product: str, quantity: int…) and Python writes __init__, __repr__ and __eq__ for you. That is why print(inv.lines[0]) is readable and two Line objects with the same data are equal with ==.
Fields with defaults go last: paid: bool = False. For a list, lines: list = [] will not do, because every invoice would share the same one; `field(default_factory=list)` creates a fresh list for each object.
`__post_init__` runs right after the generated __init__ and is the place to validate or clean data: here it strips the spaces from the customer name and title-cases it. Dataclasses can have ordinary methods too, like total().
@dataclass(frozen=True, order=True) gives Version two extras: `frozen` makes the objects read-only (assigning a field raises an error) and `order` compares them field by field, like tuples. So max() knows that 1.10 is newer than 1.9, something a plain text comparison would get wrong.
asdict(inv) turns the invoice, lines included, into plain dictionaries: the usual first step before saving it as JSON.
Code · main.py
from dataclasses import dataclass, field, asdict@dataclassclass Line: product: str quantity: int unit_price: float@dataclassclass Invoice: number: str customer: str lines: list[Line] = field(default_factory=list) # a new list each time paid: bool = False def __post_init__(self): # runs right after the generated __init__ self.customer = self.customer.strip().title() def total(self): return sum(l.quantity * l.unit_price for l in self.lines)@dataclass(frozen=True, order=True) # immutable and comparableclass Version: major: int minor: intinv = Invoice("F-2026-014", " maria lopez ")inv.lines.append(Line("Chair", 4, 49.5))inv.lines.append(Line("Table", 1, 230.0))print(inv.lines[0]) # free __repr__print(inv.customer, "owes", inv.total(), "| paid:", inv.paid)print("Equal:", Line("Chair", 4, 49.5) == inv.lines[0]) # free __eq__print(asdict(inv)["lines"][1])print("Newest:", max(Version(1, 9), Version(1, 10), Version(0, 99)))Output · python3 main.py
Line(product='Chair', quantity=4, unit_price=49.5)
Maria Lopez owes 428.0 | paid: False
Equal: True
{'product': 'Table', 'quantity': 1, 'unit_price': 230.0}
Newest: Version(major=1, minor=10)Key points
@dataclassgenerates__init__,__repr__and__eq__.- Use
field(default_factory=list)for default lists and dicts. __post_init__validates or adjusts data on creation.frozen=Truemakes it immutable;order=True, comparable.asdict()turns it into a dictionary.
Exercise
Add a discount: float = 0.0 field to Line, make Invoice.total() take it into account and give the chairs a 10% discount.