Chapter 36, Intermediate
dataclasses
A dataclass writes the `__init__`, the `__repr__`, and the `__eq__` for you. For classes that exist mainly to hold data, it eliminates a lot of repetitive code.
Code for this chapter: intermediario/cap36_dataclasses.py
The basics
Just declare the fields with type, and the decorator generates the rest:
from dataclasses import dataclass, field, asdict, replace
@dataclass
class Produto:
nome: str
preco: float
tags: list[str] = field(default_factory=list)
p = Produto("caneta", 3.5)
print(p)
print(p == Produto("caneta", 3.5))
Produto(nome='caneta', preco=3.5, tags=[])
True
The mutable default value, again
The same problem from chapter 18 comes back here, and the dataclass prevents it with a clear error. For mutable fields, use field(default_factory=...):
try:
@dataclass
class Ruim:
itens: list = []
except ValueError as erro:
print(erro)
mutable default <class 'list'> for field itens is not allowed: use default_factory
Immutable, sortable, and economical
The options of the decorator cover common cases. frozen=True prevents modification after creation (and makes the object hashable). order=True generates comparison operators. slots=True reduces the memory of each instance:
@dataclass(frozen=True, order=True, slots=True)
class Versao:
major: int
minor: int = 0
v1, v2 = Versao(1, 2), Versao(1, 10)
print(v1 < v2, sorted([v2, v1]))
try:
v1.major = 5
except Exception as erro:
print(type(erro).__name__)
True [Versao(major=1, minor=2), Versao(major=1, minor=10)]
FrozenInstanceError
Validation, copying, and conversion
The method __post_init__ runs right after the __init__ and is the place for validation. The functions asdict and replace convert to dictionary and create a copy with swapped fields, without mutating the original:
@dataclass
class Pedido:
cliente: str
itens: list[tuple[str, float]] = field(default_factory=list)
def __post_init__(self):
if not self.cliente:
raise ValueError("cliente é obrigatório")
@property
def total(self):
return sum(preco for _, preco in self.itens)
pedido = Pedido("Ana", [("caneta", 3.5), ("caderno", 18.9)])
print(round(pedido.total, 2))
print(asdict(pedido))
copia = replace(pedido, cliente="Bia")
print(copia.cliente, pedido.cliente)
22.4
{'cliente': 'Ana', 'itens': [('caneta', 3.5), ('caderno', 18.9)]}
Bia Ana
When to use each option
| I need... | Use |
|---|---|
| An immutable and lightweight record, which also behaves like a tuple | NamedTuple |
| External data (JSON) with known shape, only for type tools | TypedDict |
| A domain object with data, methods, and simple validation | dataclass |
| Heavy validation and conversion of input data | A library like Pydantic |
Exercise 1
A validated book
Create the dataclass Livro(titulo, paginas) that rejects paginas less than or equal to zero with ValueError.