Skip to content

Type argument of a parametrized generic TypedDict base is ignored when subclassed #1191

Description

@Siyet

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions