From fe2fc30a53ce5c2422bb13d273a57f51d469e017 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 2 Jun 2026 11:04:59 +0300 Subject: [PATCH] gh-138949: Fix non-generic children of generic TypedDicts with future annotations --- Lib/test/test_typing.py | 37 +++++++++++++++++++ Lib/test/typinganndata/ann_module695.py | 21 ++++++++++- Lib/typing.py | 30 +++++++++++++-- ...-06-02-10-38-48.gh-issue-138949.KI54me.rst | 2 + 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-02-10-38-48.gh-issue-138949.KI54me.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index ad644bb31288098..0aa04984c7260b8 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5364,6 +5364,43 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self): set(results.generic_func.__type_params__) ) + def test_pep695_generic_class_with_future_typed_dicts(self): + # gh-138949 + td1_hints = get_type_hints(ann_module695.TD1) + self.assertEqual(td1_hints, {'a': ann_module695.TD1.__type_params__[0]}) + + td2_hints = get_type_hints(ann_module695.TD2) # used to fail with `NameError` + self.assertEqual( + td2_hints, + {'a': ann_module695.TD1.__type_params__[0], 'b': int}, + ) + + td3_hints = get_type_hints(ann_module695.TD3) + self.assertEqual( + td3_hints, + { + 'a': ann_module695.TD1.__type_params__[0], + 'b': int, + 'c': ann_module695.TD3.__type_params__[0], + }, + ) + + td4_hints = get_type_hints(ann_module695.TD4) + self.assertEqual( + td4_hints, + { + # Type param `TD4.T` must have a higher precedence over `TD1.T`: + 'a': ann_module695.TD4.__type_params__[0], + 'b': int, + 'c': ann_module695.TD3.__type_params__[0], + 'd': ann_module695.TD4.__type_params__[0], + 'e': ann_module695.TD4.__type_params__[1], + }, + ) + + with self.assertRaisesRegex(NameError, "name 'T' is not defined"): + get_type_hints(ann_module695.TD1_2) + def test_extended_generic_rules_subclassing(self): class T1(Tuple[T, KT]): ... class T2(Tuple[T, ...]): ... diff --git a/Lib/test/typinganndata/ann_module695.py b/Lib/test/typinganndata/ann_module695.py index b6f3b06bd5065f5..4847fe1a3ba8d52 100644 --- a/Lib/test/typinganndata/ann_module695.py +++ b/Lib/test/typinganndata/ann_module695.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Callable +from typing import Callable, TypedDict class A[T, *Ts, **P]: @@ -70,3 +70,22 @@ def generic_function[Eggs, **Spam](x: Eggs, y: Spam): pass generic_func=generic_function, hints_for_generic_func=get_type_hints(generic_function) ) + +# gh-138949 +class TD1[T](TypedDict): + a: T + +class TD2(TD1): + b: int + +class TD3[CT](TD2): + c: CT + +class TD4[T, E](TD3): + d: T + e: E + +class TD1_2(TD1): + # This must raise a `NameError`, because `T` is only defined for a parent + # keys scope. + b: T diff --git a/Lib/typing.py b/Lib/typing.py index 715d08e0e1603e6..0f9f682a742aac2 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -449,9 +449,33 @@ def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset() # prefer_fwd_module flag), so that the default behavior remains more straightforward. if prefer_fwd_module and t.__forward_module__ is not None: globalns = None - # If there are type params on the owner, we need to add them back, because - # annotationlib won't. - if owner_type_params := getattr(owner, "__type_params__", None): + # # If there are type params on the owner, we need to add them back, because + # # annotationlib won't. + owner_type_params = getattr(owner, "__type_params__", ()) + # TypedDict classes copy the parent type annotations, but do not + # copy parent type params / mro. So, we need to collect them manually here. + if is_typeddict(owner): + owner_type_params = list(owner_type_params) + mro_stack = list(owner.__orig_bases__) + seen = {tp.__name__ for tp in owner_type_params} + while mro_stack: + typ = mro_stack.pop(0) + if is_typeddict(typ): + mro_stack.extend(typ.__orig_bases__) + if t not in typ.__annotations__.values(): + # We only copy __type_params__ for types that own + # this annotation. So, it won't be possible to use + # undeclared type parameters from parent types in children. + continue + + base_type_params = getattr(typ, "__type_params__", ()) + for btp in base_type_params: + if btp.__name__ in seen: + continue + owner_type_params.append(btp) + seen.add(btp.__name__) + owner_type_params = tuple(owner_type_params) + if owner_type_params: globalns = getattr( sys.modules.get(t.__forward_module__, None), "__dict__", None ) diff --git a/Misc/NEWS.d/next/Library/2026-06-02-10-38-48.gh-issue-138949.KI54me.rst b/Misc/NEWS.d/next/Library/2026-06-02-10-38-48.gh-issue-138949.KI54me.rst new file mode 100644 index 000000000000000..ae334acaad9f5ce --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-02-10-38-48.gh-issue-138949.KI54me.rst @@ -0,0 +1,2 @@ +Fix :exc:`NameError` in :func:`typing.get_type_hints` on non-generic +children of generic typed dicts with future annotations flag enabled.