dataclasses: classes de dados concisas em Python
1. Visão geral
Ao escrever classes apenas para conter dados (como Order, Customer, Point…), você geralmente precisa escrever __init__, __repr__, __eq__ manualmente — é tudo código repetitivo. O módulo dataclasses (disponível desde Python 3.7) ajuda você a gerar esses métodos automaticamente com apenas um decorator. Após ler este artigo, você saberá como usar @dataclass para escrever classes de dados concisas, validar dados com __post_init__, tornar uma classe imutável e converter para dict/JSON.
2. O que é um dataclass básico
@dataclass é um decorator que lê as type hints na classe e depois gera automaticamente __init__, __repr__, __eq__ com base nesses campos.
from dataclasses import dataclass
@dataclass
class Customer:
name: str
email: str
age: int = 0
@dataclassexamina os atributos com type hints (name,email,age) e os trata como campos.age: int = 0tem um valor padrão, assim como parâmetros padrão em funções.- Você não precisa escrever
__init__— ele é gerado automaticamente na ordem correta dos campos.
c1 = Customer("An", "an@example.com")
print(c1)
Customer(name='An', email='an@example.com', age=0)
__repr__ é gerado automaticamente para exibir todos os nomes de campo e valores, muito útil para debug — as classes geralmente não têm isso por padrão.
3. Comparando com classes normais e namedtuple
Escrever a classe equivalente manualmente seria muito mais longo:
class CustomerPlain:
def __init__(self, name, email, age=0):
self.name = name
self.email = email
self.age = age
def __repr__(self):
return f"CustomerPlain(name={self.name!r}, email={self.email!r}, age={self.age!r})"
def __eq__(self, other):
if not isinstance(other, CustomerPlain):
return NotImplemented
return (self.name, self.email, self.age) == (other.name, other.email, other.age)
@dataclass economiza você de escrever toda essa parte. Em comparação com namedtuple, dataclass diferencia em:
- Os objetos são mutáveis por padrão (você pode modificar
c1.age = 20), namedtuple não é. - Suporta type hints explícitas, fácil de integrar com type checkers como
mypy. - Suporta
__post_init__, valores padrão complexos, herança — coisas quenamedtuplenão faz bem.
Nota: Se você só precisa de uma tupla imutável leve sem métodos,
namedtupleoutyping.NamedTupleainda é uma opção mais concisa.
4. field() e valores padrão mutáveis
Um erro muito comum: usar list/dict como valor padrão diretamente.
from dataclasses import dataclass, field
@dataclass
class Order:
order_id: str
items: list[str] = field(default_factory=list)
- Se você escrever
items: list[str] = [], Python vai lançar um erroValueError: mutable default ... is not allowed— porque a lista padrão seria compartilhada entre todas as instâncias, levando a bugs difíceis de rastrear. field(default_factory=list)instrui o dataclass a chamarlist()nova para cada instância.
o1 = Order("A1")
o2 = Order("A2")
o1.items.append("Sách")
print(o1.items, o2.items)
['Sách'] []
Os dois objetos têm items independentes, exatamente como esperado.
5. Validando dados com __post_init__
O __init__ gerado por dataclass não permite inserir lógica de validação diretamente — mas há um hook __post_init__ que executa logo após __init__ terminar.
from dataclasses import dataclass
@dataclass
class PaymentRequest:
amount: float
currency: str = "VND"
def __post_init__(self):
if self.amount <= 0:
raise ValueError("amount deve ser maior que 0")
self.currency = self.currency.upper()
__post_init__é chamado automaticamente após todos os campos serem atribuídos.- Aqui validamos
amounte normalizamoscurrencypara maiúsculas.
try:
PaymentRequest(amount=-100)
except ValueError as e:
print(e)
amount deve ser maior que 0
6. Classe imutável com frozen=True
Se você quer que o objeto não possa ser modificado após a criação (como um record em Java), use frozen=True.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
frozen=Truebloqueia__setattr__— qualquer atribuição após inicialização vai lançar um erro.- Graças à imutabilidade, objetos
frozenpodem ser usados como chave emdictou elemento emset(porque dataclass gera__hash__automaticamente quandofrozen=Trueeeq=True).
p = Point(1.0, 2.0)
try:
p.x = 5.0
except Exception as e:
print(type(e).__name__, e)
FrozenInstanceError cannot assign to field 'x'
Nota:
frozen=Truesó bloqueia atribuições de atributos no nível 1. Se um campo é uma list/dict (mutável), o conteúdo dentro ainda pode ser modificado. Para verdadeira imutabilidade, usetupleou tipos imutáveis para esse campo.
7. Herança em dataclass
Dataclass suporta herança normal como qualquer classe Python, campos da classe filha vêm após os campos da classe pai.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
@dataclass
class Employee(Person):
salary: float = 0.0
Employeeherdaname,agedePersone depois adicionasalary.- A ordem dos parâmetros no
__init__gerado é:name, age, salary— mantém a ordem de declaração.
e = Employee("Bình", 30, salary=15_000_000)
print(e)
Employee(name='Bình', age=30, salary=15000000.0)
Nota: Campos sem padrão devem vir antes de campos com padrão em toda a cadeia de herança. Se a classe pai tem um campo com padrão e a classe filha adiciona um campo sem padrão, Python lançará
TypeErrorao definir a classe.
8. Comparação e ordenação com order=True
Por padrão, dataclass só gera __eq__. Para comparações <, >, <=, use order=True.
from dataclasses import dataclass, field
@dataclass(order=True)
class Product:
price: float
name: str = field(compare=False)
order=Truegera__lt__,__le__,__gt__,__ge__, comparando na ordem dos campos (como comparação de tuplas).field(compare=False)excluinameda comparação — compara apenas porprice.
products = [Product(50_000, "Bút"), Product(20_000, "Vở")]
print(sorted(products))
[Product(price=20000, name='Vở'), Product(price=50000, name='Bút')]
sorted() usa __lt__ gerado pelo dataclass, sem precisar escrever key=lambda p: p.price manualmente.
9. Convertendo para dict e tuple
dataclasses fornece as funções utilitárias asdict e astuple para serializar objetos.
from dataclasses import dataclass, asdict, astuple
@dataclass
class Address:
city: str
country: str
@dataclass
class Customer2:
name: str
address: Address
cust = Customer2("Chi", Address("Hà Nội", "Việt Nam"))
print(asdict(cust))
print(astuple(cust))
{'name': 'Chi', 'address': {'city': 'Hà Nội', 'country': 'Việt Nam'}}
('Chi', ('Hà Nội', 'Việt Nam'))
asdictconverte recursivamente até dataclass aninhado (Address) em dict aninhado — muito útil para retornar JSON via API.astupleé similar mas retorna tuplas aninhadas.
Nota:
asdict/astuplefaz deepcopy de todos os dados. Com objetos grandes ou contendo campos que não podem ser copiados (file handle, connection…), chamar essa função será lento ou falhará.
10. Escrevendo testes para dataclass
Como __eq__ é gerado automaticamente baseado nos valores dos campos, comparar objetos em testes é bem natural com pytest.
import pytest
from dataclasses import FrozenInstanceError
def test_customer_equality():
c1 = Customer("An", "an@example.com", 20)
c2 = Customer("An", "an@example.com", 20)
assert c1 == c2
def test_payment_request_rejects_non_positive_amount():
with pytest.raises(ValueError):
PaymentRequest(amount=0)
def test_point_is_frozen():
p = Point(1.0, 2.0)
with pytest.raises(FrozenInstanceError):
p.x = 10.0
test_customer_equalityverifica que dois objetos são instâncias diferentes mas com os mesmos valores de campo ainda são==— correto graças ao__eq__gerado automaticamente.test_payment_request_rejects_non_positive_amountverifica que__post_init__valida corretamente.test_point_is_frozenconfirma quefrozen=Truebloqueia atribuições de atributos.
11. Erros comuns e quando não usar
- Esquecer
default_factorypara padrões mutáveis: usar= []ou= {}diretamente vai dar erro ao declarar a classe, deve usarfield(default_factory=...). - Misturar campos com e sem padrão na ordem errada na herança: causa
TypeErrorao carregar o módulo, difícil de debugar se as classes estão espalhadas em vários arquivos. - Pensar que
frozen=Trueé deep immutable: campos do tipo list/dict dentro ainda podem ter seu conteúdo modificado, apenas não é possível reatribuir o atributo. - Usar dataclass para lógica de negócio complexa: se a classe tem muitos métodos de processamento, relacionamentos complexos com outros objetos, dataclass só ajuda com inicialização/comparação — ainda precisa escrever a lógica manualmente, não abuse para “fingir” que a classe é mais simples do que realmente é.
- Precisa de compatibilidade com Python < 3.7:
dataclassesnão é nativo, precisa instalar o packagedataclassesdo PyPI (backport) ou usarattrs.
Nota: Se precisar de validação complexa, serialização customizada ou integração profunda com JSON schema, bibliotecas como
pydanticcostumam ser mais poderosas quedataclassespuro — vale considerar em projetos maiores.
12. Conclusão
dataclasses ajuda você a eliminar boa parte do boilerplate ao escrever classes de dados: gera __init__, __repr__, __eq__ automaticamente, suporta validação via __post_init__, imutabilidade via frozen=True e serialização fácil com asdict/astuple. Para classes de dados simples a médias, é uma escolha bem mais concisa e legível que escrever manualmente. Quando as necessidades de validação ou serialização são mais complexas, você pode considerar pydantic ou attrs como alternativas.