diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index a0e396983885d2..d27e4546ecc297 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -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. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index feccc496fad0e0..448b420878b342 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1417,6 +1417,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. diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 50938eadc8f9f3..c6f08ff8a052ab 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -42,7 +42,7 @@ 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 @@ def test_subclass_special_form(self): Final[int], Literal[1, 2], Concatenate[int, ParamSpec("P")], + TypeForm[int], TypeGuard[int], TypeIs[range], ): @@ -7358,6 +7359,7 @@ class C(Generic[T]): pass 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 @@ def test_no_isinstance(self): 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 @@ def test_special_attrs(self): 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 @@ def test_special_attrs(self): 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', diff --git a/Lib/typing.py b/Lib/typing.py index 2dfa6d3b1499ca..e78fb8b71a996c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -155,6 +155,7 @@ 'Text', 'TYPE_CHECKING', 'TypeAlias', + 'TypeForm', 'TypeGuard', 'TypeIs', 'TypeAliasType', @@ -588,6 +589,13 @@ def __getitem__(self, parameters): 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 func1(val: list[object]): 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 index 00000000000000..6f496bb30e1686 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-19-20-54-25.gh-issue-145033.X9EBPQ.rst @@ -0,0 +1,2 @@ +Add :data:`typing.TypeForm`, implementing :pep:`747`. Patch by Jelle +Zijlstra.