O Python 3.7 trouxe os dataclasses, uma forma mais simples e elegante de criar classes que são basicamente containers de dados. Se você já escreveu dezenas de __init__, __repr__ e __eq__ manualmente, essa feature vai mudar a sua vida.

O problema: boilerplate em classes

Considere uma classe simples para representar um usuário:

class Usuario:
    def __init__(self, nome: str, email: str, idade: int, ativo: bool = True):
        self.nome = nome
        self.email = email
        self.idade = idade
        self.ativo = ativo

    def __repr__(self):
        return f"Usuario(nome={self.nome!r}, email={self.email!r}, idade={self.idade}, ativo={self.ativo})"

    def __eq__(self, other):
        if not isinstance(other, Usuario):
            return NotImplemented
        return (self.nome == other.nome and
                self.email == other.email and
                self.idade == other.idade and
                self.ativo == other.ativo)

30 linhas para uma classe simples. Com dataclasses:

from dataclasses import dataclass

@dataclass
class Usuario:
    nome: str
    email: str
    idade: int
    ativo: bool = True

6 linhas. O Python gera automaticamente __init__, __repr__, __eq__ e outros métodos.

Como funciona

O decorador @dataclass analisa as anotações de tipo da classe e gera métodos especiais. Ao criar uma instância:

user = Usuario(nome="Wellington", email="well@email.com", idade=30)
print(user)  # Usuario(nome='Wellington', email='well@email.com', idade=30, ativo=True)

# Igualdade funciona automaticamente
user2 = Usuario(nome="Wellington", email="well@email.com", idade=30)
print(user == user2)  # True

Valores padrão

Campos com valores padrão devem vir depois dos campos sem padrão:

@dataclass
class Config:
    host: str
    port: int
    debug: bool = False
    max_connections: int = 100

Para valores padrão mutáveis (listas, dicts), use field(default_factory=...):

from dataclasses import dataclass, field

@dataclass
class Time:
    nome: str
    jogadores: list = field(default_factory=list)
    titulos: dict = field(default_factory=dict)

# Cada instância terá sua própria lista e dict
time1 = Time("Flamengo")
time2 = Time("Vasco")
time1.jogadores.append("Jogador 1")
print(time2.jogadores)  # [] — não afetado

field() para controle fino

O field() oferece opções avançadas:

from dataclasses import dataclass, field

@dataclass
class Produto:
    nome: str
    preco: float
    # Não aparece no __repr__
    _estoque: int = field(repr=False, default=0)
    # Não participa da comparação (__eq__)
    _cache: dict = field(compare=False, default_factory=dict)
    # Valor calculado no __post_init__
    preco_com_imposto: float = field(init=False, default=0.0)

    def __post_init__(self):
        self.preco_com_imposto = self.preco * 1.1

__post_init__

O método __post_init__ é chamado após o __init__ gerado. Útil para validações ou cálculos:

@dataclass
class Retangulo:
    largura: float
    altura: float
    area: float = field(init=False)

    def __post_init__(self):
        if self.largura <= 0 or self.altura <= 0:
            ValueError("Dimensões devem ser positivas")
        self.area = self.largura * self.altura

Dataclasses imutáveis

Use frozen=True para criar objetos que não podem ser modificados após a criação:

@dataclass(frozen=True)
class Ponto:
    x: float
    y: float

p = Ponto(1.0, 2.0)
p.x = 5.0  # Erro! FrozenInstanceError

Dataclasses frozen também são hashable, o que permite usá-las como chaves de dicionário ou em sets:

pontos = {Ponto(1, 2), Ponto(3, 4), Ponto(1, 2)}
print(len(pontos))  # 2 — duplicatas removidas

Ordenação

Use order=True para gerar métodos de comparação (__lt__, __le__, __gt__, __ge__):

@dataclass(order=True)
class Tarefa:
    prioridade: int
    nome: str

tarefas = [
    Tarefa(3, "Baixa prioridade"),
    Tarefa(1, "Alta prioridade"),
    Tarefa(2, "Média prioridade"),
]

for t in sorted(tarefas):
    print(f"[{t.prioridade}] {t.nome}")
# [1] Alta prioridade
# [2] Média prioridade
# [3] Baixa prioridade

Comparação: dataclass vs namedtuple vs attrs

# namedtuple — imutável, sem métodos customizados
from collections import namedtuple
PontoNT = namedtuple("PontoNT", ["x", "y"])

# dataclass — flexível, mutável por padrão, type hints nativos
@dataclass
class PontoDC:
    x: float
    y: float

# attrs — biblioteca externa, mais opções (validação, conversão)
# import attr
# @attr.s
# class PontoAttr:
#     x = attr.ib()
#     y = attr.ib()
Feature namedtuple dataclass attrs
Imutável por padrão Sim Não Não
Type hints nativos Não Sim Opcional
Customização fácil Limitada Boa Excelente
Dependência externa Não Não Sim

Exemplo real: modelando responses de API

from dataclasses import dataclass, field
from typing import List, Optional

@dataclass
class Endereco:
    rua: str
    cidade: str
    estado: str
    cep: str

@dataclass
class UsuarioAPI:
    id: int
    nome: str
    email: str
    enderecos: List[Endereco] = field(default_factory=list)
    telefone: Optional[str] = None

    @property
    def endereco_principal(self) -> Optional[Endereco]:
        return self.enderecos[0] if self.enderecos else None

# Simulando uma response de API
dados = {
    "id": 1,
    "nome": "Wellington",
    "email": "well@email.com",
    "enderecos": [
        {"rua": "Rua A", "cidade": "SP", "estado": "SP", "cep": "01000-000"}
    ]
}

# Convertendo de dict
user = UsuarioAPI(
    id=dados["id"],
    nome=dados["nome"],
    email=dados["email"],
    enderecos=[Endereco(**e) for e in dados["enderecos"]]
)
print(user)

Outras novidades do Python 3.7

Além dos dataclasses, o 3.7 trouxe:

  • breakpoint() — função nativa para debug, substituindo import pdb; pdb.set_trace()
  • Dicionários ordenadosdict agora mantém ordem de inserção (comportamento garantido)
  • importlib.resources — acesso a recursos dentro de pacotes
  • Pedidos de melhoria (PEP) — vários PEPs que melhoraram o typing

Conclusão

Dataclasses eliminam o boilerplate de classes de dados no Python. Para a maioria dos casos onde você criaria uma classe com __init__ e __repr__, um dataclass é a escolha certa. Comece a usá-los nos seus projetos — você vai escrever menos código e ganhar mais legibilidade.

Combine com frozen=True para objetos imutáveis e order=True para ordenação, e terá classes poderosas com mínimo esforço.

Caso eu tenha falado alguma besteira, por favor, agradecerei correções e sugestões.