https://github.com/python/cpython/commit/902de283a8303177eb95bf5bc252d2421fcbd758
commit: 902de283a8303177eb95bf5bc252d2421fcbd758
branch: 3.14
author: Miss Islington (bot) <31488909+miss-isling...@users.noreply.github.com>
committer: hugovk <1324225+hug...@users.noreply.github.com>
date: 2025-08-13T14:13:14Z
summary:

[3.14] gh-137226: Fix behavior of ForwardRef.evaluate with type_params 
(GH-137227) (#137709)

Co-authored-by: Jelle Zijlstra <jelle.zijls...@gmail.com>

files:
A Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py
M Lib/typing.py

diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index c83a1573ccd3d1..bee019cd51591e 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -158,21 +158,13 @@ def evaluate(
             # as a way of emulating annotation scopes when calling `eval()`
             type_params = getattr(owner, "__type_params__", None)
 
-        # type parameters require some special handling,
-        # as they exist in their own scope
-        # but `eval()` does not have a dedicated parameter for that scope.
-        # For classes, names in type parameter scopes should override
-        # names in the global scope (which here are called `localns`!),
-        # but should in turn be overridden by names in the class scope
-        # (which here are called `globalns`!)
+        # Type parameters exist in their own scope, which is logically
+        # between the locals and the globals. We simulate this by adding
+        # them to the globals.
         if type_params is not None:
             globals = dict(globals)
-            locals = dict(locals)
             for param in type_params:
-                param_name = param.__name__
-                if not self.__forward_is_class__ or param_name not in globals:
-                    globals[param_name] = param
-                    locals.pop(param_name, None)
+                globals[param.__name__] = param
         if self.__extra_names__:
             locals = {**locals, **self.__extra_names__}
 
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index ae0e73f08c5bd0..88e0d611647f28 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -1365,6 +1365,11 @@ def test_annotations_to_string(self):
 class A:
     pass
 
+TypeParamsAlias1 = int
+
+class TypeParamsSample[TypeParamsAlias1, TypeParamsAlias2]:
+    TypeParamsAlias2 = str
+
 
 class TestForwardRefClass(unittest.TestCase):
     def test_forwardref_instance_type_error(self):
@@ -1597,6 +1602,21 @@ class Gen[T]:
             ForwardRef("alias").evaluate(owner=Gen, locals={"alias": str}), str
         )
 
+    def test_evaluate_with_type_params_and_scope_conflict(self):
+        for is_class in (False, True):
+            with self.subTest(is_class=is_class):
+                fwdref1 = ForwardRef("TypeParamsAlias1", 
owner=TypeParamsSample, is_class=is_class)
+                fwdref2 = ForwardRef("TypeParamsAlias2", 
owner=TypeParamsSample, is_class=is_class)
+
+                self.assertIs(
+                    fwdref1.evaluate(),
+                    TypeParamsSample.__type_params__[0],
+                )
+                self.assertIs(
+                    fwdref2.evaluate(),
+                    TypeParamsSample.TypeParamsAlias2,
+                )
+
     def test_fwdref_with_module(self):
         self.assertIs(ForwardRef("Format", module="annotationlib").evaluate(), 
Format)
         self.assertIs(
diff --git a/Lib/typing.py b/Lib/typing.py
index 09a574d783c8b4..d4a79ed5e35295 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -2340,10 +2340,13 @@ def get_type_hints(obj, globalns=None, localns=None, 
include_extras=False,
                 # *base_globals* first rather than *base_locals*.
                 # This only affects ForwardRefs.
                 base_globals, base_locals = base_locals, base_globals
+            type_params = base.__type_params__
+            base_globals, base_locals = _add_type_params_to_scope(
+                type_params, base_globals, base_locals, True)
             for name, value in ann.items():
                 if isinstance(value, str):
                     value = _make_forward_ref(value, is_argument=False, 
is_class=True)
-                value = _eval_type(value, base_globals, base_locals, 
base.__type_params__,
+                value = _eval_type(value, base_globals, base_locals, (),
                                    format=format, owner=obj)
                 if value is None:
                     value = type(None)
@@ -2379,6 +2382,7 @@ def get_type_hints(obj, globalns=None, localns=None, 
include_extras=False,
     elif localns is None:
         localns = globalns
     type_params = getattr(obj, "__type_params__", ())
+    globalns, localns = _add_type_params_to_scope(type_params, globalns, 
localns, False)
     for name, value in hints.items():
         if isinstance(value, str):
             # class-level forward refs were handled above, this must be either
@@ -2388,13 +2392,27 @@ 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, type_params, 
format=format, owner=obj)
+        value = _eval_type(value, globalns, localns, (), format=format, 
owner=obj)
         if value is None:
             value = type(None)
         hints[name] = value
     return hints if include_extras else {k: _strip_annotations(t) for k, t in 
hints.items()}
 
 
+# Add type parameters to the globals and locals scope. This is needed for
+# compatibility.
+def _add_type_params_to_scope(type_params, globalns, localns, is_class):
+    if not type_params:
+        return globalns, localns
+    globalns = dict(globalns)
+    localns = dict(localns)
+    for param in type_params:
+        if not is_class or param.__name__ not in globalns:
+            globalns[param.__name__] = param
+            localns.pop(param.__name__, None)
+    return globalns, localns
+
+
 def _strip_annotations(t):
     """Strip the annotations from a given type."""
     if isinstance(t, _AnnotatedAlias):
diff --git 
a/Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst 
b/Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst
new file mode 100644
index 00000000000000..522943cdd376dc
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-07-29-21-18-31.gh-issue-137226.B_4lpu.rst
@@ -0,0 +1,3 @@
+Fix behavior of :meth:`annotationlib.ForwardRef.evaluate` when the
+*type_params* parameter is passed and the name of a type param is also
+present in an enclosing scope.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: arch...@mail-archive.com

Reply via email to