Skip to content

← Modelling Data with Dataclasses step 7 of 28

Easy Primitives

Tell Fields Apart from Class Variables

Your team documents its settings classes with a script that lists every annotated name in a dataclass. Not every annotated name is a field. One belongs to the class, one is only a constructor argument, and one is a marker:

from dataclasses import KW_ONLY, InitVar, dataclass, fields
from typing import ClassVar


@dataclass
class Config:
    timeout: float = 5.0
    retries = 3
    version: ClassVar[str] = "1.0"
    seed: InitVar[int] = 0
    _: KW_ONLY
    debug: bool = False


print([f.name for f in fields(Config)])  # ['timeout', 'debug']

Write a report that lists every annotated name with its kind:

solve("Config")
# [
#     ("timeout", "field"),
#     ("version", "classvar"),
#     ("seed", "initvar"),
#     ("_", "pseudo"),
#     ("debug", "field"),
# ]

Four kinds of annotated name

  • A field is a value that each object stores. The dataclass puts it in __init__, the repr and ==.
  • ClassVar[str] marks a class variable, a value that belongs to the class and that all instances share, such as a constant or a counter. The dataclass leaves it out of __init__, the repr and ==.
  • InitVar[int] marks an argument that __init__ passes to __post_init__ and does not store. The exercise Compute Cents from an Amount with post_init used one.
  • KW_ONLY is the marker that makes the fields after it keyword-only. You met it in the exercise Fix a Dataclass Hierarchy with Keyword-Only Fields. It is not a field.

retries = 3 has no annotation, so it is a plain class attribute, and the report leaves it out.

What to write

Fill in field_signature(cls), which returns a list of (name, kind) tuples:

  1. Get the real field names from fields(cls).
  2. Walk reversed(cls.__mro__). cls.__mro__ lists the class and its bases in the order Python searches them, so the reversed walk starts at object and ends at cls.
  3. For each class, read its own annotations with inspect.get_annotations.
  4. Classify each name with the table below, and store its kind in a dictionary. If a later class declares a name again, replace its kind but keep its first position.
  5. Return the dictionary's items as a list of tuples.

Check the rows in this order, and use the first one that matches:

Annotation or name Kind
The annotation is KW_ONLY "pseudo"
It is InitVar itself, or an instance of InitVar "initvar"
It is ClassVar itself, or get_origin(annotation) is ClassVar "classvar"
The name is one of the real field names "field"
None of the above "other"

The test classes use the first four kinds. Do not parse source code, and do not create instances. Every annotation on the test classes is a real object, not a string, so you do not need to resolve strings. Keep solve and the classes as they are, and return tuples, not lists.

Follow the merge order

NC inherits from NA and NB, so its MRO is NC, NA, NB, NBase, object. The reversed walk meets NB's b before NA's a, then NC's c:

solve("NC")
# [("b", "field"), ("a", "field"), ("c", "field")]

Sub declares Base.x again with a new default. The report still puts x first, then y and z, because assigning to an existing dictionary key keeps its position.

Your code must pass mypy --strict. DataclassInstance is a type that exists only for the type checker and means "any dataclass instance". That is why the starter imports it under if TYPE_CHECKING: and writes it in quotes. Keep both as they are.