Dataclass em Python: O que É e Como Funciona | Python Brasil
Aprenda dataclass em Python: field(), __post_init__, slots, herança, kw_only e comparação com attrs, Pydantic e namedtuple. Guia completo com exemplos.
O que é Dataclass?
Uma dataclass é uma forma simplificada de criar classes em Python que servem principalmente para armazenar dados. Introduzida no Python 3.7 (PEP 557), ela gera automaticamente métodos como __init__, __repr__ e __eq__, reduzindo drasticamente o boilerplate.
O resultado é código mais limpo, mais legível e menos propenso a erros de digitação nos métodos repetitivos.
Antes e depois: a diferença em prática
# SEM dataclass (muito código repetitivo)
class ProdutoAntigo:
def __init__(self, nome, preco, quantidade):
self.nome = nome
self.preco = preco
self.quantidade = quantidade
def __repr__(self):
return f"Produto(nome='{self.nome}', preco={self.preco}, quantidade={self.quantidade})"
def __eq__(self, other):
if not isinstance(other, ProdutoAntigo):
return NotImplemented
return (self.nome == other.nome and
self.preco == other.preco and
self.quantidade == other.quantidade)
# COM dataclass (limpo e conciso)
from dataclasses import dataclass
@dataclass
class Produto:
nome: str
preco: float
quantidade: int = 0
p1 = Produto("Notebook", 3500.0, 5)
p2 = Produto("Notebook", 3500.0, 5)
print(p1) # Produto(nome='Notebook', preco=3500.0, quantidade=5)
print(p1 == p2) # True — __eq__ gerado automaticamente
A função field() em detalhe
field() controla o comportamento individual de cada campo:
from dataclasses import dataclass, field
from typing import Optional
import datetime
@dataclass
class Pedido:
# Campo normal com anotação
produto: str
# default_factory para valores mutáveis (NUNCA use lista direta como default)
itens: list[str] = field(default_factory=list)
# repr=False oculta o campo no __repr__
senha_interna: str = field(default="", repr=False)
# compare=False exclui do __eq__ e __hash__
timestamp: datetime.datetime = field(
default_factory=datetime.datetime.now,
compare=False,
repr=False
)
# hash=False exclui do __hash__ individualmente
metadata: dict = field(default_factory=dict, hash=False, compare=False)
# init=False — campo não aparece no __init__, definido no __post_init__
_id: Optional[int] = field(default=None, init=False, repr=False)
def __post_init__(self):
self._id = id(self) # Gera ID baseado no endereço de memória
p = Pedido("Notebook", itens=["carregador", "mouse"])
print(p)
# Pedido(produto='Notebook', itens=['carregador', 'mouse'])
print(p._id) # Algum inteiro grande
post_init: inicialização adicional
__post_init__ é chamado automaticamente pelo __init__ gerado após a atribuição de todos os campos. É o lugar para validações, transformações e campos derivados:
from dataclasses import dataclass, field
import re
@dataclass
class Usuario:
nome: str
email: str
idade: int
nome_normalizado: str = field(init=False, repr=False)
maior_de_idade: bool = field(init=False)
def __post_init__(self):
# Validações
if self.idade < 0:
raise ValueError(f"Idade inválida: {self.idade}")
if not re.match(r'^[\w.+-]+@[\w-]+\.[a-z]{2,}$', self.email):
raise ValueError(f"Email inválido: {self.email}")
# Campos derivados
self.nome_normalizado = self.nome.strip().title()
self.maior_de_idade = self.idade >= 18
u = Usuario(" ana silva ", "ana@email.com", 25)
print(u.nome_normalizado) # Ana Silva
print(u.maior_de_idade) # True
Dataclasses imutáveis (frozen)
Com frozen=True, os campos tornam-se somente leitura e a instância pode ser usada como chave de dicionário:
@dataclass(frozen=True)
class Coordenada:
latitude: float
longitude: float
def distancia_para(self, outra: 'Coordenada') -> float:
import math
dlat = math.radians(outra.latitude - self.latitude)
dlon = math.radians(outra.longitude - self.longitude)
a = (math.sin(dlat/2)**2 +
math.cos(math.radians(self.latitude)) *
math.cos(math.radians(outra.latitude)) *
math.sin(dlon/2)**2)
return 6371 * 2 * math.asin(math.sqrt(a))
sp = Coordenada(-23.5505, -46.6333)
rj = Coordenada(-22.9068, -43.1729)
print(sp.distancia_para(rj)) # ~357 km
# sp.latitude = 0 # FrozenInstanceError!
# frozen=True torna a instância hashable
pontos = {sp, rj}
cache = {sp: "São Paulo", rj: "Rio de Janeiro"}
slots=True: performance melhorada (Python 3.10+)
Python 3.10 introduziu slots=True diretamente no decorator, eliminando o __dict__ e melhorando acesso e consumo de memória:
import sys
@dataclass
class PontoSemSlots:
x: float
y: float
@dataclass(slots=True)
class PontoComSlots:
x: float
y: float
sem = PontoSemSlots(1.0, 2.0)
com = PontoComSlots(1.0, 2.0)
print(sys.getsizeof(sem)) # ~56 bytes (mais o __dict__)
print(sys.getsizeof(com)) # ~48 bytes (sem __dict__)
# Com slots=True, tentativas de adicionar atributos dinâmicos falham
# com.z = 3 # AttributeError
kw_only e match_args (Python 3.10+)
kw_only=True força todos os campos a serem passados como argumentos nomeados, eliminando ambiguidade:
@dataclass(kw_only=True)
class Configuracao:
host: str
porta: int = 5432
timeout: float = 30.0
debug: bool = False
# Todos os argumentos devem ser nomeados
cfg = Configuracao(host="localhost", debug=True)
# Configuracao("localhost") # TypeError — não aceita posicional
# match_args controla o structural pattern matching (Python 3.10+)
@dataclass(match_args=True) # Padrão: True
class Ponto:
x: float
y: float
ponto = Ponto(1.0, 2.0)
match ponto:
case Ponto(x=0, y=0):
print("Origem")
case Ponto(x=x, y=0):
print(f"Eixo X: {x}")
case Ponto(x=0, y=y):
print(f"Eixo Y: {y}")
case Ponto(x=x, y=y):
print(f"Ponto: ({x}, {y})")
Dataclass com herança
Herança funciona com dataclasses, mas há uma regra importante: subclasses não podem ter campos com valor padrão antes de campos sem valor padrão da classe pai:
@dataclass
class Entidade:
id: int
criado_em: str = field(default_factory=lambda: "2024-01-01")
@dataclass
class Produto(Entidade):
nome: str = ""
preco: float = 0.0
def __post_init__(self):
if self.preco < 0:
raise ValueError("Preço não pode ser negativo")
@dataclass
class ProdutoDigital(Produto):
url_download: str = ""
tamanho_mb: float = 0.0
pd = ProdutoDigital(id=1, nome="Ebook Python", preco=29.90, url_download="http://...")
print(pd)
Dataclass vs namedtuple vs attrs vs Pydantic
Cada solução tem seu uso ideal:
from collections import namedtuple
from typing import NamedTuple
# namedtuple: imutável, leve, indexável — ideal para dados simples
Cor = namedtuple('Cor', ['r', 'g', 'b'])
vermelho = Cor(255, 0, 0)
print(vermelho[0]) # 255 — indexável
print(vermelho.r) # 255 — por nome
# NamedTuple com tipos (mais moderno)
class Ponto(NamedTuple):
x: float
y: float
z: float = 0.0
# dataclass: mutável por padrão, mais flexível, métodos opcionais
@dataclass
class ProdutoDataclass:
nome: str
preco: float
def aplicar_desconto(self, pct):
self.preco *= (1 - pct / 100)
# attrs: mais recursos que dataclass, validators integrados
# pip install attrs
# import attr
# @attr.s(auto_attribs=True)
# class Produto:
# nome: str = attr.ib(validator=attr.validators.instance_of(str))
# preco: float = attr.ib(validator=attr.validators.gt(0))
# Pydantic: validação automática, conversão de tipos, ideal para APIs
# from pydantic import BaseModel
# class ProdutoPydantic(BaseModel):
# nome: str
# preco: float # Converte "10.5" -> 10.5 automaticamente
Quando usar cada um:
namedtuple/NamedTuple: dados imutáveis simples, sem métodos, memória eficientedataclass: estruturas de dados com possível mutabilidade e métodos auxiliaresattrs: quando quiser validators e conversores embutidos, sem dependências de runtimePydantic: validação de dados externos (APIs, formulários, arquivos de configuração)
Quando NÃO usar dataclass
Evite dataclasses quando: a classe tem lógica de negócio complexa que vai além de armazenar dados (prefira classe comum); você precisa de validação robusta de dados externos (prefira Pydantic); a imutabilidade e eficiência são críticas (prefira NamedTuple ou frozen=True com slots=True); você está em Python menor que 3.7.
# NÃO use dataclass para isso — lógica de negócio complexa
@dataclass # Errado — este é um Service/Manager, não um dado
class GerenciadorPedidos:
repositorio: object
servico_email: object
servico_pagamento: object
def processar_pedido(self, pedido_id):
# lógica complexa
pass
# USE dataclass para isso — estrutura de dados
@dataclass
class Pedido:
id: int
cliente: str
itens: list[str]
total: float
status: str = "pendente"
Considerações de performance
import timeit
from dataclasses import dataclass
@dataclass
class Normal:
x: float
y: float
@dataclass(slots=True)
class ComSlots:
x: float
y: float
# Acesso de atributos com slots é ~10-15% mais rápido
# Criação de instâncias é similar
# Consumo de memória com slots é menor
Termos Relacionados
- Classe - Base da programação orientada a objetos
- Type Hints - Obrigatórios em dataclasses
- Pydantic - Alternativa com validação automática
- Tupla - Alternativa imutável mais simples