Módulo 16 - Projeto final: mini biblioteca de utilidades

Dataclasses tipadas e Enum do domínio

10 min de leitura · por Cesar Gargiulo, revisado pela equipe ValorFinal e GuardiaSec · Atualizado em 01/07/2026

O que você vai aprender

  • Modelar um Produto com dataclass totalmente tipada.
  • Criar uma Enum Categoria para valores fixos do domínio.
  • Usar frozen e __post_init__ para imutabilidade e validação.
  • Entender por que dataclass e Enum deixam o domínio mais seguro.

A Enum do domínio

Começamos pelos valores fixos do domínio. Um produto pertence a uma categoria, e o conjunto de categorias é conhecido e limitado. Representar isso com texto solto, como a palavra bebida escrita à mão em cada lugar, é um convite a erros de digitação: basta escrever bebidas no plural em um ponto e a comparação falha em silêncio. A Enum resolve isso: ela define um conjunto nomeado de valores válidos. Usar Categoria.BEBIDA é seguro, o editor completa, e qualquer valor fora da lista é um erro imediato, não um bug escondido.

# src/catalogo/modelos.py
from enum import Enum

class Categoria(Enum):
    BEBIDA = "bebida"
    ALIMENTO = "alimento"
    LIMPEZA = "limpeza"
    HIGIENE = "higiene"

# Uso seguro: o editor completa e valores invalidos falham na hora
c = Categoria.BEBIDA
print(c.value)          # bebida
print(Categoria("alimento"))   # Categoria.ALIMENTO

A Enum Categoria fixa as opções válidas; cada membro tem um nome e um valor legível.

Repare que cada membro da Enum tem um nome, em maiúsculas, e um valor associado, aqui um texto minúsculo. O nome é o que você usa no código, Categoria.BEBIDA, claro e à prova de digitação. O valor é útil para exibir, salvar em arquivo ou reconstruir a categoria a partir de um texto vindo de fora, com Categoria(alimento). Essa dupla, nome estável para o código e valor legível para o mundo externo, é o que torna a Enum a ferramenta certa para representar conjuntos fechados de opções no domínio.

A dataclass Produto

Com a categoria pronta, modelamos o produto. Uma dataclass é perfeita: você declara os campos com seus tipos e o decorador gera o construtor, a representação legível e a comparação por valor, tudo sem escrever esse código repetitivo. O Produto tem nome, preço e categoria, cada um com seu tipo. Como preço aqui é dinheiro, usamos o tipo Decimal, que evita os erros de arredondamento do ponto flutuante em contas de dinheiro. E marcamos a classe como frozen, ou seja, imutável: uma vez criado, o produto não muda, o que evita uma classe inteira de bugs de alteração acidental.

# src/catalogo/modelos.py (continuacao)
from dataclasses import dataclass
from decimal import Decimal

@dataclass(frozen=True)
class Produto:
    nome: str
    preco: Decimal
    categoria: Categoria

    def __post_init__(self):
        if not self.nome.strip():
            raise ValueError("nome do produto nao pode ser vazio")
        if self.preco < 0:
            raise ValueError("preco nao pode ser negativo")

cafe = Produto("Cafe", Decimal("18.90"), Categoria.BEBIDA)
print(cafe)   # Produto(nome="Cafe", preco=Decimal("18.90"), categoria=Categoria.BEBIDA)

A dataclass Produto: campos tipados, imutável com frozen, validada no __post_init__.

Sem dataclass e Enum

  • Escrever __init__ e __repr__ na mão
  • Categoria como texto solto, sujeito a erro de digitação
  • Comparação por identidade, não por valor
  • Validação espalhada por quem cria o objeto

Com dataclass e Enum

  • Construtor e representação gerados sozinhos
  • Categoria com valores fixos e verificáveis
  • Comparação por valor, de graça
  • Validação central no __post_init__

Validação com __post_init__

A dataclass gera o construtor, mas não sabe as regras do seu domínio: que o nome não pode ser vazio e que o preço não pode ser negativo. É aí que entra o método especial __post_init__, chamado automaticamente logo depois que a dataclass monta o objeto. Ele é o lugar certo para validar. Se algo estiver errado, ele levanta um erro na hora da criação, garantindo que nenhum Produto inválido consiga existir. Essa é a ideia de tornar estados inválidos irrepresentáveis: se o objeto foi criado, ele é válido, e o resto do código pode confiar nisso sem reverificar.

Combinando as três decisões, o modelo fica robusto por construção. A tipagem torna o contrato explícito e verificável pelo mypy. O frozen impede alterações acidentais, o que também deixa o produto seguro para usar como chave de dicionário ou em um set. E o __post_init__ centraliza a validação, de modo que a garantia acontece num lugar só, não espalhada por quem cria o objeto. Nas próximas aulas, a lógica de preço vai receber esses produtos confiando que eles são válidos, porque o modelo já garantiu isso. É a fundação de tudo que vem depois.

Teste rápido

Para que serve o método __post_init__ numa dataclass?

Perguntas frequentes

Por que usar Decimal em vez de float para preço?
Porque float representa números em binário e sofre erros de arredondamento em contas decimais, o que é inaceitável para dinheiro. O Decimal representa os valores com precisão decimal exata, ideal para preços e valores financeiros. Em domínios de dinheiro, prefira sempre Decimal ao float.
O que o frozen=True traz para a dataclass?
Ele torna as instâncias imutáveis: depois de criadas, seus campos não podem ser alterados. Isso evita bugs de mudança acidental, deixa o objeto seguro para usar como chave de dicionário ou em um set, e torna o código mais fácil de raciocinar, porque o valor não muda pelas suas costas.
Qual a vantagem da Enum sobre usar textos para categoria?
A Enum fixa o conjunto de valores válidos e dá nomes estáveis a eles. Isso elimina erros de digitação, deixa o editor completar as opções e faz qualquer valor fora da lista falhar imediatamente. Texto solto, ao contrário, aceita qualquer coisa e esconde erros de escrita como bugs silenciosos.
A dataclass gera comparação entre objetos?
Sim. Por padrão, ela gera o método de igualdade comparando os campos, então dois produtos com o mesmo nome, preço e categoria são considerados iguais. Isso é comparação por valor, muito mais útil no dia a dia que a comparação por identidade padrão de objetos comuns.
Posso ter campos opcionais numa dataclass?
Pode. Basta dar um valor padrão ao campo, como estoque igual a zero. Campos com padrão devem vir depois dos sem padrão na declaração. Para valores padrão que são listas ou dicionários, use field com default_factory, para não compartilhar o mesmo objeto entre instâncias.

Fontes

Seu progresso fica salvo neste aparelho. Assinantes sincronizam entre os aparelhos.