LESSON 173 · DEEPER PYTHON28 min

Dataclass to dict in Python: asdict, nested objects, and copy behavior

Convert dataclass instances to dictionaries with asdict and understand nested conversion versus shallow field access.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Dataclass to dict in Python: asdict, nested objects, and copy behavior

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    We have an Order dataclass with a nested Customer and a list of items. A downstream function expects a dictionary, perhaps because it will build a response or write JSON. The tempting shortcut is order.__dict__. It prints like a dictionary, so at first glance it looks finished. But the nested customer is still a Customer object, not a dictionary, and JSON encoding will not know what to do with it. The documented conversion is dataclasses.asdict. It follows declared fields and recursively converts nested dataclasses. We will reproduce the failed shortcut, inspect the field structure, then use asdict and test what happens when we change the exported data.

  2. 00:43

    There is a second question behind the conversion: do we want a detached snapshot or a shallow mapping that shares nested objects? asdict gives us the former. I will show both so you can choose based on the behavior you need, rather than treating any object attribute dictionary as a complete exporter. The first line looks close to our target: it has customer and items keys. Look at the value of customer, though. It is still a dataclass instance. __dict__ exposes this object's attributes but does not recursively transform values. json.dumps then fails when it reaches Customer. This does not mean every asdict result is automatically JSON-ready either; date,

  3. 01:27

    Decimal, and other special values still need a deliberate serialization policy. The immediate failure here tells us the nested dataclass was not converted. The shortcut is also not universal because a slotted dataclass may not have a normal __dict__ at all. We should use the dataclasses API when our goal is conversion of declared fields. Avoid adding a generic JSON default function that stringifies anything just to silence this error. That can hide a bad data contract by turning a structured object into a surprising string. First produce the mapping we actually intend. The fields function gives us the declared dataclass field objects.

  4. 02:10

    A mapping comprehension can read each field's current value, but this is intentionally shallow. Customer remains a Customer, and the items list is the exact same list held by order. That can be useful inside a controlled program when a live reference is desired, but it does not solve our nested conversion goal. It also illustrates why copying semantics matter. If we hand a shallow mapping to another function and it appends an item, the original Order changes too. asdict takes a different route: nested dataclasses, dictionaries, lists, and tuples are traversed, while other objects are deep- copied. It returns a separate structure.

  5. 02:52

    We can test that claim directly rather than simply trusting the printed shape. Notice that type annotations on the dataclass do not validate input automatically; this lesson assumes the Order was already constructed with appropriate values and focuses on exporting it. This is the conversion we wanted. asdict reads the declared fields, replaces the nested Customer with a dictionary, and returns a new outer dictionary. json.dumps now works because this particular example contains only JSON-compatible strings and a list. Keep the operations distinct in your mental model: asdict produces Python data;

  6. 03:32

    json.dumps produces JSON text. If the Order had a datetime field, converting the dataclass would still leave a datetime value, and the standard JSON encoder would reject it without additional handling. The simplest safe path is to define a specific conversion for those special fields before serialization. The lesson page's task asks you to change export_order to return asdict(order) and then checks the nested shape and list independence. It does not ask you to override JSON encoding. That narrower exercise makes the critical behavior easy to verify.

  7. 04:12

    Now let's prove the result is not secretly a live view into the original object. The exported data changed, but the Order did not. That is the snapshot behavior we wanted for a response or a transformation step. It also means editing a dictionary returned by asdict is not a way to update the dataclass. To change the model, assign to its fields deliberately or construct a new instance, then export again. With very large nested structures, this copying has a cost, so use the fields- based shallow mapping when its shared- reference semantics are actually the intended contract. Be explicit about that choice because the two forms look similar when simply

  8. 04:53

    printed. The original shortcut, order.__dict__, is still sometimes useful for debugging an ordinary object, but it is not the documented general answer to nested dataclass conversion. In the browser task, try a nested Order, inspect the type of the exported customer, and append to the exported items. If the original list changes, you have built a shallow mapping instead of the required asdict snapshot.

Understand the concept

To convert a dataclass to dict, import asdict from dataclasses and pass it an instance, not the class. asdict uses the declared dataclass fields as keys. The result is an ordinary dictionary suitable for inspection and for later serialization when its values are JSON-compatible. This lesson focuses on the conversion itself: @dataclass defines a record, asdict exports it, and json.dumps is a separate operation that can fail on unsupported values such as a datetime without custom handling.

The important behavior appears with nested data. asdict recursively converts nested dataclass instances, including ones inside lists, tuples, and dictionaries. A Customer object inside an Order becomes a nested dict. It also deep-copies other contained objects, so the result is not merely the original instance's __dict__. Changing a list inside the exported dictionary should not mutate the original dataclass. That distinction matters when a caller prepares an API response and then edits it for display.

If you actually want a shallow field mapping, use {field.name: getattr(obj, field.name) for field in fields(obj)}. That keeps nested objects as objects and shares references to mutable field values. A shallow mapping can be faster but carries different semantics. Do not treat obj.__dict__ as a universal dataclass exporter: slotted or otherwise customized classes may not expose it, and it does not recursively convert nested records.

The exercise builds an Order containing a Customer and a list of line items. Implement export_order(order) with asdict, then prove the nested customer is a dictionary and the exported list is independent. Use the result as a data boundary; if you intend to write JSON, also define how to handle non-JSON values. The related dataclasses lesson covers decorators and default_factory, while this guide answers the conversion and copying question.

  • dataclasses.asdict converts declared fields
  • Nested dataclasses convert recursively
  • Other values are deep-copied
  • fields() enables a shallow mapping

See it step by step

Read the code, predict the output, then compare it with the result.

01. Convert a simple dataclass

PYTHON
from dataclasses import dataclass, asdict
@dataclass
class Point:
    x: int
    y: int
print(asdict(Point(2, 3)))
EXPECTED OUTPUT
{'x': 2, 'y': 3}

The field names become dictionary keys and field values become dictionary values.

02. Convert a nested dataclass

PYTHON
from dataclasses import dataclass, asdict
@dataclass
class Customer:
    name: str
@dataclass
class Order:
    customer: Customer
    items: list[str]
print(asdict(Order(Customer('Ada'), ['book'])))
EXPECTED OUTPUT
{'customer': {'name': 'Ada'}, 'items': ['book']}

The nested Customer is converted recursively rather than retained as a custom object.

03. Compare a shallow field mapping

PYTHON
from dataclasses import dataclass, fields
@dataclass
class Box:
    labels: list[str]
box = Box(['a'])
shallow = {f.name: getattr(box, f.name) for f in fields(box)}
shallow['labels'].append('b')
print(box.labels)
EXPECTED OUTPUT
['a', 'b']

A shallow mapping shares its nested list; asdict would copy it.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Export declared fields with asdict

A dataclass instance is an object, while a consumer may need a mapping. Import asdict from dataclasses and pass the instance. For Point(2, 3), the result is {'x': 2, 'y': 3}. The operation is based on declared dataclass fields and returns a new Python dictionary. It does not change the Point object, write a file, or produce JSON text by itself.

Passing the class instead of an instance raises TypeError. Calling __dict__ may appear to work for a simple class, but it is the wrong general guarantee: nested dataclasses remain objects, and a slotted dataclass may not have __dict__. Start with the documented API, then choose a shallow mapping only if your application actually needs one.

PYTHON
from dataclasses import dataclass, asdict
@dataclass
class Point:
    x: int
    y: int
point = Point(2, 3)
print(point)
print(asdict(point))
EXPECTED OUTPUT
Point(x=2, y=3)
{'x': 2, 'y': 3}
Follow the reasoning
  1. Import dataclass and asdict together.
  2. Compare the object representation with the returned dictionary.
02 / 03

Observe nested conversion and independence

The useful difference from obj.__dict__ appears in nested data. An Order contains a Customer and a list of item strings. asdict turns Customer into another dictionary. It recursively handles dataclasses inside standard containers and deep-copies other contained objects. The exported list is separate from the original field list, which makes the mapping safer to adapt for a response.

Changing the result is not a way to update the original dataclass. In the code below, append a label to the exported items list and print both structures. The Order still holds only its original item. If you need to persist a change, modify the model intentionally and then export a fresh snapshot. Think of asdict as a data boundary rather than a live view.

PYTHON
from dataclasses import dataclass, asdict
@dataclass
class Customer:
    name: str
@dataclass
class Order:
    customer: Customer
    items: list[str]
order = Order(Customer('Ada'), ['book'])
record = asdict(order)
record['items'].append('pen')
print(record)
print(order.items)
EXPECTED OUTPUT
{'customer': {'name': 'Ada'}, 'items': ['book', 'pen']}
['book']
Follow the reasoning
  1. Convert a parent dataclass with a nested child.
  2. Mutate the exported list and verify the original is unchanged.
03 / 03

Choose shallow mapping and JSON separately

The documentation also shows a shallow alternative: iterate fields(obj) and use getattr(obj, field.name). This produces a dictionary of field names while retaining references to the original values. A nested Customer remains a Customer object, and a mutable list remains shared. That can be appropriate inside a controlled program, but it is a different contract from asdict. Pick by desired behavior, not by which expression is shorter.

JSON is another boundary. json.dumps(asdict(order)) works only when every resulting value is JSON-compatible. A date, datetime, Decimal, or custom object may require an explicit conversion policy. Avoid describing dataclass-to-dict as automatic JSON serialization. First make the Python mapping correct, then decide the wire format and any special-value rules.

PYTHON
from dataclasses import dataclass, fields
@dataclass
class Customer:
    name: str
@dataclass
class Order:
    customer: Customer
order = Order(Customer('Ada'))
shallow = {f.name: getattr(order, f.name) for f in fields(order)}
print(type(shallow['customer']).__name__)
print(shallow['customer'] is order.customer)
EXPECTED OUTPUT
Customer
True
Follow the reasoning
  1. Build a shallow mapping with fields and getattr.
  2. Observe that a nested dataclass remains an object.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnChange export_order(order) to return a recursively converted, independent dictionary using dataclasses.asdict. The prepared call should print {'customer': {'name': 'Ada'}, 'items': ['book']}.
Need a hint?

The imported asdict function accepts a dataclass instance. Return asdict(order).

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.What happens to a nested dataclass when asdict converts its parent?

    02.Does asdict itself produce JSON?

    03.Which expression builds a shallow mapping of declared fields?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Using __dict__ as recursive conversion

    COMMON MISTAKE
    order.__dict__
    FIX
    Use asdict(order) for recursive dataclass conversion.

    What happens: Nested Customer is still a Customer object and slotted dataclasses may lack __dict__.
    asdict follows declared fields and converts nested dataclasses.

    Assuming JSON compatibility

    COMMON MISTAKE
    json.dumps(asdict(record_with_datetime))
    FIX
    Convert special values explicitly before json.dumps, or supply a deliberate serializer.

    What happens: JSON encoding raises TypeError for unsupported values.
    A Python dict is not automatically valid JSON data.

    Browse this lesson’s problem fixes

    Sources and further reading