Você tem uma dataclass e precisa de um dict — para serializar, para logar, para entregar a uma biblioteca que só fala dict. Existem três jeitos de fazer isso, e eles se comportam de formas diferentes que importam.
O asdict() é a resposta na maioria das vezes
from dataclasses import dataclass, asdict
@dataclass
class Point:
x: int
y: int
p = Point(3, 4)
print(asdict(p))
Ele imprime:
{'x': 3, 'y': 4}
Essa é a API inteira para o caso simples. Fica interessante um nível abaixo.
Ele desce recursivamente, e é esse o ponto
O asdict não para no topo. Ele percorre dataclasses aninhadas, e as listas, tuplas e dicts que contêm elas.
from dataclasses import dataclass, asdict, field
@dataclass
class Address:
city: str
country: str
@dataclass
class Person:
name: str
address: Address
tags: list = field(default_factory=list)
p = Person('ada', Address('london', 'uk'), ['engineer', 'mathematician'])
print(asdict(p))
Ele imprime:
{'name': 'ada', 'address': {'city': 'london', 'country': 'uk'}, 'tags': ['engineer', 'mathematician']}
O Address aninhado virou um dict aninhado. Nada na saída ainda é uma dataclass, que é exatamente o que um serializador precisa.
O asdict() copia. O vars() não
Essa é a diferença que causa bugs, e é fácil de não notar porque os dois produzem um dict que parece certo.
from dataclasses import dataclass, asdict, field
@dataclass
class Team:
name: str
members: list = field(default_factory=list)
t = Team('core', ['ada', 'grace'])
copied = asdict(t)
shallow = vars(t)
copied['members'].append('MUTATED VIA asdict')
shallow['members'].append('MUTATED VIA vars')
print('original :', t.members)
print('asdict :', copied['members'])
print('vars :', shallow['members'])
Ele imprime:
original : ['ada', 'grace', 'MUTATED VIA vars']
asdict : ['ada', 'grace', 'MUTATED VIA asdict']
vars : ['ada', 'grace', 'MUTATED VIA vars']
Leia a primeira linha. Adicionar ao resultado do asdict deixou o objeto em paz. Adicionar ao resultado do vars mudou o objeto, porque vars(t) devolve o __dict__ real da instância — não uma cópia dele, a coisa em si.
O asdict faz cópia profunda de cada valor na saída. O vars te dá uma referência viva. Se você está prestes a entregar o dict para algo que modifica, essa distinção é o seu bug.
fields() quando você quer controle
O asdict é tudo ou nada. Quando você precisa pular um campo, renomear um, ou parar antes de descer, percorra os campos você mesmo.
from dataclasses import dataclass, fields, field
@dataclass
class User:
name: str
email: str
password: str = field(repr=False)
u = User('ada', 'ada@example.com', 'hunter2')
public = {f.name: getattr(u, f.name) for f in fields(u) if f.name != 'password'}
print(public)
print([f.name for f in fields(u)])
Ele imprime:
{'name': 'ada', 'email': 'ada@example.com'}
['name', 'email', 'password']
O fields() devolve as definições dos campos, então você pode filtrar por qualquer coisa que elas carreguem — inclusive metadados seus.
Marcando um campo como nunca exportado
O field(metadata=...) é o jeito organizado de dizer isso uma vez, na definição, em vez de manter uma lista de nomes em algum outro lugar.
from dataclasses import dataclass, fields, field
@dataclass
class Account:
user: str
token: str = field(metadata={'private': True})
def public_dict(obj):
return {f.name: getattr(obj, f.name)
for f in fields(obj) if not f.metadata.get('private')}
a = Account('ada', 'secret-token-value')
print(public_dict(a))
Ele imprime:
{'user': 'ada'}
O dict_factory muda como cada nível é montado
O asdict aceita uma factory que recebe uma lista de pares (chave, valor). Ela é chamada para o objeto do topo e para cada dataclass aninhada, então uma função cobre a árvore inteira.
from dataclasses import dataclass, asdict
@dataclass
class Inner:
first_name: str
@dataclass
class Outer:
inner: Inner
is_active: bool
def camel(pairs):
def to_camel(s):
head, *rest = s.split('_')
return head + ''.join(w.capitalize() for w in rest)
return {to_camel(k): v for k, v in pairs}
print(asdict(Outer(Inner('ada'), True), dict_factory=camel))
Ele imprime:
{'inner': {'firstName': 'ada'}, 'isActive': True}
Os dois níveis foram renomeados por uma função só.
JSON, e os tipos que travam ele
O json.dumps(asdict(obj)) funciona até o momento em que um campo guarda algo sobre o qual o JSON não tem opinião.
import json
from dataclasses import dataclass, asdict
from datetime import date
from decimal import Decimal
@dataclass
class Invoice:
id: int
issued: date
total: Decimal
inv = Invoice(17, date(2026, 9, 9), Decimal('249.50'))
try:
print(json.dumps(asdict(inv)))
except TypeError as err:
print('TypeError:', err)
def encode(value):
if isinstance(value, Decimal):
return str(value)
if isinstance(value, date):
return value.isoformat()
raise TypeError(f'cannot serialize {type(value).__name__}')
print(json.dumps(asdict(inv), default=encode))
Ele imprime:
TypeError: Object of type date is not JSON serializable
{"id": 17, "issued": "2026-09-09", "total": "249.50"}
O asdict fez o trabalho dele — produziu um dict. Ele não converte os valores das folhas, e nunca teve essa função. Conversão de tipo é assunto do default=, ou de um dict_factory se você quiser que aconteça na hora de montar o dict.
Repare que o Decimal virou string, não float. float(Decimal('249.50')) traria exatamente o erro de arredondamento que o Decimal estava ali para evitar.
Três erros que vale nomear
Chamar o asdict dentro de um laço quente. Ele faz cópia profunda da árvore inteira toda vez. Se você só precisa dos nomes dos campos, o fields() é muito mais barato.
Esperar que ele funcione fora de uma dataclass. Ele levanta erro em vez de adivinhar.
from dataclasses import asdict, is_dataclass
class Plain:
def __init__(self):
self.x = 1
try:
asdict(Plain())
except TypeError as err:
print('TypeError:', err)
print('is_dataclass:', is_dataclass(Plain()))
Ele imprime:
TypeError: asdict() should be called on dataclass instances
is_dataclass: False
Recorrer ao vars() numa classe com slots. Uma dataclass com slots=True não tem __dict__ nenhum, então o atalho não é só arriscado, ele falha direto.
from dataclasses import dataclass, asdict
@dataclass(slots=True)
class Fast:
x: int
y: int
f = Fast(1, 2)
print('asdict works:', asdict(f))
try:
print(vars(f))
except TypeError as err:
print('TypeError:', err)
Ele imprime:
asdict works: {'x': 1, 'y': 2}
TypeError: vars() argument must have __dict__ attribute
O asdict lê as definições dos campos, não o __dict__, então ele continua funcionando. Esse é mais um motivo para ele ser a escolha padrão.
O que lembrar
-
O
asdict(obj)desce por dataclasses aninhadas, listas, tuplas e dicts, e faz cópia profunda de cada valor no caminho. -
O
vars(obj)é raso e devolve o__dict__vivo. Modificar o que você recebe modifica o objeto, e ele falha numa dataclass com slots. -
Use
fields(obj)quando precisar pular, renomear ou parar antes. Ofield(metadata=...)registra essa decisão ao lado do próprio campo. -
O
dict_factoryvale para todos os níveis da árvore, então renomear chaves é uma função só. -
O
asdictproduz um dict, não JSON.dateeDecimalainda precisam dodefault=, e oDecimaldeve virar string, não float.