Skip to content

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: int

O 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)  # True

O 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: int

Tipos 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 list

A 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_012345

Principais parâmetros de field():

ParameterTypeDescription
defaultAnyValor padrão do campo
default_factoryCallableFunção sem argumentos que retorna o valor padrão
initboolInclui o campo em __init__ (padrão: True)
reprboolInclui o campo em __repr__ (padrão: True)
compareboolInclui o campo nos métodos de comparação (padrão: True)
hashboolInclui o campo em __hash__ (padrão: None)
metadatadictMetadados arbitrários (não usados pelo módulo dataclasses)
kw_onlyboolTorna 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 York

Mé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)  # Gold

Para 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.0

Heranç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 = 30

O 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")  # TypeError

slots=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 bytes

Dataclasses 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"  # AttributeError

kw_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

FeaturedataclassnamedtupleTypedDictPydanticattrs
MutabilityMutável (padrão)ImutávelN/A (subclasse de dict)MutávelConfigurável
Type validationApenas anotaçõesNãoApenas anotaçõesValidação em runtimeValidação em runtime
Default valuesSimSimNãoSimSim
MethodsSuporte completo a classesLimitadoNãoSuporte completo a classesSuporte completo a classes
InheritanceSimNãoLimitadoSimSim
Memory overheadModeradoBaixoBaixoMaiorModerado
Slots supportSim (3.10+)NãoNãoSimSim
PerformanceRápidoMais rápidoRápidoMais lento (validação)Rápido
Built-inSim (3.7+)SimSim (3.8+)NãoNã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)  # True

Registros 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 data

Dataclasses 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 code

Com 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 bug

Boas práticas e armadilhas (gotchas)

  1. Sempre use default_factory para padrões mutáveis: nunca atribua [] ou {} diretamente
  2. Type hints são obrigatórios: dataclasses dependem de anotações, não de valores
  3. A ordem dos campos importa: campos sem padrão antes dos campos com padrão
  4. frozen=True para dados imutáveis: útil para objetos hashable e thread safety
  5. Use __post_init__ com moderação: lógica demais reduz a simplicidade das dataclasses
  6. Considere slots=True para grandes volumes de dados: economia significativa de memória em Python 3.10+
  7. 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.

📚