We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modelling Data with Dataclasses step 27 of 28
Resolve Dataclass Field Types in Python 3.14
This exercise needs Python 3.14. The detail it covers matters most to tools that read field types, like the codec in Build a JSON Codec for Dataclasses.
A tool that reads f.type expects a class such as int. In Python 3.14
it can also find a string or a ForwardRef. Which one depends on how you
wrote the annotation, and on whether its names existed when @dataclass
ran:
from dataclasses import dataclass, fields
@dataclass
class Forward:
c: Later
d: "Later"
class Later:
pass
print([type(f.type).__name__ for f in fields(Forward)]) # ['ForwardRef', 'str']
Python 3.14 evaluates an annotation only when code asks for it. When
@dataclass asks, Later does not exist yet, so the decorator stores a
forward reference, a ForwardRef object that holds the name
"Later". A quoted annotation stays a string. Defining Later
afterwards does not change what the dataclass stored, so your resolver
evaluates the names itself, once Later exists.
What to write
Fill in resolve_field_types(cls) -> dict[str, str]. It maps each field
name to a display string for the field's type.
- Walk
dataclasses.fields(cls), so the result has exactly the dataclass's fields. - Build the namespace by copying this module's
globals(), then updating the copy withvars(cls). A name defined in the class overrides a global one. - Use a
matchon the annotation. A class,case type(), stays as it is, withouteval. A string,case str(), goes toevalwith the namespace. For aForwardRef,case ForwardRef(), evaluate its__forward_arg__string the same way. - Pass each result to the supplied
_display. It turns a class into its__name__and any other object intostr(object). - Do not change the class or its
Fieldobjects.
If eval raises NameError, raise UnresolvedField(field_name, missing)
from it, so the NameError becomes the cause. missing is exc.name,
or the source string if exc.name is None. Let any other error
propagate. solve catches UnresolvedField and returns
{"__error__": "UnresolvedField:<field>:<missing>"}.
Expected results
| Target | What it shows | Result |
|---|---|---|
Plain |
Annotations that are classes | {"a": "int", "b": "str"} |
Stringy |
Quoted generic and union types | {"b": "list[str]", "c": "int | None"} |
Forward |
A forward reference and a quoted name, both for a later class | {"c": "Later", "d": "Later"} |
AllStrings |
Quoted simple and generic types | {"x": "int", "y": "dict[str, int]"} |
Broken |
A name that exists only for mypy | {"__error__": "UnresolvedField:d:NeverDefined"} |
Aliased |
A ClassVar alias that the decorator did not recognize |
{"n": "typing.ClassVar[int]"} |
Why Aliased has a field n
Aliased defines CV = ClassVar in its class body and annotates n with
the string "CV[int]". The decorator recognizes a quoted ClassVar only
through names it finds in the class's module, and CV is not there. So
n becomes a field. Your resolver finds CV through vars(cls) and
shows typing.ClassVar[int]. Still report n, because resolving the
annotation now does not change the fields that the decorator made. The
starter builds Aliased with type(), because mypy rejects CV[int] in
a class body written out by hand.
Type checking
cast("type[object] | str | ForwardRef", f.type)tells mypy which three forms to expect. End thematchwithcase _: assert_never(annotation), so mypy reports a missing case if the union grows.- A cast checks nothing at runtime. If
f.typeholds another kind of object, such as an evaluatedlist[str], the match reachesassert_never, which raisesAssertionError. DataclassInstanceis a type that exists only for the type checker and means "any dataclass instance". That is why the starter imports it underif TYPE_CHECKING:and writes it in quotes.
Going further
- For a real tool, use
typing.get_type_hintsor theannotationlibmodule that Python 3.14 added. They also handle inherited fields, other modules and quoted names nested inside types, which this resolver does not. evalruns code. Evaluate annotations only from code you trust.- With
from __future__ import annotationsat the top of a module, Python stores every annotation in that module as a string.
See annotationlib and get_type_hints for the standard tools.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.