How @dataclass generates code

dataclasses · Expert · 13 min

Understanding the generated code explains performance, pattern matching, and how dataclass-like libraries work.

@dataclass runs once, at class creation. It reads the class's __annotations__ in order, builds a Field object for each and stores them in __dataclass_fields__ (with options in __dataclass_params__). Then it generates source code for methods such as __init__, __repr__ and __eq__ as text, compiles it with exec, and attaches the functions to the class, unless the class already defines them (for __init__, __repr__, __eq__) or raises TypeError on conflict (ordering methods with order=True).

So the generated __init__ is ordinary Python, as fast as one you'd write by hand. There's no per-instance overhead beyond normal attribute storage; the cost is paid once at import.

Other generated pieces:

  • __match_args__: a tuple of positional field names, enabling case Point(x, y): in structural pattern matching (match_args=False turns it off).
  • __slots__ or __weakref__ with the corresponding options.
  • __replace__ (3.13), used by copy.replace().

make_dataclass("Point", [("x", float), ("y", float, 0.0)]) builds a dataclass dynamically, useful when the fields come from a schema.

Descriptor-typed fields: if a field's default is a descriptor (with __get__/__set__), __init__ assigns through it, which lets you add per-field validation or conversion while keeping dataclass ergonomics.

Example

import inspect
from dataclasses import dataclass, make_dataclass

@dataclass
class Point:
    x: float
    y: float = 0.0

print(list(Point.__dataclass_fields__))       # ['x', 'y']
print(inspect.signature(Point))               # (x: float, y: float = 0.0) -> None
print(Point.__match_args__)                   # ('x', 'y')
print(Point.__init__.__qualname__)            # Point.__init__  (generated with exec)

def describe(obj):
    match obj:
        case Point(0, 0): return "origin"
        case Point(x, 0): return f"on the x-axis at {x}"
        case Point(): return "somewhere"

print(describe(Point(0, 0)), "|", describe(Point(5)))   # origin | on the x-axis at 5

Color = make_dataclass("Color", [("r", int), ("g", int), ("b", int, 0)], frozen=True)
print(Color(255, 128), hasattr(Color, "__replace__"))   # Color(r=255, g=128, b=0) True

Remember

  • The decorator stores Field objects in __dataclass_fields__ and exec-generates methods
  • Generated methods are plain Python: cost is paid once at class creation
  • __match_args__ enables positional patterns; make_dataclass builds classes dynamically

Watch out for

  • Expecting runtime type checks from the generated __init__; it only assigns values.

Check yourself

  1. Why is a dataclass's __init__ as fast as a hand-written one?

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.make_dataclass