https://github.com/python/cpython/commit/6038447d99663b0434a4f2c545c0543e9a81fdc3
commit: 6038447d99663b0434a4f2c545c0543e9a81fdc3
branch: 3.14
author: Jelle Zijlstra <[email protected]>
committer: hugovk <[email protected]>
date: 2025-09-17T12:22:16+03:00
summary:

[3.14] gh-137226: Fix get_type_hints() on generic TypedDict with stringified 
annotations (GH-138953) (#138989)

files:
A Misc/NEWS.d/next/Library/2025-09-15-13-09-19.gh-issue-137226.HH3_ik.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 889deddc56cd3f..f19044bbcdeeae 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -7132,6 +7132,19 @@ def add_right(self, node: 'Node[T]' = None):
         right_hints = get_type_hints(t.add_right, globals(), locals())
         self.assertEqual(right_hints['node'], Node[T])
 
+    def test_stringified_typeddict(self):
+        ns = run_code(
+            """
+            from __future__ import annotations
+            from typing import TypedDict
+            class TD[UniqueT](TypedDict):
+                a: UniqueT
+            """
+        )
+        TD = ns['TD']
+        self.assertEqual(TD.__annotations__, {'a': 
EqualToForwardRef('UniqueT', owner=TD, module=TD.__module__)})
+        self.assertEqual(get_type_hints(TD), {'a': TD.__type_params__[0]})
+
 
 class GetUtilitiesTestCase(TestCase):
     def test_get_origin(self):
@@ -8678,8 +8691,8 @@ def _make_td(future, class_name, annos, base, 
extra_names=None):
                     child = _make_td(
                         child_future, "Child", {"child": "int"}, "Base", 
{"Base": base}
                     )
-                    base_anno = ForwardRef("int", module="builtins") if 
base_future else int
-                    child_anno = ForwardRef("int", module="builtins") if 
child_future else int
+                    base_anno = ForwardRef("int", module="builtins", 
owner=base) if base_future else int
+                    child_anno = ForwardRef("int", module="builtins", 
owner=child) if child_future else int
                     self.assertEqual(base.__annotations__, {'base': base_anno})
                     self.assertEqual(
                         child.__annotations__, {'child': child_anno, 'base': 
base_anno}
diff --git a/Lib/typing.py b/Lib/typing.py
index da5e0b56534045..dbc8734418d9a9 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -172,16 +172,16 @@ def __getattr__(self, attr):
 _lazy_annotationlib = _LazyAnnotationLib()
 
 
-def _type_convert(arg, module=None, *, allow_special_forms=False):
+def _type_convert(arg, module=None, *, allow_special_forms=False, owner=None):
     """For converting None to type(None), and strings to ForwardRef."""
     if arg is None:
         return type(None)
     if isinstance(arg, str):
-        return _make_forward_ref(arg, module=module, 
is_class=allow_special_forms)
+        return _make_forward_ref(arg, module=module, 
is_class=allow_special_forms, owner=owner)
     return arg
 
 
-def _type_check(arg, msg, is_argument=True, module=None, *, 
allow_special_forms=False):
+def _type_check(arg, msg, is_argument=True, module=None, *, 
allow_special_forms=False, owner=None):
     """Check that the argument is a type, and return it (internal helper).
 
     As a special case, accept None and return type(None) instead. Also wrap 
strings
@@ -199,7 +199,7 @@ def _type_check(arg, msg, is_argument=True, module=None, *, 
allow_special_forms=
         if is_argument:
             invalid_generic_forms += (Final,)
 
-    arg = _type_convert(arg, module=module, 
allow_special_forms=allow_special_forms)
+    arg = _type_convert(arg, module=module, 
allow_special_forms=allow_special_forms, owner=owner)
     if (isinstance(arg, _GenericAlias) and
             arg.__origin__ in invalid_generic_forms):
         raise TypeError(f"{arg} is not valid as type argument")
@@ -431,7 +431,7 @@ def __repr__(self):
 
 
 def _eval_type(t, globalns, localns, type_params=_sentinel, *, 
recursive_guard=frozenset(),
-               format=None, owner=None, parent_fwdref=None):
+               format=None, owner=None, parent_fwdref=None, 
prefer_fwd_module=False):
     """Evaluate all forward references in the given type t.
 
     For use of globalns and localns see the docstring for get_type_hints().
@@ -444,8 +444,20 @@ def _eval_type(t, globalns, localns, 
type_params=_sentinel, *, recursive_guard=f
     if isinstance(t, _lazy_annotationlib.ForwardRef):
         # If the forward_ref has __forward_module__ set, evaluate() infers the 
globals
         # from the module, and it will probably pick better than the globals 
we have here.
-        if t.__forward_module__ is not None:
+        # We do this only for calls from get_type_hints() (which opts in 
through the
+        # 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):
+                globalns = getattr(
+                    sys.modules.get(t.__forward_module__, None), "__dict__", 
None
+                )
+                if globalns is not None:
+                    globalns = dict(globalns)
+                    for type_param in owner_type_params:
+                        globalns[type_param.__name__] = type_param
         return evaluate_forward_ref(t, globals=globalns, locals=localns,
                                     type_params=type_params, owner=owner,
                                     _recursive_guard=recursive_guard, 
format=format)
@@ -466,7 +478,7 @@ def _eval_type(t, globalns, localns, type_params=_sentinel, 
*, recursive_guard=f
         ev_args = tuple(
             _eval_type(
                 a, globalns, localns, type_params, 
recursive_guard=recursive_guard,
-                format=format, owner=owner,
+                format=format, owner=owner, 
prefer_fwd_module=prefer_fwd_module,
             )
             for a in t.__args__
         )
@@ -2363,7 +2375,7 @@ def get_type_hints(obj, globalns=None, localns=None, 
include_extras=False,
                 if isinstance(value, str):
                     value = _make_forward_ref(value, is_argument=False, 
is_class=True)
                 value = _eval_type(value, base_globals, base_locals, (),
-                                   format=format, owner=obj)
+                                   format=format, owner=obj, 
prefer_fwd_module=True)
                 if value is None:
                     value = type(None)
                 hints[name] = value
@@ -2408,7 +2420,7 @@ def get_type_hints(obj, globalns=None, localns=None, 
include_extras=False,
                 is_argument=not isinstance(obj, types.ModuleType),
                 is_class=False,
             )
-        value = _eval_type(value, globalns, localns, (), format=format, 
owner=obj)
+        value = _eval_type(value, globalns, localns, (), format=format, 
owner=obj, prefer_fwd_module=True)
         if value is None:
             value = type(None)
         hints[name] = value
@@ -3147,7 +3159,7 @@ def __new__(cls, name, bases, ns, total=True):
             own_annotations = {}
         msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
         own_checked_annotations = {
-            n: _type_check(tp, msg, module=tp_dict.__module__)
+            n: _type_check(tp, msg, owner=tp_dict, module=tp_dict.__module__)
             for n, tp in own_annotations.items()
         }
         required_keys = set()
diff --git 
a/Misc/NEWS.d/next/Library/2025-09-15-13-09-19.gh-issue-137226.HH3_ik.rst 
b/Misc/NEWS.d/next/Library/2025-09-15-13-09-19.gh-issue-137226.HH3_ik.rst
new file mode 100644
index 00000000000000..38683c845dec33
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-09-15-13-09-19.gh-issue-137226.HH3_ik.rst
@@ -0,0 +1,2 @@
+Fix :func:`typing.get_type_hints` calls on generic :class:`typing.TypedDict`
+classes defined with string annotations.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to