Repository: python/typing_extensions

Issue:
test_invalid_special_forms fails on Python 3.14.0b2
```
[typing_extensions (main)]$ git rev-parse @
fadc1edbcfd942074007875007870c1df6acd4d0
[typing_extensions (main)]$ python3.14 --version
Python 3.14.0b2
[typing_extensions (main)]$ tox -e py314
py314: recreate env because python changed python_preference=None->'system'
py314: remove tox env folder .../typing_extensions/.tox/py314
py314: venv> /usr/bin/uv venv -p cpython3.14 --allow-existing --python-preference system .../typing_extensions/.tox/py314
.pkg: recreate env because python changed version_info=[3, 12, 6, 'final', 0]->[3, 13, 3, 'final', 0] | python_preference=None->'system'
.pkg: remove tox env folder .../typing_extensions/.tox/.pkg
.pkg: venv> /usr/bin/uv venv -p /usr/bin/python3 --allow-existing --python-preference system .../typing_extensions/.tox/.pkg
.pkg: install_requires> /usr/bin/uv pip install 'flit_core<4,>=3.11'
.pkg: _optional_hooks> python /usr/lib/python3.13/site-packages/pyproject_api/_backend.py True flit_core.buildapi
.pkg: get_requires_for_build_sdist> python /usr/lib/python3.13/site-packages/pyproject_api/_backend.py True flit_core.buildapi
.pkg: build_sdist> python /usr/lib/python3.13/site-packages/pyproject_api/_backend.py True flit_core.buildapi
py314: install_package> /usr/bin/uv pip install --reinstall --no-deps typing-extensions@.../typing_extensions/.tox/.tmp/package/6/typing_extensions-4.14.0rc1.tar.gz
py314: commands[0] .../typing_extensions/src> python -m unittest discover
................................................................................................................................................................................s......................................................................................................................................................................F...............................................s.................................s..........................................s.s...ss.....ssss.....s............................s............
======================================================================
FAIL: test_invalid_special_forms (test_typing_extensions.TestEvaluateForwardRefs.test_invalid_special_forms)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../typing_extensions/src/test_typing_extensions.py", line 9112, in test_invalid_special_forms
    with self.assertRaisesRegex(
         ~~~~~~~~~~~~~~~~~~~~~~^
        TypeError, r"Plain .*Protocol('>)? is not valid as type argument"
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ):
    ^
AssertionError: TypeError not raised

----------------------------------------------------------------------
Ran 532 tests in 0.141s

FAILED (failures=1, skipped=13)
py314: exit 1 (0.32 seconds) .../typing_extensions/src> python -m unittest discover pid=802355
.pkg: _exit> python /usr/lib/python3.13/site-packages/pyproject_api/_backend.py True flit_core.buildapi
  py314: FAIL code 1 (1.14=setup[0.82]+cmd[0.32] seconds)
  evaluation failed :( (1.19 seconds)
```

Hints from issue comments:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9c1718..81ca4da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
   on Python versions <3.10. PEP 604 was introduced in Python 3.10, and
   `typing_extensions` does not generally attempt to backport PEP-604 methods
   to prior versions.
+- Further update `typing_extensions.evaluate_forward_ref` with changes in Python 3.14.
 
 # Release 4.14.0rc1 (May 24, 2025)
 
diff --git a/src/typing_extensions.py b/src/typing_extensions.py
index 292641a..5d5a5c7 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -4060,57 +4060,6 @@ else:
         forward_ref.__forward_value__ = value
         return value
 
-    def _lax_type_check(
-        value, msg, is_argument=True, *, module=None, allow_special_forms=False
-    ):
-        """
-        A lax Python 3.11+ like version of typing._type_check
-        """
-        if hasattr(typing, "_type_convert"):
-            if (
-                sys.version_info >= (3, 10, 3)
-                or (3, 9, 10) < sys.version_info[:3] < (3, 10)
-            ):
-                # allow_special_forms introduced later cpython/#30926 (bpo-46539)
-                type_ = typing._type_convert(
-                    value,
-                    module=module,
-                    allow_special_forms=allow_special_forms,
-                )
-            # module was added with bpo-41249 before is_class (bpo-46539)
-            elif "__forward_module__" in typing.ForwardRef.__slots__:
-                type_ = typing._type_convert(value, module=module)
-            else:
-                type_ = typing._type_convert(value)
-        else:
-            if value is None:
-                return type(None)
-            if isinstance(value, str):
-                return ForwardRef(value)
-            type_ = value
-        invalid_generic_forms = (Generic, Protocol)
-        if not allow_special_forms:
-            invalid_generic_forms += (ClassVar,)
-            if is_argument:
-                invalid_generic_forms += (Final,)
-        if (
-            isinstance(type_, typing._GenericAlias)
-            and get_origin(type_) in invalid_generic_forms
-        ):
-            raise TypeError(f"{type_} is not valid as type argument") from None
-        if type_ in (Any, LiteralString, NoReturn, Never, Self, TypeAlias):
-            return type_
-        if allow_special_forms and type_ in (ClassVar, Final):
-            return type_
-        if (
-            isinstance(type_, (_SpecialForm, typing._SpecialForm))
-            or type_ in (Generic, Protocol)
-        ):
-            raise TypeError(f"Plain {type_} is not valid as type argument") from None
-        if type(type_) is tuple:  # lax version with tuple instead of callable
-            raise TypeError(f"{msg} Got {type_!r:.100}.")
-        return type_
-
     def evaluate_forward_ref(
         forward_ref,
         *,
@@ -4163,24 +4112,15 @@ else:
             else:
                 raise
 
-        msg = "Forward references must evaluate to types."
-        if not _FORWARD_REF_HAS_CLASS:
-            allow_special_forms = not forward_ref.__forward_is_argument__
-        else:
-            allow_special_forms = forward_ref.__forward_is_class__
-        type_ = _lax_type_check(
-            value,
-            msg,
-            is_argument=forward_ref.__forward_is_argument__,
-            allow_special_forms=allow_special_forms,
-        )
+        if isinstance(value, str):
+            value = ForwardRef(value)
 
         # Recursively evaluate the type
-        if isinstance(type_, ForwardRef):
-            if getattr(type_, "__forward_module__", True) is not None:
+        if isinstance(value, ForwardRef):
+            if getattr(value, "__forward_module__", True) is not None:
                 globals = None
             return evaluate_forward_ref(
-                type_,
+                value,
                 globals=globals,
                 locals=locals,
                  type_params=type_params, owner=owner,
@@ -4194,28 +4134,19 @@ else:
                     locals[tvar.__name__] = tvar
         if sys.version_info < (3, 12, 5):
             return typing._eval_type(
-                type_,
+                value,
                 globals,
                 locals,
                 recursive_guard=_recursive_guard | {forward_ref.__forward_arg__},
             )
-        if sys.version_info < (3, 14):
+        else:
             return typing._eval_type(
-                type_,
+                value,
                 globals,
                 locals,
                 type_params,
                 recursive_guard=_recursive_guard | {forward_ref.__forward_arg__},
             )
-        return typing._eval_type(
-            type_,
-            globals,
-            locals,
-            type_params,
-            recursive_guard=_recursive_guard | {forward_ref.__forward_arg__},
-            format=format,
-            owner=owner,
-        )
 
 
 class Sentinel:


Test patch:
diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py
index 60f6a1d..7fb748b 100644
--- a/src/test_typing_extensions.py
+++ b/src/test_typing_extensions.py
@@ -8944,7 +8944,147 @@ class TestGetAnnotationsWithPEP695(BaseTestCase):
             set(results.generic_func.__type_params__)
         )
 
-class TestEvaluateForwardRefs(BaseTestCase):
+
+class EvaluateForwardRefTests(BaseTestCase):
+    def test_evaluate_forward_ref(self):
+        int_ref = typing_extensions.ForwardRef('int')
+        self.assertIs(typing_extensions.evaluate_forward_ref(int_ref), int)
+        self.assertIs(
+            typing_extensions.evaluate_forward_ref(int_ref, type_params=()),
+            int,
+        )
+        self.assertIs(
+            typing_extensions.evaluate_forward_ref(int_ref, format=typing_extensions.Format.VALUE),
+            int,
+        )
+        self.assertIs(
+            typing_extensions.evaluate_forward_ref(
+                int_ref, format=typing_extensions.Format.FORWARDREF,
+            ),
+            int,
+        )
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(
+                int_ref, format=typing_extensions.Format.STRING,
+            ),
+            'int',
+        )
+
+    def test_evaluate_forward_ref_undefined(self):
+        missing = typing_extensions.ForwardRef('missing')
+        with self.assertRaises(NameError):
+            typing_extensions.evaluate_forward_ref(missing)
+        self.assertIs(
+            typing_extensions.evaluate_forward_ref(
+                missing, format=typing_extensions.Format.FORWARDREF,
+            ),
+            missing,
+        )
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(
+                missing, format=typing_extensions.Format.STRING,
+            ),
+            "missing",
+        )
+
+    def test_evaluate_forward_ref_nested(self):
+        ref = typing_extensions.ForwardRef("Union[int, list['str']]")
+        ns = {"Union": Union}
+        if sys.version_info >= (3, 11):
+            expected = Union[int, list[str]]
+        else:
+            expected = Union[int, list['str']]  # TODO: evaluate nested forward refs in Python < 3.11
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(ref, globals=ns),
+            expected,
+        )
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(
+                ref, globals=ns, format=typing_extensions.Format.FORWARDREF
+            ),
+            expected,
+        )
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.STRING),
+            "Union[int, list['str']]",
+        )
+
+        why = typing_extensions.ForwardRef('"\'str\'"')
+        self.assertIs(typing_extensions.evaluate_forward_ref(why), str)
+
+    @skipUnless(sys.version_info >= (3, 10), "Relies on PEP 604")
+    def test_evaluate_forward_ref_nested_pep604(self):
+        ref = typing_extensions.ForwardRef("int | list['str']")
+        if sys.version_info >= (3, 11):
+            expected = int | list[str]
+        else:
+            expected = int | list['str']  # TODO: evaluate nested forward refs in Python < 3.11
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(ref),
+            expected,
+        )
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.FORWARDREF),
+            expected,
+        )
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.STRING),
+            "int | list['str']",
+        )
+
+    def test_evaluate_forward_ref_none(self):
+        none_ref = typing_extensions.ForwardRef('None')
+        self.assertIs(typing_extensions.evaluate_forward_ref(none_ref), None)
+
+    def test_globals(self):
+        A = "str"
+        ref = typing_extensions.ForwardRef('list[A]')
+        with self.assertRaises(NameError):
+            typing_extensions.evaluate_forward_ref(ref)
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(ref, globals={'A': A}),
+            list[str] if sys.version_info >= (3, 11) else list['str'],
+        )
+
+    def test_owner(self):
+        ref = typing_extensions.ForwardRef("A")
+
+        with self.assertRaises(NameError):
+            typing_extensions.evaluate_forward_ref(ref)
+
+        # We default to the globals of `owner`,
+        # so it no longer raises `NameError`
+        self.assertIs(
+            typing_extensions.evaluate_forward_ref(ref, owner=Loop), A
+        )
+
+    @skipUnless(sys.version_info >= (3, 14), "Not yet implemented in Python < 3.14")
+    def test_inherited_owner(self):
+        # owner passed to evaluate_forward_ref
+        ref = typing_extensions.ForwardRef("list['A']")
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(ref, owner=Loop),
+            list[A],
+        )
+
+        # owner set on the ForwardRef
+        ref = typing_extensions.ForwardRef("list['A']", owner=Loop)
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(ref),
+            list[A],
+        )
+
+    @skipUnless(sys.version_info >= (3, 14), "Not yet implemented in Python < 3.14")
+    def test_partial_evaluation(self):
+        ref = typing_extensions.ForwardRef("list[A]")
+        with self.assertRaises(NameError):
+            typing_extensions.evaluate_forward_ref(ref)
+
+        self.assertEqual(
+            typing_extensions.evaluate_forward_ref(ref, format=typing_extensions.Format.FORWARDREF),
+            list[EqualToForwardRef('A')],
+        )
+
     def test_global_constant(self):
         if sys.version_info[:3] > (3, 10, 0):
             self.assertTrue(_FORWARD_REF_HAS_CLASS)
@@ -9107,30 +9247,17 @@ class TestEvaluateForwardRefs(BaseTestCase):
             self.assertEqual(get_args(evaluated_ref3), (Z[str],))
 
     def test_invalid_special_forms(self):
-        # tests _lax_type_check to raise errors the same way as the typing module.
-        # Regex capture "< class 'module.name'> and "module.name"
-        with self.assertRaisesRegex(
-            TypeError, r"Plain .*Protocol('>)? is not valid as type argument"
-        ):
-            evaluate_forward_ref(typing.ForwardRef("Protocol"), globals=vars(typing))
-        with self.assertRaisesRegex(
-            TypeError, r"Plain .*Generic('>)? is not valid as type argument"
-        ):
-            evaluate_forward_ref(typing.ForwardRef("Generic"), globals=vars(typing))
-        with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"):
-            evaluate_forward_ref(typing.ForwardRef("Final"), globals=vars(typing))
-        with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"):
-            evaluate_forward_ref(typing.ForwardRef("ClassVar"), globals=vars(typing))
+        for name in ("Protocol", "Final", "ClassVar", "Generic"):
+            with self.subTest(name=name):
+                self.assertIs(
+                    evaluate_forward_ref(typing.ForwardRef(name), globals=vars(typing)),
+                    getattr(typing, name),
+                )
         if _FORWARD_REF_HAS_CLASS:
             self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_class=True), globals=vars(typing)), Final)
             self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_class=True), globals=vars(typing)), ClassVar)
-            with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"):
-                evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing))
-            with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"):
-                evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing))
-        else:
-            self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)), Final)
-            self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar)
+        self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)), Final)
+        self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar)
 
 
 class TestSentinels(BaseTestCase):