https://github.com/python/cpython/commit/6d6aba252301cdf9d5ae3189629e1e43101dd58f
commit: 6d6aba252301cdf9d5ae3189629e1e43101dd58f
branch: main
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-09-16T07:28:39-07:00
summary:
gh-137226: Fix get_type_hints() on generic TypedDict with stringified
annotations (#138953)
This issue appears specifically for TypedDicts because the TypedDict constructor
code converts string annotations to ForwardRef objects, and those are not
evaluated
properly by the get_type_hints() stack because of other shenanigans with type
parameters.
This issue does not affect normal generic classes because their annotations are
not
pre-converted to ForwardRefs.
The fix attempts to restore the pre- #137227 behavior in the narrow scenario
where
the issue manifests. It mostly makes changes only in the paths accessible from
get_type_hints(),
ensuring that newer APIs (such as evaluate_forward_ref() and annotationlib) are
not affected
by get_type_hints()'s past odd choices. This PR does not fix issue #138949, an
older issue I
discovered while playing around with this one; we'll need a separate and
perhaps more
invasive fix for that, but it should wait until after 3.14.0.
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 8238c62f0715f8..d776a019795582 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -7172,6 +7172,19 @@ def func(x: MyClass['int'], y: MyClass[Annotated[int,
...]]): ...
assert isinstance(get_type_hints(func)['x'], MyAlias)
assert isinstance(get_type_hints(func)['y'], MyAlias)
+ 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):
@@ -8657,8 +8670,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 babe3c44d9dc55..0554343c8e3a0e 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -171,16 +171,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
@@ -198,7 +198,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")
@@ -454,7 +454,7 @@ def
_deprecation_warning_for_no_type_params_passed(funcname: str) -> None:
def _eval_type(t, globalns, localns, type_params, *,
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().
@@ -464,8 +464,20 @@ def _eval_type(t, globalns, localns, type_params, *,
recursive_guard=frozenset()
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)
@@ -481,7 +493,7 @@ def _eval_type(t, globalns, localns, type_params, *,
recursive_guard=frozenset()
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 args
)
@@ -2369,7 +2381,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
@@ -2414,7 +2426,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
@@ -3111,7 +3123,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]