O que são Dataclasses e Por Que Importam
Dataclasses são uma ferramenta poderosa do Python moderno (introduzidas na versão 3.7) que simplificam drasticamente a criação de classes destinadas principalmente ao armazenamento de dados. Antes delas, você precisava escrever muito código repetitivo: __init__, __repr__, __eq__ e outros métodos especiais. As dataclasses automatizam isso através do decorador @dataclass, reduzindo significativamente a quantidade de boilerplate.
A motivação por trás das dataclasses é clara: em muitos projetos, criamos classes apenas para agrupar dados relacionados — pense em uma classe Pessoa com atributos como nome, idade e email. O Python exigia que você implementasse manualmente toda a infraestrutura para essas classes funcionarem bem. As dataclasses reconhecem esse padrão comum e oferecem automação elegante, mantendo a legibilidade do código e seguindo os princípios da linguagem.
Começando com @dataclass: O Básico
Estrutura Fundamental
O decorador @dataclass transforma uma classe comum em uma dataclass. Você simplesmente anota seus atributos com type hints, e o Python cuida do resto. Veja um exemplo prático:
from dataclasses import dataclass
@dataclass
class Pessoa:
nome: str
idade: int
email: str
# Uso imediato
pessoa1 = Pessoa("Alice", 30, "alice@example.com")
print(pessoa1)
# Saída: Pessoa(nome='Alice', idade=30, email='alice@example.com')
pessoa2 = Pessoa("Bob", 25, "bob@example.com")
print(pessoa1 == pessoa2) # False — comparação automática
Neste exemplo, você não precisou escrever __init__, __repr__ ou __eq__. O decorador gerou tudo automaticamente. O __repr__ é particularmente útil para debug, pois mostra claramente o estado do objeto. A comparação por igualdade também funciona inteligentemente: duas instâncias são iguais se todos os seus atributos forem iguais.
Valores Padrão e Flexibilidade
As dataclasses suportam valores padrão para atributos, funcionando como parâmetros opcionais em funções:
from dataclasses import dataclass
@dataclass
class Configuracao:
host: str
porta: int = 8080
debug: bool = False
# Diferentes formas de instanciar
config1 = Configuracao("localhost")
print(config1)
# Saída: Configuracao(host='localhost', porta=8080, debug=False)
config2 = Configuracao("example.com", 443, True)
print(config2)
# Saída: Configuracao(host='example.com', porta=443, debug=True)
Uma regra importante: atributos sem valor padrão devem vir antes dos que têm. Caso contrário, o Python lança um TypeError em tempo de classe. Isso mantém a ordem lógica dos parâmetros no __init__.
Controle Avançado com fields()
Inspecionando Campos
A função fields() do módulo dataclasses retorna informações sobre os campos de uma dataclass. Isso é útil quando você precisa iterar sobre os atributos ou acessar metadados:
from dataclasses import dataclass, fields
@dataclass
class Produto:
nome: str
preco: float
estoque: int = 0
# Inspecionando os campos
for campo in fields(Produto):
print(f"Campo: {campo.name}, Tipo: {campo.type}, Padrão: {campo.default}")
Saída:
Campo: nome, Tipo: <class 'str'>, Padrão: <dataclasses._MISSING_TYPE object at ...>
Campo: preco, Tipo: <class 'float'>, Padrão: <dataclasses._MISSING_TYPE object at ...>
Campo: estoque, Tipo: <class 'int'>, Padrão: 0
A função fields() é poderosa para validação genérica, serialização e frameworks que precisam explorar a estrutura dinâmica das classes. Cada campo retornado é um objeto Field com atributos como name, type, default, default_factory e outros.
Field com default_factory
Há uma pegadinha comum em Python: usar listas ou dicionários como valores padrão. O default_factory resolve isso elegantemente:
from dataclasses import dataclass, field
@dataclass
class Equipe:
nome: str
membros: list = field(default_factory=list)
# Sem default_factory (ERRADO — evite)
# membros: list = [] # Compartilharia a mesma lista entre instâncias!
equipe1 = Equipe("Backend")
equipe2 = Equipe("Frontend")
equipe1.membros.append("Alice")
print(equipe1.membros) # ['Alice']
print(equipe2.membros) # [] — lista separada, como esperado
# Outro exemplo com dicionário
@dataclass
class Cache:
nome: str
dados: dict = field(default_factory=dict)
cache1 = Cache("cache_a")
cache1.dados["chave"] = "valor"
print(cache1.dados) # {'chave': 'valor'}
Sem default_factory, todas as instâncias compartilhariam o mesmo objeto mutável, causando bugs silenciosos. O default_factory recebe uma função que é chamada para cada nova instância, garantindo dados independentes.
Inicialização Customizada com __post_init__
O Problema e a Solução
Às vezes você precisa executar lógica de validação ou transformação após o __init__ gerado automaticamente. O método __post_init__ é invocado imediatamente após o construtor, permitindo esse tipo de customização:
from dataclasses import dataclass
@dataclass
class Usuario:
nome: str
email: str
def __post_init__(self):
# Validação simples
if not self.email or "@" not in self.email:
raise ValueError("Email inválido")
# Transformação
self.nome = self.nome.strip().title()
# Funcionamento
try:
user1 = Usuario(" alice silva ", "alice@example.com")
print(user1) # Usuario(nome='Alice Silva', email='alice@example.com')
user2 = Usuario("Bob", "bob_invalid") # Lança ValueError
except ValueError as e:
print(f"Erro: {e}")
Esse padrão é muito mais limpo do que sobrescrever __init__. Você aproveita a geração automática de parâmetros enquanto adiciona lógica específica do seu domínio.
Cenário Mais Complexo: Conversão de Tipos
__post_init__ é ideal para transformar dados recebidos em tipos internos apropriados:
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Evento:
titulo: str
data_str: str # Recebe como string
def __post_init__(self):
# Converte string em datetime
try:
self.data = datetime.strptime(self.data_str, "%d/%m/%Y")
except ValueError:
raise ValueError(f"Formato de data inválido: {self.data_str}")
# Remove o atributo temporário se não for mais necessário
delattr(self, 'data_str')
evento = Evento("Reunião de Sprint", "15/01/2025")
print(evento)
print(f"Data processada: {evento.data}")
Aqui usamos __post_init__ não apenas para validação, mas para transformar a entrada em um formato mais útil internamente. Isso desacopla a interface pública (aceita strings) da implementação interna (usa datetime).
Opções do Decorador @dataclass
Controle Fino Sobre Geração
O decorador @dataclass aceita parâmetros que modificam seu comportamento:
from dataclasses import dataclass
# frozen=True torna a dataclass imutável
@dataclass(frozen=True)
class Ponto:
x: float
y: float
ponto = Ponto(1.0, 2.0)
print(ponto) # Ponto(x=1.0, y=2.0)
# Tentativa de modificação gera erro
try:
ponto.x = 5.0 # FrozenInstanceError
except Exception as e:
print(f"Erro: {type(e).__name__}: {e}")
# order=True habilita comparações
@dataclass(order=True)
class Produto:
preco: float
nome: str
p1 = Produto(10.0, "A")
p2 = Produto(20.0, "B")
print(p1 < p2) # True — comparação baseada em preco
# eq=False desativa geração de __eq__
@dataclass(eq=False)
class Sessao:
id: str
usuario: str
s1 = Sessao("123", "alice")
s2 = Sessao("123", "alice")
print(s1 == s2) # False — __eq__ não foi gerado
print(s1 is s2) # False — são objetos diferentes
Os parâmetros mais comuns são:
- frozen=True: torna a instância imutável (como namedtuple)
- order=True: gera métodos de comparação (__lt__, __le__, etc.)
- eq=True (padrão): gera __eq__
- repr=True (padrão): gera __repr__
Padrões Práticos e Casos de Uso Reais
Exemplo 1: Integração com APIs
Dataclasses são excelentes para modelar respostas de API:
from dataclasses import dataclass
from typing import Optional
import json
@dataclass
class Usuario:
id: int
nome: str
email: str
telefone: Optional[str] = None
def para_json(self) -> dict:
return {
"id": self.id,
"nome": self.nome,
"email": self.email,
"telefone": self.telefone
}
# Simulando resposta de API
resposta_api = {"id": 1, "nome": "Alice", "email": "alice@ex.com", "telefone": None}
usuario = Usuario(**resposta_api)
print(usuario)
print(json.dumps(usuario.para_json(), ensure_ascii=False))
Exemplo 2: Configuração de Aplicação
Dataclasses funcionam muito bem para centralizar configurações:
from dataclasses import dataclass
import os
@dataclass
class Config:
debug: bool = False
host: str = "localhost"
porta: int = 8000
banco_dados: str = "sqlite:///app.db"
def __post_init__(self):
# Sobrescreve com variáveis de ambiente se existirem
self.debug = os.getenv("DEBUG", "false").lower() == "true"
self.host = os.getenv("HOST", self.host)
self.porta = int(os.getenv("PORT", self.porta))
config = Config()
print(config)
Conclusão
Ao longo deste artigo, você aprendeu que dataclasses eliminam boilerplate significativo ao automatizar __init__, __repr__, __eq__ e outros métodos especiais, permitindo que você se concentre na lógica de negócio. A função fields() oferece introspecção poderosa, enquanto default_factory resolve o problema clássico de valores padrão mutáveis em Python. O __post_init__ é sua ferramenta para validação e transformação de dados após a inicialização automática, tornando suas classes expressivas e seguras sem complexidade desnecessária.