Description
When a TypedDict inherits from a parametrized generic TypedDict, the type argument is dropped. The inherited field keeps the type parameter instead of the type it was parametrized with, so values that should be rejected are accepted.
from typing import Generic, TypedDict, TypeVar
import msgspec
L = TypeVar("L")
class Base(TypedDict, Generic[L]):
b: L
class Sub(Base[int]):
n: str
print(msgspec.json.decode(b'{"b": "not an int", "n": "a"}', type=Sub))
I expected a ValidationError for b, because Base[int] declares it as int. What I get is:
{'b': 'not an int', 'n': 'a'}
The same shape built on a Struct, a dataclass or an attrs class raises ValidationError: Expected `int`, got `str` - at `$.b` , so this looks specific to TypedDict. Decoding Base[int] directly, without the subclass, also rejects the value correctly, so the inheritance step is what drops the argument.
The PEP 695 spelling behaves the same way, and under from __future__ import annotations it fails with a NameError instead:
from __future__ import annotations
from typing import TypedDict
import msgspec
class Base[Q](TypedDict):
b: Q
class Sub(Base[int]):
n: str
msgspec.json.decode(b'{"b": 1, "n": "a"}', type=Sub)
Traceback (most recent call last):
File "mcve.py", line 13, in <module>
msgspec.json.decode(b'{"b": 1, "n": "a"}', type=Sub)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../msgspec/_utils.py", line 241, in get_typeddict_info
raw_hints = get_class_annotations(obj)
File ".../msgspec/_utils.py", line 193, in get_class_annotations
value = _eval_type(value, cls_locals, cls_globals)
File ".../msgspec/_utils.py", line 49, in _eval_type
return typing._eval_type(t, globalns, localns, ())
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../python3.14/typing.py", line 461, in _eval_type
return evaluate_forward_ref(t, globals=globalns, locals=localns,
type_params=type_params, owner=owner,
_recursive_guard=recursive_guard, format=format)
File ".../python3.14/typing.py", line 1000, in evaluate_forward_ref
value = forward_ref.evaluate(globals=globals, locals=locals,
type_params=type_params, owner=owner, format=format)
File ".../python3.14/annotationlib.py", line 203, in evaluate
raise NameError(_NAME_ERROR_MSG.format(name=arg), name=arg)
NameError: name 'Q' is not defined
A subclass that stays generic is affected too when written with PEP 695 syntax: for class Sub[Q](Base[Q]), decoding with type=Sub[int] accepts a string for b. The TypeVar equivalent of that one does substitute correctly.
get_class_annotations substitutes correctly for an ordinary class of the same shape, which test_generic_sub11 already covers, and returns the unsubstituted parameter only for the TypedDict equivalent:
ordinary class: {'y': <class 'float'>, 'x': <class 'int'>}
TypedDict: {'x': ~T, 'y': <class 'float'>}
Reproduced on msgspec 0.21.1 and on current main (0.21.2.dev98+g74901110c), CPython 3.12.13, 3.13.14 and 3.14.6. This is unrelated to #1130: the same results appear with that PR applied.
Description
When a
TypedDictinherits from a parametrized genericTypedDict, the type argument is dropped. The inherited field keeps the type parameter instead of the type it was parametrized with, so values that should be rejected are accepted.I expected a
ValidationErrorforb, becauseBase[int]declares it asint. What I get is:The same shape built on a
Struct, adataclassor anattrsclass raisesValidationError: Expected `int`, got `str` - at `$.b`, so this looks specific toTypedDict. DecodingBase[int]directly, without the subclass, also rejects the value correctly, so the inheritance step is what drops the argument.The PEP 695 spelling behaves the same way, and under
from __future__ import annotationsit fails with aNameErrorinstead:A subclass that stays generic is affected too when written with PEP 695 syntax: for
class Sub[Q](Base[Q]), decoding withtype=Sub[int]accepts a string forb. TheTypeVarequivalent of that one does substitute correctly.get_class_annotationssubstitutes correctly for an ordinary class of the same shape, whichtest_generic_sub11already covers, and returns the unsubstituted parameter only for theTypedDictequivalent:Reproduced on msgspec 0.21.1 and on current main (
0.21.2.dev98+g74901110c), CPython 3.12.13, 3.13.14 and 3.14.6. This is unrelated to #1130: the same results appear with that PR applied.