We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Data Modelling and Invariants step 11 of 25
@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 indataclasses.fields(cls)) -
"classvar"— annotatedClassVar[...](or bareClassVar) -
"initvar"— annotatedInitVar[...](or bareInitVar) -
"pseudo"— theKW_ONLYsentinel
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.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.