Skip to main content

Lib/dataclasses.py (part 3)

Source:

cpython 3.14 @ ab2d84fe1023/Lib/dataclasses.py

This annotation covers method generation and advanced features. See lib_dataclasses2_detail for the @dataclass decorator, field(), and __post_init__.

Map

LinesSymbolRole
1-100_init_fnGenerate __init__ source and exec it
101-200_repr_fnGenerate __repr__
201-300_eq_fn / _hash_fnEquality and hash generation
301-400Frozen dataclassesRaise FrozenInstanceError on mutation
401-600slots=TrueRe-create the class with __slots__

Reading

_init_fn

# CPython: Lib/dataclasses.py:480 _init_fn
def _init_fn(fields, frozen, has_post_init, self_name, globals):
seen_default = False
body_lines = []
for f in fields:
if not f.init:
continue
if f.default is MISSING and f.default_factory is MISSING:
if seen_default:
raise TypeError(f'non-default argument {f.name!r} follows default argument')
else:
seen_default = True
body_lines += _field_init(f, frozen, has_post_init, self_name)
if has_post_init:
body_lines.append(f' {self_name}.{_POST_INIT_NAME}({_POST_INIT_PARAMS})')
return _create_fn('__init__',
[self_name] + [_init_param(f) for f in fields if f.init],
body_lines, globals=globals, locals=None,
return_type=None)

_init_fn builds Python source code as a string and calls exec on it. The generated __init__ has the correct default values as closures in globals. This approach allows correct repr of the generated code in tracebacks.

_create_fn

# CPython: Lib/dataclasses.py:420 _create_fn
def _create_fn(name, args, body, *, globals=None, locals=None,
return_type=MISSING):
if locals is None:
locals = {}
if return_type is not MISSING:
locals['_return_type'] = return_type
return_annotation = '->_return_type'
else:
return_annotation = ''
args = ','.join(args)
body = '\n'.join(f' {b}' for b in body)
txt = f' def {name}({args}){return_annotation}:\n{body}'
ns = {}
exec(txt, globals, ns)
return ns[name]

exec in a controlled namespace is the core mechanism. The generated function has access to defaults, factory functions, and type annotations via globals. This is why dataclass-generated __init__ shows correct source in inspect.getsource-like contexts.

Frozen dataclasses

# CPython: Lib/dataclasses.py:620 _frozen_setattr
def _frozen_setattr(self, name, value):
if type(self) is cls or type(self) in cls.__subclasses__():
raise FrozenInstanceError('cannot assign to field %r' % name)
object.__setattr__(self, name, value)

def _frozen_delattr(self, name):
if type(self) is cls or type(self) in cls.__subclasses__():
raise FrozenInstanceError('cannot delete field %r' % name)
object.__delattr__(self, name)

@dataclass(frozen=True) installs __setattr__ and __delattr__ that raise FrozenInstanceError. The __init__ uses object.__setattr__ directly to bypass this check during initialization. Frozen dataclasses are hashable by default.

slots=True

# CPython: Lib/dataclasses.py:980 _process_class with slots
def _add_slots(cls, is_frozen, has_weakref_slot):
# Need to create a new class with __slots__ since slots must be defined at class creation time
cls_dict = {}
for key, value in cls.__dict__.items():
if key not in ('__dict__', '__weakref__'):
cls_dict[key] = value
slots = [f.name for f in fields(cls)]
if has_weakref_slot:
slots.append('__weakref__')
cls_dict['__slots__'] = tuple(slots)
qualname = cls.__qualname__
new_cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
new_cls.__qualname__ = qualname
return new_cls

slots=True (Python 3.10+) requires re-creating the class because __slots__ must be specified at class creation. The original class dict is copied to a new class with __slots__ added. This eliminates the per-instance __dict__, reducing memory and improving attribute access speed.

gopy notes

_init_fn generates code as a string and execs it. In gopy, dataclasses uses the pure-Python implementation from stdlib (shipped via stdlib/dataclasses.py). The exec-based method generation works through gopy's exec support in vm/eval_import.go. FrozenInstanceError is a subclass of AttributeError.