Skip to content

    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:

    intermediario/cap36_dataclasses.pylinhas 10 a 22
    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))
    
    Output
    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=...):

    intermediario/cap36_dataclasses.pylinhas 27 a 32
    try:
        @dataclass
        class Ruim:
            itens: list = []
    except ValueError as erro:
        print(erro)
    
    Output
    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:

    intermediario/cap36_dataclasses.pylinhas 37 a 48
    @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__)
    
    Output
    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:

    intermediario/cap36_dataclasses.pylinhas 53 a 71
    @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)
    
    Output
    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 tupleNamedTuple
    External data (JSON) with known shape, only for type toolsTypedDict
    A domain object with data, methods, and simple validationdataclass
    Heavy validation and conversion of input dataA 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.