Dataclasses: records without the boilerplate
A class that mainly holds data needs the same __init__, the same readable printout and the same comparison every time. @dataclass writes them for you from a list of annotated fields.
- Declare fields as
name: type(with defaults) and let@dataclasswrite__init__, a readable printout and== - Use
field(default_factory=list)for a list per object, and__post_init__to tidy and validate - Make an immutable, hashable value with
@dataclass(frozen=True)
The classes in the Classes & Objects module repeated self.sku = sku for every attribute, printed as <__main__.Product object at 0x…>, and compared by identity. For classes whose job is mostly to hold data, the standard library's dataclasses module writes that code for you.
Put @dataclass on the line above class and list the fields with annotations: sku: str means "a field called sku, holding text". The decorator reads that list and adds an __init__ taking the fields in order, a printout showing every field, and == that compares the fields. A field with = value has a default, and fields with defaults must come after those without.
Run each example to see what it prints. Running examples doesn't count toward progress.
from dataclasses import dataclass @dataclass class Product: sku: str name: str price: float stock: int = 0 mat = Product("SPO-11681", "Yoga mat", 31.0, 12) lamp = Product("HOM-22847", "Desk lamp", 48.0) print(mat) print(lamp.stock, lamp == Product("HOM-22847", "Desk lamp", 48.0)) mat.stock -= 2 print(mat.stock, mat == Product("SPO-11681", "Yoga mat", 31.0, 12))
from dataclasses import dataclass @dataclass class Basket: customer_id: str items: list = [] print(Basket("C00038"))
Three more tools cover most real data classes:
field(default_factory=list)callslist()for every new object, so each gets its own list.__post_init__(self)runs right after the generated__init__: the place to tidy and validate fields (raise ValueError, as in the Classes & Objects module).@dataclass(frozen=True)makes objects read-only. Assigning a field raisesFrozenInstanceError(a kind of AttributeError). Frozen objects with equal fields hash equally, so they work in sets and as dictionary keys, which suits values like money or a (warehouse, SKU) pair.
from dataclasses import dataclass, field @dataclass class OrderLine: sku: str quantity: int unit_price: float def __post_init__(self): self.sku = self.sku.strip().upper() if self.quantity < 1: raise ValueError(f"{self.sku}: quantity must be at least 1") @dataclass class Order: order_id: str customer_id: str lines: list = field(default_factory=list) def total(self): return round(sum(line.quantity * line.unit_price for line in self.lines), 2) @dataclass(frozen=True) class Money: cents: int currency: str first = Order("O100282", "C00077") second = Order("O100918", "C01338") first.lines.append(OrderLine(" spo-11681", 2, 17.92)) print(first.total(), second.lines) print(first.lines[0]) price = Money(1250, "GBP") try: price.cents = 999 except AttributeError as err: print(type(err).__name__, err) print(price == Money(1250, "GBP"), len({price, Money(1250, "GBP"), Money(1250, "EUR")}))
When to use it: for records: rows from a file, API payloads, configuration, value objects. Keep ordinary classes for objects whose main job is behaviour.
Mistakes to avoid: expecting annotations to convert or check types (Product("A", "B", "31") stores the text "31"; validate in __post_init__); a list default instead of field(default_factory=list); and a field without a default after one with a default (a TypeError when the class is defined).
@dataclasswrites__init__, a readable printout and field-by-field==fromname: typelines.- Use
field(default_factory=list)for per-object lists and__post_init__to tidy and validate. frozen=Truemakes read-only, hashable values.- Next: properties, for attributes that are computed or checked when they are read or set.