Dataclasses em Python Explicado - Conceitos, Código e Boas Práticas

Introdução

Quando uma classe Python existe principalmente para representar dados, é comum escrevermos muito código repetitivo. Precisamos declarar __init__(), atribuir atributos, implementar __repr__() para debugging e, muitas vezes, __eq__() para comparar objetos.

As Dataclasses reduzem esse boilerplate e tornam a intenção da classe mais explícita.


O que são Dataclasses?

Uma Dataclass é uma classe normal que foi processada pelo decorador @dataclass. O decorador examina as anotações de tipo e adiciona métodos gerados, como __init__(), __repr__() e __eq__().

PYTHON
from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
Clique para expandir e ver mais

Podemos então fazer:

PYTHON
user = User("Alison", 38)

print(user)
# User(name='Alison', age=38)
Clique para expandir e ver mais

E:

PYTHON
user1 = User("Alison", 38)
user2 = User("Alison", 38)

print(user1 == user2)
# True
Clique para expandir e ver mais

A documentação oficial explica que o decorador não cria uma nova classe: ele processa a classe existente, adiciona os métodos necessários e retorna a mesma classe.


Quando surgiram?

Dataclasses foram propostas na PEP 557 e adicionadas ao Python 3.7. A proposta buscava oferecer uma solução da biblioteca padrão para classes que representam principalmente dados, reduzindo código repetitivo.

Antes delas, era comum escrever classes manualmente, usar namedtuple, typing.NamedTuple ou bibliotecas como attrs.


O que acontece quando a Dataclass é criada?

Esta é uma das partes mais importantes para entender o recurso.

Considere:

PYTHON
@dataclass
class User:
    name: str
    age: int
Clique para expandir e ver mais

Quando o decorador é executado, ele examina User.__annotations__ e identifica name e age como campos.

Com os parâmetros padrão (init=True, repr=True, eq=True), ele gera métodos equivalentes a:

PYTHON
def __init__(self, name: str, age: int):
    self.name = name
    self.age = age

def __repr__(self):
    return f"User(name={self.name!r}, age={self.age!r})"

def __eq__(self, other):
    if other.__class__ is self.__class__:
        return (
            self.name == other.name
            and self.age == other.age
        )
    return NotImplemented
Clique para expandir e ver mais

Isso é uma representação conceitual do resultado; não significa que o código-fonte desses métodos seja inserido literalmente na classe dessa forma.


Getters e Setters: O que são e como funcionam em Python?

O que são?

Em linguagens como Java ou C#, é comum ver código assim:

JAVA
// Java - getters e setters tradicionais
public class User {
    private String name;
    private int age;

    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return this.age; }
    public void setAge(int age) { this.age = age; }
}
Clique para expandir e ver mais

Nessas linguagens, o acesso a atributos passa sempre por métodos:

JAVA
User user = new User();
user.setName("Alison");
System.out.println(user.getName());
Clique para expandir e ver mais

Como funciona em Python?

Em Python, o acesso a atributos é direto - não precisamos de métodos get_ e set_:

PYTHON
user = User("Alison", 38)

# Acesso direto (não precisa de get_name())
print(user.name)   # Alison

# Atribuição direta (não precisa de set_name())
user.age = 39
Clique para expandir e ver mais

A Dataclass cria getters e setters?

Não. É comum encontrar a afirmação de que uma Dataclass “cria getters e setters para cada atributo”. Isso não é verdade para Python.

Dataclasses não geram automaticamente métodos como:

PYTHON
get_name()
set_name()
get_age()
set_age()
Clique para expandir e ver mais

O que a Dataclass gera são os métodos especiais (__init__, __repr__, __eq__), que rolam por “debaixo dos panos” e não são chamados diretamente pelo programador.

E se eu precisar de lógica no acesso ou na alteração?

Se você precisa de validação ou lógica ao ler ou alterar um atributo, use @property:

PYTHON
from dataclasses import dataclass

@dataclass
class User:
    name: str
    _age: int

    @property
    def age(self) -> int:
        """Getter: lógica ao acessar o valor."""
        return self._age

    @age.setter
    def age(self, value: int) -> None:
        """Setter: lógica ao alterar o valor."""
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value
Clique para expandir e ver mais

Agora:

PYTHON
user = User("Alison", 38)

print(user.age)      # 38 - por baixo chama o getter
user.age = 39        # por baixo chama o setter
user.age = -5        # ValueError: Age cannot be negative
Clique para expandir e ver mais

O property fornece a interface user.age, mas por baixo existe lógica no getter e no setter. Essa lógica não é criada automaticamente pela Dataclass.

Resumo visual

PLAINTEXT
┌─────────────────────────────────────────────────────────────┐
│  OUTRAS LINGUAGENS (Java, C#)                               │
│  user.getName()  →  getter explícito                        │
│  user.setName(x) →  setter explícito                        │
├─────────────────────────────────────────────────────────────┤
│  PYTHON (padrão)                                            │
│  user.name       →  acesso direto ao atributo               │
│  user.name = x   →  atribuição direta                       │
├─────────────────────────────────────────────────────────────┤
│  PYTHON (com @property)                                     │
│  user.name       →  chama getter por baixo                  │
│  user.name = x   →  chama setter por baixo                  │
│  (interface igual, mas com lógica embutida)                 │
├─────────────────────────────────────────────────────────────┤
│  DATACLASS                                                  │
│  NÃO cria get_name() / set_name()                           │
│  Cria __init__, __repr__, __eq__ (métodos especiais)        │
└─────────────────────────────────────────────────────────────┘
Clique para expandir e ver mais

__init__(), __repr__() e __eq__()

__init__()

Inicializa a instância depois que ela é criada.

PYTHON
@dataclass
class User:
    name: str
    age: int
Clique para expandir e ver mais

A Dataclass gera conceitualmente:

PYTHON
def __init__(self, name: str, age: int):
    self.name = name
    self.age = age
Clique para expandir e ver mais

Assim:

PYTHON
user = User("Alison", 38)
Clique para expandir e ver mais

funciona sem precisarmos escrever o método.

__repr__()

Fornece uma representação útil do objeto:

PYTHON
print(User("Alison", 38))
# User(name='Alison', age=38)
Clique para expandir e ver mais

Isso é especialmente útil em debugging, logs e coleções.

Podemos impedir que um campo apareça:

PYTHON
from dataclasses import dataclass, field

@dataclass
class User:
    name: str
    password: str = field(repr=False)
Clique para expandir e ver mais

__eq__()

Permite comparação por valor:

PYTHON
user1 = User("Alison", 38)
user2 = User("Alison", 38)

user1 == user2
# True
Clique para expandir e ver mais

O eq=True padrão faz a Dataclass gerar esse método.

Observação de versão: no Python 3.13, a implementação gerada de __eq__() passou a comparar os campos individualmente, em vez de construir tuplas para a comparação.


@dataclass - Parâmetros do decorador

A assinatura atual é:

PYTHON
@dataclass(
    *,
    init=True,
    repr=True,
    eq=True,
    order=False,
    unsafe_hash=False,
    frozen=False,
    match_args=True,
    kw_only=False,
    slots=False,
    weakref_slot=False,
)
Clique para expandir e ver mais

Os parâmetros mais importantes são:

ParâmetroO que fazDocumentação
initGera __init__()dataclasses
reprGera __repr__()dataclasses
eqGera __eq__()dataclasses
orderGera __lt__, __le__, __gt__, __ge__dataclasses
unsafe_hashControla a geração forçada de __hash__()dataclasses
frozenImpede atribuições normais aos camposFrozen instances
match_argsControla __match_args__ para pattern matchingdataclasses
kw_onlyTorna os campos keyword-onlydataclasses
slotsGera __slots__dataclasses
weakref_slotAdiciona __weakref__, exige slots=Truedataclasses

Valor padrão vs. field(default=...) - Qual a diferença?

Estas duas declarações:

PYTHON
age: int = 18
Clique para expandir e ver mais

e:

PYTHON
age: int = field(default=18)
Clique para expandir e ver mais

produzem, para esse caso simples, o mesmo valor padrão e o mesmo parâmetro no __init__():

PYTHON
def __init__(self, age: int = 18):
    self.age = age
Clique para expandir e ver mais

A diferença é de finalidade e flexibilidade.

Quando usar age: int = 18?

Use quando você simplesmente quer um valor padrão, sem configurações extras:

PYTHON
@dataclass
class User:
    name: str
    age: int = 18
Clique para expandir e ver mais

Vantagem: mais curto, mais legível, suficiente para 90% dos casos.

Quando usar field(default=18)?

Use quando também precisa configurar o comportamento daquele campo:

PYTHON
from dataclasses import field

@dataclass
class User:
    name: str
    age: int = field(
        default=18,
        repr=False,      # não aparece no print()
        compare=False,   # não entra na comparação ==
    )
Clique para expandir e ver mais

Nesse exemplo, age continua com valor padrão 18, mas:

O que mais field() permite?

PYTHON
field(
    default=18,                    # valor padrão simples
    default_factory=list,          # valor padrão mutável (função)
    init=False,                   # não aparece no __init__
    repr=False,                   # não aparece no __repr__
    compare=False,                # não entra em ==, <, >
    hash=False,                   # não entra no __hash__
    metadata={"description": "User age"},  # metadados extras
)
Clique para expandir e ver mais

Fluxograma de decisão

PLAINTEXT
                    Você quer um valor padrão?
                           │
           ┌───────────────┴───────────────┐
           ▼                               ▼
    É só um valor simples?        Precisa de configuração extra?
           │                               │
           ▼                               ▼
    age: int = 18                  age: int = field(
    (mais legível)                      default=18,
                                        repr=False,
    ✅ Recomendado                      compare=False,
    para iniciantes                     ...)

                                        ✅ Recomendado
                                        quando precisa de controle
Clique para expandir e ver mais

Atributos com valores padrão

PYTHON
@dataclass
class User:
    name: str
    age: int
    active: bool = True
Clique para expandir e ver mais

Então:

PYTHON
user = User("Alison", 38)
Clique para expandir e ver mais

usa True para active.

⚠️ Regra importante: campos sem valor padrão devem aparecer antes dos campos com valor padrão.


Valores mutáveis e default_factory

❌ O problema

Evite:

PYTHON
@dataclass
class ShoppingCart:
    items: list[str] = []   # PERIGO! Todos os objetos compartilham a mesma lista
Clique para expandir e ver mais

Isso cria uma única lista no momento da definição da classe. Todas as instâncias compartilham essa mesma lista.

✅ A solução

Para valores mutáveis, use:

PYTHON
from dataclasses import field

@dataclass
class ShoppingCart:
    items: list[str] = field(default_factory=list)
Clique para expandir e ver mais

default_factory recebe uma função sem argumentos. Ela é chamada quando uma nova instância precisa do valor padrão, criando uma lista nova e independente para cada objeto.

PYTHON
cart1 = ShoppingCart()
cart2 = ShoppingCart()

cart1.items.append("Book")

print(cart1.items)  # ['Book']
print(cart2.items)  # []  ← lista separada!
Clique para expandir e ver mais

Por que usar __post_init__()?

__post_init__() é o ponto de extensão da inicialização automática da Dataclass. Ele é chamado automaticamente depois que o __init__() gerado atribui todos os campos.

Quando usar?

SituaçãoExemplo
Calcular atributos derivadosarea = width * height
Validar combinações de camposif price < 0: raise ValueError
Normalizar dados depois da atribuiçãoconverter string para maiúsculas
Inicialização que depende de vários atributosfull_name = first + " " + last
Trabalhar com InitVarpassar valores temporários para o init
Chamar __init__() de uma classe baseherança com dataclasses

Exemplo 1: Atributo derivado

PYTHON
from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)  # não entra no __init__()

    def __post_init__(self):
        self.area = self.width * self.height
Clique para expandir e ver mais

O que acontece por baixo quando criamos Rectangle(10, 5):

PLAINTEXT
1. __init__() gerado executa:
   self.width = 10
   self.height = 5

2. Automaticamente chama:
   self.__post_init__()

3. __post_init__() executa:
   self.area = 10 * 5  →  50.0
Clique para expandir e ver mais
PYTHON
rectangle = Rectangle(10, 5)
print(rectangle.area)  # 50.0
Clique para expandir e ver mais

Exemplo 2: Validação

PYTHON
@dataclass
class Product:
    name: str
    price: float

    def __post_init__(self):
        if self.price < 0:
            raise ValueError("Price cannot be negative")
        if not self.name.strip():
            raise ValueError("Name cannot be empty")
Clique para expandir e ver mais
PYTHON
# Funciona normalmente
product = Product("Laptop", 1500.00)

# Levanta erro na criação
product = Product("Laptop", -100)
# ValueError: Price cannot be negative
Clique para expandir e ver mais

Exemplo 3: Normalização de dados

PYTHON
@dataclass
class User:
    name: str
    email: str

    def __post_init__(self):
        self.name = self.name.strip().title()
        self.email = self.email.strip().lower()
Clique para expandir e ver mais
PYTHON
user = User("  alison  ", "  Alison@Email.COM  ")
print(user.name)   # Alison
print(user.email)  # alison@email.com
Clique para expandir e ver mais

⚠️ Atenção

__post_init__() é chamado automaticamente apenas quando a Dataclass gera o __init__(). Se você usar init=False ou escrever seu próprio __init__(), a chamada automática não acontece.


Métodos personalizados

Dataclasses continuam sendo classes Python normais:

PYTHON
@dataclass
class Product:
    name: str
    price: float

    def apply_discount(self, percentage: float) -> None:
        self.price *= 1 - percentage / 100

    @property
    def price_with_tax(self) -> float:
        return self.price * 1.10
Clique para expandir e ver mais

Podemos adicionar propriedades, métodos, validações e outras funcionalidades normalmente.


frozen=True - Instâncias imutáveis

PYTHON
from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: float
    y: float
Clique para expandir e ver mais

Depois de criado:

PYTHON
point = Point(10, 20)
point.x = 30
# FrozenInstanceError: cannot assign to field 'x'
Clique para expandir e ver mais

Isso fornece uma forma de emular instâncias somente-leitura.

⚠️ Atenção: frozen=True não torna objetos internos profundamente imutáveis. Uma lista armazenada em um campo continua sendo uma lista mutável.


order=True - Ordenação de objetos

PYTHON
@dataclass(order=True)
class Product:
    price: float
Clique para expandir e ver mais

Gera:

PYTHON
__lt__   # menor que (<)
__le__   # menor ou igual (<=)
__gt__   # maior que (>)
__ge__   # maior ou igual (>=)
Clique para expandir e ver mais

Permite:

PYTHON
Product(10) < Product(20)
# True
Clique para expandir e ver mais

⚠️ Só use order=True quando a ordenação realmente fizer sentido para o domínio. Nem toda entidade possui um conceito útil de “menor” e “maior”.


slots=True - Otimização de memória

PYTHON
@dataclass(slots=True)
class User:
    name: str
    age: int
Clique para expandir e ver mais

slots=True cria uma Dataclass baseada em __slots__, o que pode:


weakref_slot=True

É um recurso mais avançado:

PYTHON
@dataclass(slots=True, weakref_slot=True)
class User:
    name: str
Clique para expandir e ver mais

Ele adiciona __weakref__ e exige slots=True.


kw_only=True - Apenas argumentos nomeados

PYTHON
@dataclass(kw_only=True)
class User:
    name: str
    age: int
Clique para expandir e ver mais

Agora:

PYTHON
User(name="Alison", age=38)   # ✅ válido
User("Alison", 38)             # ❌ TypeError
Clique para expandir e ver mais

Útil para evitar confusão com a ordem dos argumentos.


Funções auxiliares do módulo dataclasses

O módulo oferece ferramentas auxiliares importantes:

PYTHON
from dataclasses import (
    asdict,
    astuple,
    fields,
    replace,
    is_dataclass,
)
Clique para expandir e ver mais

asdict() - Converter para dicionário

PYTHON
user = User(name="Alison", age=38)

asdict(user)
# {"name": "Alison", "age": 38}
Clique para expandir e ver mais

astuple() - Converter para tupla

PYTHON
astuple(user)
# ("Alison", 38)
Clique para expandir e ver mais

fields() - Inspecionar campos

PYTHON
for item in fields(User):
    print(item.name, item.type, item.default)

# name <class 'str'> <dataclasses._MISSING_TYPE object>
# age <class 'int'> 18
Clique para expandir e ver mais

replace() - Criar cópia modificada

PYTHON
updated = replace(user, age=39)
# User(name='Alison', age=39)  - user original não muda!
Clique para expandir e ver mais

replace() cria uma nova instância e passa pelo __init__() e, consequentemente, pelo __post_init__().

is_dataclass() - Verificar se é dataclass

PYTHON
is_dataclass(User)     # True
is_dataclass(user)     # True (instância também funciona)
is_dataclass(str)      # False
Clique para expandir e ver mais

Dataclass vs. classe normal vs. NamedTuple

CaracterísticaClasse normalDataclassNamedTuple
BoilerplateMaiorMenorMenor
MutabilidadeNormalmente simSim por padrãoNão
Métodos personalizadosExcelenteExcelentePossível
Igualdade por valorManualAutomáticaSim
Valores padrãoManualSimSim
Semântica de tuplaNãoNãoSim
Controle sobre camposMáximoAltoMais limitado
Performance (slots)SimSim (slots=True)Sim (inherente)

Quando usar cada uma?

PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐
│  A classe representa PRINCIPALMENTE dados estruturados?         │
│                      │                                          │
│         ┌────────────┴────────────┐                           │
│         ▼                         ▼                           │
│        SIM                        NÃO                          │
│         │                         │                           │
│         ▼                         ▼                           │
│  ┌──────────────┐        ┌─────────────────┐                │
│  │  Precisa de  │        │  Classe normal  │                │
│  │  imutabilidade│        │  (controle      │                │
│  │  ou semântica │        │  total do       │                │
│  │  de tupla?    │        │  comportamento) │                │
│  │      │        │        └─────────────────┘                │
│  │  ┌───┴───┐    │                                           │
│  │  ▼       ▼    │                                           │
│  │ SIM     NÃO   │                                           │
│  │  │      │     │                                           │
│  │  ▼      ▼     │                                           │
│  │ NamedTuple  Dataclass                                      │
│  └───────────────┘                                           │
└─────────────────────────────────────────────────────────────────┘
Clique para expandir e ver mais

Casos práticos

DTO (Data Transfer Object)

PYTHON
@dataclass
class UserDTO:
    id: int
    name: str
    email: str
Clique para expandir e ver mais

Configuração

PYTHON
@dataclass
class DatabaseConfig:
    host: str
    port: int = 5432
    database: str = "app"
    ssl: bool = True
Clique para expandir e ver mais

Resultado de processamento

PYTHON
@dataclass
class ProcessingResult:
    success: bool
    processed: int
    errors: int
Clique para expandir e ver mais

Modelo interno com lógica

PYTHON
@dataclass
class OrderItem:
    product_id: int
    quantity: int
    unit_price: float

    @property
    def total(self) -> float:
        return self.quantity * self.unit_price

    def __post_init__(self):
        if self.quantity <= 0:
            raise ValueError("Quantity must be positive")
Clique para expandir e ver mais

Erros e armadilhas comuns

❌ Não use listas, dicionários ou sets mutáveis diretamente como defaults

PYTHON
# ERRADO
items: list[str] = []

# CERTO
items: list[str] = field(default_factory=list)
Clique para expandir e ver mais

❌ Não confunda frozen=True com imutabilidade profunda

PYTHON
@dataclass(frozen=True)
class Container:
    items: list[str]

container = Container(["a", "b"])
container.items.append("c")  # Funciona! A lista interna é mutável
Clique para expandir e ver mais

❌ Não use unsafe_hash=True sem entender hashing e igualdade

Hash precisa ser consistente com a semântica de igualdade do objeto.

❌ Não use order=True sem uma ordenação natural

Nem toda entidade possui um conceito útil de “menor” e “maior”.

❌ Não transforme toda classe em Dataclass

Dataclass é uma ferramenta, não uma regra arquitetural.


Dataclass não é ORM

Uma Dataclass não é automaticamente um modelo de banco de dados.

Ela pode representar um DTO, um objeto de domínio, uma configuração ou um resultado de serviço, mas não substitui mecanismos de persistência como Django ORM ou SQLAlchemy.

PLAINTEXT
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Banco de      │     │   SQLAlchemy/   │     │   Dataclass     │
│   Dados         │◄────│   Django ORM    │────►│   (DTO/Modelo)  │
│                 │     │                 │     │                 │
└─────────────────┘     └─────────────────┘     └─────────────────┘
       ↑                                               ↓
       └──────────── Persistência ─────────────────────┘

Dataclass = representação de dados
ORM       = persistência e consulta
Clique para expandir e ver mais

Conclusão

Dataclasses foram introduzidas no Python 3.7 pela PEP 557 para reduzir boilerplate e tornar mais declarativas as classes orientadas a dados.

Os conceitos mais importantes são:

O ponto principal não é simplesmente escrever menos código.

É declarar claramente a estrutura dos dados e deixar o Python gerar comportamentos mecânicos quando eles fazem sentido.


Referências

Iniciar busca

Digite palavras-chave para buscar

↑↓
ESC
⌘K Atalho