Dataclasses em Python: um guia completo do decorator @dataclass
Updated on
Escrever classes em Python frequentemente envolve código repetitivo (boilerplate). Você define __init__ para inicializar atributos, __repr__ para uma saída legível, __eq__ para comparações e, às vezes, __hash__ para permitir hashability. Essa implementação manual fica cansativa em classes que apenas armazenam dados, especialmente ao lidar com objetos de configuração, respostas de API ou registros de banco de dados.
O Python 3.7 introduziu dataclasses por meio da PEP 557, automatizando esse boilerplate enquanto mantém a flexibilidade das classes comuns. O decorator @dataclass gera métodos especiais automaticamente com base em anotações de tipo, reduzindo dezenas de linhas de código para apenas algumas. Este guia mostra como aproveitar dataclasses para obter um código Python mais limpo e fácil de manter.
Por que dataclasses existem: resolvendo o problema do boilerplate
Classes tradicionais em Python exigem definições explícitas de métodos para operações comuns. Considere esta classe padrão para armazenar dados de usuário:
class User:
def __init__(self, name, email, age):
self.name = name
self.email = email
self.age = age
def __repr__(self):
return f"User(name={self.name!r}, email={self.email!r}, age={self.age!r})"
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return (self.name, self.email, self.age) == (other.name, other.email, other.age)Com dataclasses, isso se reduz a:
from dataclasses import dataclass
@dataclass
class User:
name: str
email: str
age: intO decorator gera __init__, __repr__ e __eq__ automaticamente a partir das anotações de tipo. Isso elimina mais de 15 linhas de boilerplate mantendo a mesma funcionalidade.
Sintaxe básica de @dataclass
A dataclass mais simples requer apenas anotações de tipo para os campos:
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
quantity: int
product = Product("Laptop", 999.99, 5)
print(product) # Product(name='Laptop', price=999.99, quantity=5)
product2 = Product("Laptop", 999.99, 5)
print(product == product2) # TrueO decorator aceita parâmetros para personalizar o comportamento:
@dataclass(
init=True, # Gera __init__ (padrão: True)
repr=True, # Gera __repr__ (padrão: True)
eq=True, # Gera __eq__ (padrão: True)
order=False, # Gera métodos de comparação (padrão: False)
frozen=False, # Torna instâncias imutáveis (padrão: False)
unsafe_hash=False # Gera __hash__ (padrão: False)
)
class Config:
host: str
port: intTipos de campos e valores padrão
Dataclasses suportam valores padrão para campos. Campos sem padrão devem aparecer antes de campos com padrão:
from dataclasses import dataclass
@dataclass
class Server:
host: str
port: int = 8080
protocol: str = "http"
server1 = Server("localhost")
print(server1) # Server(host='localhost', port=8080, protocol='http')
server2 = Server("api.example.com", 443, "https")
print(server2) # Server(host='api.example.com', port=443, protocol='https')Para valores padrão mutáveis como listas ou dicionários, use default_factory para evitar referências compartilhadas:
from dataclasses import dataclass, field
# WRONG - all instances share the same list
@dataclass
class WrongConfig:
tags: list = [] # Raises error in Python 3.10+
# CORRECT - each instance gets a new list
@dataclass
class CorrectConfig:
tags: list = field(default_factory=list)
metadata: dict = field(default_factory=dict)
config1 = CorrectConfig()
config2 = CorrectConfig()
config1.tags.append("production")
print(config1.tags) # ['production']
print(config2.tags) # [] - separate listA função field(): configuração avançada de campos
A função field() oferece controle granular sobre campos individuais:
from dataclasses import dataclass, field
from typing import List
@dataclass
class Employee:
name: str
employee_id: int
salary: float = field(repr=False) # Hide salary in repr
skills: List[str] = field(default_factory=list)
_internal_id: str = field(init=False, repr=False) # Not in __init__
performance_score: float = field(default=0.0, compare=False) # Exclude from comparison
def __post_init__(self):
self._internal_id = f"EMP_{self.employee_id:06d}"
emp = Employee("Alice", 12345, 85000.0, ["Python", "SQL"])
print(emp) # Employee(name='Alice', employee_id=12345, skills=['Python', 'SQL'], performance_score=0.0)
print(emp._internal_id) # EMP_012345Principais parâmetros de field():
| Parameter | Type | Description |
|---|---|---|
default | Any | Valor padrão do campo |
default_factory | Callable | Função sem argumentos que retorna o valor padrão |
init | bool | Inclui o campo em __init__ (padrão: True) |
repr | bool | Inclui o campo em __repr__ (padrão: True) |
compare | bool | Inclui o campo nos métodos de comparação (padrão: True) |
hash | bool | Inclui o campo em __hash__ (padrão: None) |
metadata | dict | Metadados arbitrários (não usados pelo módulo dataclasses) |
kw_only | bool | Torna o campo somente por palavra-chave (Python 3.10+) |
O parâmetro metadata armazena informações arbitrárias acessíveis via fields():
from dataclasses import dataclass, field, fields
@dataclass
class APIRequest:
endpoint: str = field(metadata={"description": "API endpoint path"})
method: str = field(default="GET", metadata={"choices": ["GET", "POST", "PUT", "DELETE"]})
for f in fields(APIRequest):
print(f"{f.name}: {f.metadata}")
# endpoint: {'description': 'API endpoint path'}
# method: {'choices': ['GET', 'POST', 'PUT', 'DELETE']}Anotações de tipo com dataclasses
Dataclasses dependem de anotações de tipo, mas não as aplicam em runtime. Use o módulo typing para tipos complexos:
from dataclasses import dataclass
from typing import List, Dict, Optional, Union, Tuple
from datetime import datetime
@dataclass
class DataAnalysisJob:
job_id: str
dataset_path: str
columns: List[str]
filters: Dict[str, Union[str, int, float]]
output_format: str = "csv"
created_at: datetime = field(default_factory=datetime.now)
completed_at: Optional[datetime] = None
error_message: Optional[str] = None
results: Optional[Dict[str, Tuple[float, float]]] = None
job = DataAnalysisJob(
job_id="job_001",
dataset_path="/data/sales.csv",
columns=["date", "revenue", "region"],
filters={"year": 2026, "region": "US"}
)Para verificação de tipos em runtime, integre com bibliotecas como pydantic ou use validação em __post_init__.
frozen=True: criando dataclasses imutáveis
Defina frozen=True para tornar instâncias imutáveis após a criação, similar a named tuples:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
def distance_from_origin(self):
return (self.x**2 + self.y**2) ** 0.5
point = Point(3.0, 4.0)
print(point.distance_from_origin()) # 5.0
# Attempting to modify raises FrozenInstanceError
try:
point.x = 5.0
except AttributeError as e:
print(f"Error: {e}") # Error: cannot assign to field 'x'Dataclasses congeladas (frozen) são hashable por padrão se todos os campos forem hashable, permitindo uso em sets e como chaves de dicionário:
@dataclass(frozen=True)
class Coordinate:
latitude: float
longitude: float
locations = {
Coordinate(40.7128, -74.0060): "New York",
Coordinate(51.5074, -0.1278): "London"
}
print(locations[Coordinate(40.7128, -74.0060)]) # New YorkMétodo post_init: validação e campos computados
O método __post_init__ é executado após __init__, permitindo validação e inicialização de campos computados:
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class BankAccount:
account_number: str
balance: float
created_at: datetime = field(default_factory=datetime.now)
account_type: str = field(init=False)
def __post_init__(self):
if self.balance < 0:
raise ValueError("Initial balance cannot be negative")
# Compute account_type based on balance
if self.balance >= 100000:
self.account_type = "Premium"
elif self.balance >= 10000:
self.account_type = "Gold"
else:
self.account_type = "Standard"
account = BankAccount("ACC123456", 50000.0)
print(account.account_type) # GoldPara campos com init=False que dependem de outros campos, use __post_init__:
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False)
perimeter: float = field(init=False)
def __post_init__(self):
self.area = self.width * self.height
self.perimeter = 2 * (self.width + self.height)
rect = Rectangle(5.0, 3.0)
print(f"Area: {rect.area}, Perimeter: {rect.perimeter}") # Area: 15.0, Perimeter: 16.0Herança com dataclasses
Dataclasses suportam herança com mesclagem automática de campos:
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str
is_good_boy: bool = True
dog = Dog("Buddy", 5, "Golden Retriever")
print(dog) # Dog(name='Buddy', age=5, breed='Golden Retriever', is_good_boy=True)Subclasses herdam os campos da classe pai e podem adicionar novos. Campos sem valores padrão não podem vir depois de campos com valores padrão ao longo da herança:
from dataclasses import dataclass
@dataclass
class BaseConfig:
environment: str = "production"
# ERROR: Non-default field 'api_key' cannot follow default field 'environment'
# @dataclass
# class APIConfig(BaseConfig):
# api_key: str
# CORRECT: Use default or rearrange fields
@dataclass
class APIConfig(BaseConfig):
api_key: str = "" # Provide default
timeout: int = 30O Python 3.10+ introduziu kw_only para resolver isso:
from dataclasses import dataclass
@dataclass
class BaseConfig:
environment: str = "production"
@dataclass(kw_only=True)
class APIConfig(BaseConfig):
api_key: str # Must be passed as keyword argument
timeout: int = 30
config = APIConfig(api_key="secret_key_123") # OK
# config = APIConfig("secret_key_123") # TypeErrorslots=True: eficiência de memória (Python 3.10+)
O Python 3.10 adicionou slots=True para definir __slots__, reduzindo overhead de memória:
from dataclasses import dataclass
import sys
@dataclass
class RegularUser:
username: str
email: str
age: int
@dataclass(slots=True)
class SlottedUser:
username: str
email: str
age: int
regular = RegularUser("john", "john@example.com", 30)
slotted = SlottedUser("jane", "jane@example.com", 28)
print(f"Regular: {sys.getsizeof(regular.__dict__)} bytes") # ~104 bytes
print(f"Slotted: {sys.getsizeof(slotted)} bytes") # ~64 bytesDataclasses com slots oferecem economia de memória de 30–40% e acesso a atributos mais rápido, mas abrem mão da adição dinâmica de atributos:
regular.new_attribute = "allowed" # OK
# slotted.new_attribute = "error" # AttributeErrorkw_only=True: campos somente por keyword (Python 3.10+)
Força todos os campos a serem somente por keyword para uma instanciação mais clara:
from dataclasses import dataclass
@dataclass(kw_only=True)
class DatabaseConnection:
host: str
port: int
username: str
password: str
database: str = "default"
# Must use keyword arguments
conn = DatabaseConnection(
host="localhost",
port=5432,
username="admin",
password="secret"
)
# Positional arguments raise TypeError
# conn = DatabaseConnection("localhost", 5432, "admin", "secret")Combine kw_only com controle por campo:
from dataclasses import dataclass, field
@dataclass
class MixedArgs:
required_positional: str
optional_positional: int = 0
required_keyword: str = field(kw_only=True)
optional_keyword: bool = field(default=False, kw_only=True)
obj = MixedArgs("value", 10, required_keyword="kw_value")Comparação: dataclass vs alternativas
| Feature | dataclass | namedtuple | TypedDict | Pydantic | attrs |
|---|---|---|---|---|---|
| Mutability | Mutável (padrão) | Imutável | N/A (subclasse de dict) | Mutável | Configurável |
| Type validation | Apenas anotações | Não | Apenas anotações | Validação em runtime | Validação em runtime |
| Default values | Sim | Sim | Não | Sim | Sim |
| Methods | Suporte completo a classes | Limitado | Não | Suporte completo a classes | Suporte completo a classes |
| Inheritance | Sim | Não | Limitado | Sim | Sim |
| Memory overhead | Moderado | Baixo | Baixo | Maior | Moderado |
| Slots support | Sim (3.10+) | Não | Não | Sim | Sim |
| Performance | Rápido | Mais rápido | Rápido | Mais lento (validação) | Rápido |
| Built-in | Sim (3.7+) | Sim | Sim (3.8+) | Não | Não |
Escolha dataclasses para:
- Projetos Python padrão sem dependências
- Contêineres de dados simples com type hints
- Quando você precisa de flexibilidade entre mutável/congelado (frozen)
- Hierarquias com herança
Escolha Pydantic para:
- Validação de request/response de APIs
- Gerenciamento de configuração com validação estrita
- Geração de JSON schema
Escolha namedtuple para:
- Contêineres imutáveis leves
- Máxima eficiência de memória
- Compatibilidade com Python < 3.7
Convertendo para/de dicionários
Dataclasses fornecem asdict() e astuple() para serialização:
from dataclasses import dataclass, asdict, astuple
@dataclass
class Config:
host: str
port: int
ssl_enabled: bool = True
config = Config("api.example.com", 443)
# Convert to dictionary
config_dict = asdict(config)
print(config_dict) # {'host': 'api.example.com', 'port': 443, 'ssl_enabled': True}
# Convert to tuple
config_tuple = astuple(config)
print(config_tuple) # ('api.example.com', 443, True)Para dataclasses aninhadas:
from dataclasses import dataclass, asdict
@dataclass
class Address:
street: str
city: str
zipcode: str
@dataclass
class Person:
name: str
address: Address
person = Person("Alice", Address("123 Main St", "Springfield", "12345"))
person_dict = asdict(person)
print(person_dict)
# {'name': 'Alice', 'address': {'street': '123 Main St', 'city': 'Springfield', 'zipcode': '12345'}}Dataclasses com serialização JSON
Dataclasses não oferecem suporte nativo a serialização JSON, mas a integração é direta:
import json
from dataclasses import dataclass, asdict
from datetime import datetime
@dataclass
class Event:
name: str
timestamp: datetime
attendees: int
def to_json(self):
data = asdict(self)
# Custom serialization for datetime
data['timestamp'] = self.timestamp.isoformat()
return json.dumps(data)
@classmethod
def from_json(cls, json_str):
data = json.loads(json_str)
data['timestamp'] = datetime.fromisoformat(data['timestamp'])
return cls(**data)
event = Event("Python Conference", datetime.now(), 500)
json_str = event.to_json()
print(json_str)
restored = Event.from_json(json_str)
print(restored)Para cenários mais complexos, use a biblioteca dataclasses-json ou Pydantic.
Padrões do mundo real
Objetos de configuração
from dataclasses import dataclass, field
from typing import List
@dataclass
class AppConfig:
app_name: str
version: str
debug: bool = False
allowed_hosts: List[str] = field(default_factory=lambda: ["localhost"])
database_url: str = "sqlite:///app.db"
cache_timeout: int = 300
def __post_init__(self):
if self.debug:
print(f"Running {self.app_name} v{self.version} in DEBUG mode")
config = AppConfig("DataAnalyzer", "2.1.0", debug=True)Modelos de resposta de API
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
@dataclass
class APIResponse:
status: str
data: Optional[List[dict]] = None
error_message: Optional[str] = None
timestamp: datetime = field(default_factory=datetime.now)
@property
def is_success(self):
return self.status == "success"
response = APIResponse("success", data=[{"id": 1, "name": "Dataset A"}])
print(response.is_success) # TrueRegistros de banco de dados com integração PyGWalker
from dataclasses import dataclass, asdict
from typing import List
import pandas as pd
@dataclass
class SalesRecord:
date: str
product: str
revenue: float
region: str
quantity: int
# Create sample data
records = [
SalesRecord("2026-01-01", "Laptop", 1299.99, "US", 5),
SalesRecord("2026-01-02", "Mouse", 29.99, "EU", 50),
SalesRecord("2026-01-03", "Keyboard", 89.99, "US", 20),
]
# Convert to DataFrame for visualization with PyGWalker
df = pd.DataFrame([asdict(r) for r in records])
# Use PyGWalker for interactive data exploration
# import pygwalker as pyg
# walker = pyg.walk(df)
# This creates a Tableau-like interface to visualize your dataclass-based dataDataclasses se destacam ao estruturar dados antes da visualização. PyGWalker converte DataFrames em interfaces visuais interativas, tornando fluxos de análise baseados em dataclass mais fluidos.
Benchmarks de performance vs classes comuns
import timeit
from dataclasses import dataclass
# Regular class
class RegularClass:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return f"RegularClass(x={self.x}, y={self.y}, z={self.z})"
def __eq__(self, other):
return (self.x, self.y, self.z) == (other.x, other.y, other.z)
@dataclass
class DataClass:
x: int
y: int
z: int
# Benchmark instantiation
regular_time = timeit.timeit(lambda: RegularClass(1, 2, 3), number=1000000)
dataclass_time = timeit.timeit(lambda: DataClass(1, 2, 3), number=1000000)
print(f"Regular class: {regular_time:.4f}s")
print(f"Dataclass: {dataclass_time:.4f}s")
# Dataclasses are typically 5-10% slower due to decorator overhead
# but provide significantly cleaner codeCom slots=True (Python 3.10+), dataclasses igualam ou superam a performance de classes comuns, além de reduzir o uso de memória em 30–40%.
Padrões avançados: ordenação customizada de campos
from dataclasses import dataclass, field
def sort_by_priority(items):
return sorted(items, key=lambda x: x.priority, reverse=True)
@dataclass(order=True)
class Task:
priority: int
name: str = field(compare=False)
description: str = field(compare=False)
tasks = [
Task(3, "Review PR", "Code review for feature X"),
Task(1, "Write docs", "Documentation update"),
Task(5, "Fix bug", "Critical production issue"),
]
sorted_tasks = sorted(tasks)
for task in sorted_tasks:
print(f"Priority {task.priority}: {task.name}")
# Priority 1: Write docs
# Priority 3: Review PR
# Priority 5: Fix bugBoas práticas e armadilhas (gotchas)
- Sempre use
default_factorypara padrões mutáveis: nunca atribua[]ou{}diretamente - Type hints são obrigatórios: dataclasses dependem de anotações, não de valores
- A ordem dos campos importa: campos sem padrão antes dos campos com padrão
frozen=Truepara dados imutáveis: útil para objetos hashable e thread safety- Use
__post_init__com moderação: lógica demais reduz a simplicidade das dataclasses - Considere
slots=Truepara grandes volumes de dados: economia significativa de memória em Python 3.10+ - Valide em
__post_init__: dataclasses não impõem tipos em runtime
Perguntas frequentes (FAQ)
Conclusão
Dataclasses em Python eliminam boilerplate enquanto preservam todo o poder das classes. O decorator @dataclass gera automaticamente métodos de inicialização, representação e comparação, reduzindo o tempo de desenvolvimento e o custo de manutenção. De objetos de configuração a modelos de API e registros de banco de dados, dataclasses fornecem uma abordagem limpa e com type annotations para classes focadas em armazenar dados.
Entre as principais vantagens estão a geração automática de métodos, o comportamento customizável de campos via field(), a imutabilidade com frozen=True, a validação por meio de __post_init__ e a eficiência de memória com slots=True. Embora alternativas como namedtuples e Pydantic sejam ideais para casos específicos, dataclasses oferecem um equilíbrio excelente entre simplicidade e recursos para a maioria dos projetos Python.
Para fluxos de trabalho de análise de dados, combinar dataclasses com ferramentas como PyGWalker cria pipelines poderosos em que modelos de dados estruturados alimentam diretamente visualizações interativas, agilizando tudo — da ingestão de dados à geração de insights.