Skip to content

← Data Modelling and Invariants step 11 of 25

Easy Primitives

@dataclass is a code generator: what it emits

@dataclass is not a class system. It is a code generator that runs once, at class-creation time, reads your class body, and writes __init__, __repr__, __eq__ and friends into the class. Almost every senior-review comment about dataclasses follows from forgetting that one sentence.

What it reads — and what it silently ignores

The generator walks annotations, not assignments. So this:

@dataclass
class Config:
    timeout: float = 5.0
    retries = 3          # <- NOT a field

gives you a class where retries is ordinary shared class state. It is absent from fields(), absent from asdict(), absent from replace(), absent from __eq__, absent from __init__. Config(timeout=1.0).retries reads 3 from the class; cfg.retries = 5 creates an instance attribute that shadows it for that object only, and every other object still sees 3. Under mypy --strict this produces no diagnostic at all — the annotation is what makes something a field, and there is no rule saying a class body may not also contain plain attributes.

Field order comes from reverse MRO

Fields are collected by walking cls.__mro__ in reverse (object first, the class itself last) and merging each class’s own annotations into one ordered table. For a diamond class NC(NA, NB) the MRO is NC, NA, NB, NBase, object, so reversed it is object, NBase, NB, NA, NC — and the field order is b, a, c, not a, b, c. Since field order is __init__ parameter order, positional construction of a diamond subclass is a trap.

A redeclared inherited field is merged into the entry that already exists, so it changes the annotation and the default but keeps the base class’s position. (mypy does catch a redeclaration that changes the type — that one is an assignment error — but it says nothing about the position.)

Four kinds of annotated entry

declaration in fields() in __init__ stored
x: int yes yes yes
x: ClassVar[int] no no class attribute
x: InitVar[int] no yes no — forwarded to __post_init__
_: KW_ONLY no no nothing; a marker

ClassVar is how you say “class-level constant, not a field”. InitVar is how you say “constructor parameter that is consumed, not kept”. KW_ONLY is a pseudo-field: everything declared after it becomes keyword-only, and its name is ignored (_ by convention).

The __eq__ it emits

Read the generated method: it begins

if other.__class__ is self.__class__:
    return (self.a, self.b) == (other.a, other.b)
return NotImplemented

is, not isinstance. A dataclass never compares equal to its own subclass — which is exactly the safety that NamedTuple does not give you (there, Point(1, 2) == (1, 2) is True).


Your task

The module already defines several dataclasses. Implement:

def field_signature(cls: type[DataclassInstance]) -> list[tuple[str, str]]:

It returns one (name, kind) tuple per annotated class-body entry, in the order the dataclass machinery assembles them, where kind is one of:

  • "field" — a real field (it is in dataclasses.fields(cls))
  • "classvar" — annotated ClassVar[...] (or bare ClassVar)
  • "initvar" — annotated InitVar[...] (or bare InitVar)
  • "pseudo" — the KW_ONLY sentinel

solve(target) looks the class up in REGISTRY and returns that list.

Constraints. Use dataclasses.fields() to decide what is a real field — never string-parse the source. To see the entries fields() omits, walk cls.__mro__ in reverse and read each class’s own annotations (inspect.get_annotations(base) gives you exactly that, and it works both before and after PEP 649). Merging into a dict gives you the redeclaration rule for free: assigning to an existing key updates the value and keeps the original insertion position.

Returning a list of lists instead of a list of tuples fails. The container type is part of the answer.

Types. fields() is stubbed as taking DataclassInstance | type[DataclassInstance], and _typeshed.DataclassInstance is importable only under TYPE_CHECKING — hence the quoted annotation on REGISTRY. That import dance is the standard price of writing generic code over dataclasses; you will meet it again.