Voltar ao Glossario
Glossario Python

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 eficiente
  • dataclass: estruturas de dados com possível mutabilidade e métodos auxiliares
  • attrs: quando quiser validators e conversores embutidos, sem dependências de runtime
  • Pydantic: 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