Converting and copying: asdict, fields, replace

dataclasses · Practical · 9 min

Serialisation and copying are what you do with data objects most, and the helpers have important subtleties.

The module provides helpers that work on any dataclass instance:

  • asdict(obj) returns a dict of field names to values, recursively converting nested dataclasses, and copying lists, tuples and dicts with copy.deepcopy for other values. Perfect for json.dumps(asdict(obj)), as long as the values are JSON-serialisable (dates and Decimals need default=str).
  • astuple(obj) does the same into a tuple.
  • dict_factory= customises the result, e.g. dropping None values.
  • replace(obj, **changes) creates a new instance through __init__, so __post_init__ and validation run again. Fields with init=False can't be passed and are recomputed.
  • fields(obj) lists the fields; is_dataclass(x) checks for a dataclass class or instance.

asdict is convenient but not free: it deep-copies, which is slow for large objects. For hot paths, a hand-written {"a": self.a, ...} or vars(obj) (shallow, includes only instance attributes, not for slotted classes) is much faster.

The reverse direction, dict to dataclass, is MyClass(**data) for flat data; nested structures need your own code or a library such as cattrs, dacite or Pydantic.

Example

import json
from dataclasses import asdict, astuple, dataclass, field, replace

@dataclass
class Address:
    city: str
    zip: str | None = None

@dataclass
class User:
    name: str
    address: Address
    roles: list[str] = field(default_factory=list)

u = User("ada", Address("London"), ["admin"])
print(asdict(u))
# {'name': 'ada', 'address': {'city': 'London', 'zip': None}, 'roles': ['admin']}
print(astuple(u))                       # ('ada', ('London', None), ['admin'])
no_none = asdict(u.address, dict_factory=lambda kv: {k: v for k, v in kv if v is not None})
print(json.dumps(no_none))              # {"city": "London"}

moved = replace(u, address=Address("Paris", "75001"))
print(moved.address.city, u.address.city)   # Paris London
print(User(**{"name": "bob", "address": Address("Rome")}))

Remember

  • asdict/astuple convert recursively, deep-copying values
  • replace() re-runs __init__ and __post_init__
  • Dict to nested dataclass needs custom code or a library

Watch out for

  • Calling asdict on large objects in a hot loop; its deep copy is expensive.

Check yourself

  1. What does asdict() do with a field holding another dataclass instance?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/dataclasses.html#dataclasses.asdict