]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-145033: Implement PEP 747 (#145034)
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Sun, 1 Mar 2026 03:52:04 +0000 (19:52 -0800)
committerGitHub <noreply@github.com>
Sun, 1 Mar 2026 03:52:04 +0000 (19:52 -0800)
Doc/library/typing.rst
Doc/whatsnew/3.15.rst
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2026-02-19-20-54-25.gh-issue-145033.X9EBPQ.rst [new file with mode: 0644]

index ef44701bb251dd30ee013de6348ea52cd37da10f..09e9103e1b80d0465c44f04eefacb0194895d254 100644 (file)
@@ -1524,6 +1524,35 @@ These can be used as types in annotations. They all support subscription using
    .. versionadded:: 3.9
 
 
+.. data:: TypeForm
+
+   A special form representing the value that results from evaluating a
+   type expression.
+
+   This value encodes the information supplied in the type expression, and
+   it represents the type described by that type expression.
+
+   When used in a type expression, ``TypeForm`` describes a set of type form
+   objects. It accepts a single type argument, which must be a valid type
+   expression. ``TypeForm[T]`` describes the set of all type form objects that
+   represent the type ``T`` or types assignable to ``T``.
+
+   ``TypeForm(obj)`` simply returns ``obj`` unchanged. This is useful for
+   explicitly marking a value as a type form for static type checkers.
+
+   Example::
+
+      from typing import Any, TypeForm
+
+      def cast[T](typ: TypeForm[T], value: Any) -> T: ...
+
+      reveal_type(cast(int, "x"))  # Revealed type is "int"
+
+   See :pep:`747` for details.
+
+   .. versionadded:: 3.15
+
+
 .. data:: TypeIs
 
    Special typing construct for marking user-defined type predicate functions.
index 163d50d7e20e205be82a4c0c75530e7f6fae0321..63ef5f84301794dd3c9509db2d658838276009a9 100644 (file)
@@ -1431,6 +1431,25 @@ threading
 typing
 ------
 
+* :pep:`747`: Add :data:`~typing.TypeForm`, a new special form for annotating
+  values that are themselves type expressions.
+  ``TypeForm[T]`` means "a type form object describing ``T`` (or a type
+  assignable to ``T``)". At runtime, ``TypeForm(x)`` simply returns ``x``,
+  which allows explicit annotation of type-form values without changing
+  behavior.
+
+  This helps libraries that accept user-provided type expressions
+  (for example ``int``, ``str | None``, :class:`~typing.TypedDict`
+  classes, or ``list[int]``) expose precise signatures:
+
+  .. code-block:: python
+
+     from typing import Any, TypeForm
+
+     def cast[T](typ: TypeForm[T], value: Any) -> T: ...
+
+  (Contributed by Jelle Zijlstra in :gh:`145033`.)
+
 * The undocumented keyword argument syntax for creating
   :class:`~typing.NamedTuple` classes (for example,
   ``Point = NamedTuple("Point", x=int, y=int)``) is no longer supported.
index 50938eadc8f9f38a64dd12bd152e3836142be69c..c6f08ff8a052abccc071382e88e0def3d37c7cab 100644 (file)
@@ -42,7 +42,7 @@ from typing import Annotated, ForwardRef
 from typing import Self, LiteralString
 from typing import TypeAlias
 from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
-from typing import TypeGuard, TypeIs, NoDefault
+from typing import TypeForm, TypeGuard, TypeIs, NoDefault
 import abc
 import textwrap
 import typing
@@ -5890,6 +5890,7 @@ class GenericTests(BaseTestCase):
             Final[int],
             Literal[1, 2],
             Concatenate[int, ParamSpec("P")],
+            TypeForm[int],
             TypeGuard[int],
             TypeIs[range],
         ):
@@ -7358,6 +7359,7 @@ class GetUtilitiesTestCase(TestCase):
         self.assertEqual(get_args(Required[int]), (int,))
         self.assertEqual(get_args(NotRequired[int]), (int,))
         self.assertEqual(get_args(TypeAlias), ())
+        self.assertEqual(get_args(TypeForm[int]), (int,))
         self.assertEqual(get_args(TypeGuard[int]), (int,))
         self.assertEqual(get_args(TypeIs[range]), (range,))
         Ts = TypeVarTuple('Ts')
@@ -10646,6 +10648,72 @@ class TypeIsTests(BaseTestCase):
             issubclass(int, TypeIs)
 
 
+class TypeFormTests(BaseTestCase):
+    def test_basics(self):
+        TypeForm[int]  # OK
+        self.assertEqual(TypeForm[int], TypeForm[int])
+
+        def foo(arg) -> TypeForm[int]: ...
+        self.assertEqual(gth(foo), {'return': TypeForm[int]})
+
+        with self.assertRaises(TypeError):
+            TypeForm[int, str]
+
+    def test_repr(self):
+        self.assertEqual(repr(TypeForm), 'typing.TypeForm')
+        cv = TypeForm[int]
+        self.assertEqual(repr(cv), 'typing.TypeForm[int]')
+        cv = TypeForm[Employee]
+        self.assertEqual(repr(cv), 'typing.TypeForm[%s.Employee]' % __name__)
+        cv = TypeForm[tuple[int]]
+        self.assertEqual(repr(cv), 'typing.TypeForm[tuple[int]]')
+
+    def test_cannot_subclass(self):
+        with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE):
+            class C(type(TypeForm)):
+                pass
+        with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE):
+            class D(type(TypeForm[int])):
+                pass
+        with self.assertRaisesRegex(TypeError,
+                                    r'Cannot subclass typing\.TypeForm'):
+            class E(TypeForm):
+                pass
+        with self.assertRaisesRegex(TypeError,
+                                    r'Cannot subclass typing\.TypeForm\[int\]'):
+            class F(TypeForm[int]):
+                pass
+
+    def test_call(self):
+        objs = [
+            1,
+            "int",
+            int,
+            tuple[int, str],
+            Tuple[int, str],
+        ]
+        for obj in objs:
+            with self.subTest(obj=obj):
+                self.assertIs(TypeForm(obj), obj)
+
+        with self.assertRaises(TypeError):
+            TypeForm()
+        with self.assertRaises(TypeError):
+            TypeForm("too", "many")
+
+    def test_cannot_init_type(self):
+        with self.assertRaises(TypeError):
+            type(TypeForm)()
+        with self.assertRaises(TypeError):
+            type(TypeForm[Optional[int]])()
+
+    def test_no_isinstance(self):
+        with self.assertRaises(TypeError):
+            isinstance(1, TypeForm[int])
+        with self.assertRaises(TypeError):
+            issubclass(int, TypeForm)
+
+
 SpecialAttrsP = typing.ParamSpec('SpecialAttrsP')
 SpecialAttrsT = typing.TypeVar('SpecialAttrsT', int, float, complex)
 
@@ -10747,6 +10815,7 @@ class SpecialAttrsTests(BaseTestCase):
             typing.Never: 'Never',
             typing.Optional: 'Optional',
             typing.TypeAlias: 'TypeAlias',
+            typing.TypeForm: 'TypeForm',
             typing.TypeGuard: 'TypeGuard',
             typing.TypeIs: 'TypeIs',
             typing.TypeVar: 'TypeVar',
@@ -10761,6 +10830,7 @@ class SpecialAttrsTests(BaseTestCase):
             typing.Literal[1, 2]: 'Literal',
             typing.Literal[True, 2]: 'Literal',
             typing.Optional[Any]: 'Union',
+            typing.TypeForm[Any]: 'TypeForm',
             typing.TypeGuard[Any]: 'TypeGuard',
             typing.TypeIs[Any]: 'TypeIs',
             typing.Union[Any]: 'Any',
index 2dfa6d3b1499ca11771fc458feac2d0f64be3736..e78fb8b71a996c7bcb6c71b690cc536fd3562157 100644 (file)
@@ -155,6 +155,7 @@ __all__ = [
     'Text',
     'TYPE_CHECKING',
     'TypeAlias',
+    'TypeForm',
     'TypeGuard',
     'TypeIs',
     'TypeAliasType',
@@ -588,6 +589,13 @@ class _TypedCacheSpecialForm(_SpecialForm, _root=True):
         return self._getitem(self, *parameters)
 
 
+class _TypeFormForm(_SpecialForm, _root=True):
+    # TypeForm(X) is equivalent to X but indicates to the type checker
+    # that the object is a TypeForm.
+    def __call__(self, obj, /):
+        return obj
+
+
 class _AnyMeta(type):
     def __instancecheck__(self, obj):
         if self is Any:
@@ -895,6 +903,31 @@ def TypeGuard(self, parameters):
     return _GenericAlias(self, (item,))
 
 
+@_TypeFormForm
+def TypeForm(self, parameters):
+    """A special form representing the value that results from the evaluation
+    of a type expression.
+
+    This value encodes the information supplied in the type expression, and it
+    represents the type described by that type expression.
+
+    When used in a type expression, TypeForm describes a set of type form
+    objects. It accepts a single type argument, which must be a valid type
+    expression. ``TypeForm[T]`` describes the set of all type form objects that
+    represent the type T or types that are assignable to T.
+
+    Usage::
+
+        def cast[T](typ: TypeForm[T], value: Any) -> T: ...
+
+        reveal_type(cast(int, "x"))  # int
+
+    See PEP 747 for more information.
+    """
+    item = _type_check(parameters, f'{self} accepts only single type.')
+    return _GenericAlias(self, (item,))
+
+
 @_SpecialForm
 def TypeIs(self, parameters):
     """Special typing construct for marking user-defined type predicate functions.
@@ -1348,10 +1381,11 @@ class _GenericAlias(_BaseGenericAlias, _root=True):
     #     A = Callable[[], None]  # _CallableGenericAlias
     #     B = Callable[[T], None]  # _CallableGenericAlias
     #     C = B[int]  # _CallableGenericAlias
-    # * Parameterized `Final`, `ClassVar`, `TypeGuard`, and `TypeIs`:
+    # * Parameterized `Final`, `ClassVar`, `TypeForm`, `TypeGuard`, and `TypeIs`:
     #     # All _GenericAlias
     #     Final[int]
     #     ClassVar[float]
+    #     TypeForm[bytes]
     #     TypeGuard[bool]
     #     TypeIs[range]
 
diff --git a/Misc/NEWS.d/next/Library/2026-02-19-20-54-25.gh-issue-145033.X9EBPQ.rst b/Misc/NEWS.d/next/Library/2026-02-19-20-54-25.gh-issue-145033.X9EBPQ.rst
new file mode 100644 (file)
index 0000000..6f496bb
--- /dev/null
@@ -0,0 +1,2 @@
+Add :data:`typing.TypeForm`, implementing :pep:`747`. Patch by Jelle
+Zijlstra.