diff --git a/peps/pep-0718.rst b/peps/pep-0718.rst index 370885c2b0e..dc2afe53947 100644 --- a/peps/pep-0718.rst +++ b/peps/pep-0718.rst @@ -15,16 +15,18 @@ Post-History: `24-Jun-2023 `__, Abstract -------- -This PEP proposes making function objects subscriptable for typing purposes. Doing so -gives developers explicit control over the types produced by the type checker where -bi-directional inference (which allows for the types of parameters of anonymous -functions to be inferred) and other methods than specialisation are insufficient. It -also makes functions consistent with regular classes in their ability to be -subscripted. +This PEP proposes making function objects subscriptable so that developers can +explicitly supply type arguments to generic functions, as in ``func[int](...)``. +This allows explicit specialisation when type inference is insufficient. It also +makes generic functions consistent with generic classes in their support for +subscription. Motivation ---------- +Comments marked ``Invalid`` in this section indicate errors reported by type +checkers. Runtime exceptions are labelled with their exception type. + Currently, classes allow passing type annotations for generic containers. This is especially useful in common constructors such as ``list``\, ``tuple`` and ``dict`` etc. @@ -50,8 +52,10 @@ code where we want to ensure that keys are ``str`` and values are ``int``: three="3" # Invalid: Literal["3"] is not of type int ) -In spite of the utility of this syntax, when trying to use it with a function, an error -is raised, as functions are not subscriptable. +At runtime this call succeeds and stores the string ``"3"`` in the dictionary. + +Attempting to use this syntax with a function raises ``TypeError`` at runtime, +as functions are not subscriptable. .. code-block:: python @@ -116,13 +120,21 @@ specialised usages: NameNumberDict = dict[str, int] NameNumberDict(one=1, two=2, three="3") # Invalid: Literal["3"] is not of type int -This is not currently possible for functions but if allowed we could easily -specialise operations in certain sections of the codebase: +A generic function can accept arguments of different types by inferring a common +supertype for its type parameter. For example, mypy accepts the following call +and infers ``object`` as its result type: .. code-block:: python def constrained_addition[T](a: T, b: T) -> T: ... + reveal_type(constrained_addition(1, "1")) # mypy: object + +With the proposed subscription syntax, we could require ``int`` for both +arguments: + +.. code-block:: python + # where we work exclusively with ints int_addition = constrained_addition[int] int_addition(2, 4+8j) # Invalid: complex is not of type int @@ -159,6 +171,8 @@ situations similar to: def foo[T](x: list[T]) -> T: ... reveal_type(foo([])) # type checker cannot infer T (yet again) +With this proposal, ``T`` could be supplied explicitly: + .. code-block:: python reveal_type(foo[int]([])) # type is int @@ -176,7 +190,7 @@ they weren't already. Some of this can be emulated with generic classes, but onl extra boilerplate and not in every type checker, so syntactic sugar would be highly welcome. -Due to this, specialising the function and using it as a new factory is fine +The proposed syntax also allows a specialised function to be reused as a factory: .. code-block:: python @@ -246,9 +260,15 @@ If accepted, this syntax will reduce the necessity to learn about :pep:`747`'s Specification ------------- -In this PEP, "function objects" refers to ``types.FunctionType``\ , -``types.MethodType``\ , ``types.BuiltinFunctionType``\ , ``types.BuiltinMethodType`` -and ``types.MethodWrapperType``\ . +Examples in this section illustrate the proposed behaviour unless explicitly +described as current behaviour. + +.. note:: + + In this PEP, "function objects" means instances of ``types.FunctionType``, + ``types.MethodType``, ``types.BuiltinFunctionType``, ``types.BuiltinMethodType`` + and ``types.MethodWrapperType``. Other callable objects, such as instances of + classes defining ``__call__``, are outside this definition. For ``MethodType`` you should be able to write: @@ -344,10 +364,13 @@ parameter defaults. Subscripting a function that has no type parameters is an er For functions using old-style ``TypeVar``\ s, which have no declared type parameter list, the order is the order in which the type variables first appear in the parameter -annotations, and then in the return annotation. This makes that order part of a -function's interface: reordering the parameters of such a function changes the meaning -of a subscription, so stubs should prefer :pep:`695` syntax for functions intended to be -subscripted. +annotations, and then in the return annotation. This is the same first-appearance rule +used by generic classes with old-style type variables when their order is not +explicitly given by a ``Generic[...]`` or ``Protocol[...]`` base. + +This makes that order part of a function's interface: reordering the parameters of such +a function changes the meaning of a subscription, so stubs should prefer :pep:`695` +syntax for functions intended to be subscripted. .. code-block:: python @@ -445,7 +468,7 @@ instance of a callable class. This PEP accepts that risk: The remaining risk mostly comes from decorators annotated as returning a ``Callable`` whose type variables type checkers scope to the returned callable, for example -``def make(func) -> Callable[[T], T]`` returning an instance of a callable class. The +``def make[T](func) -> Callable[[T], T]`` returning an instance of a callable class. The decorated name then has a generic callable type and can be subscripted for the type checker, but raises ``TypeError`` at runtime. @@ -604,8 +627,11 @@ variables cannot be resolved unambiguously with the current syntax. .. code-block:: python - foo[int, str, bool, complex](Bar(), Baz()) # Invalid: cannot determine which parameters are passed to *T and *U. Explicitly parameterise the instances individually - spam[int, str, bool, complex](Bar()) # OK + foo[int, str, bool, complex](Bar(), Baz()) # Invalid: cannot determine which + # parameters are passed to *T and + # *U. Explicitly parameterise the + # instances individually. + spam[int, str, bool, complex](Bar()) # OK As for generic classes, a single ``TypeVarTuple`` may be bound using an unpacked tuple type: @@ -669,7 +695,7 @@ cannot be inferred from the arguments: acc.send(1.5) # OK Lambdas cannot declare type parameters, so they are never generic: subscripting a lambda -is a type error, although it succeeds at runtime. The compiler's existing +is a type checker error, although it succeeds at runtime. The compiler's existing ``SyntaxWarning`` for subscripting a lambda literal is kept. See `Deferred Ideas`_. Backwards Compatibility @@ -689,7 +715,8 @@ relying on the ``TypeError``. Runtime Performance ------------------- -Each use of ``func[T]`` allocates a ``types.FunctionGenericAlias`` object on the heap. +Under this proposal, each use of ``func[T]`` allocates a +``types.FunctionGenericAlias`` object on the heap. When the subscript is immediately called (``func[T](args)``), the alias is constructed, called, and then discarded — it is a transient allocation per call site. Class subscriptions such as ``list[int]()`` pay a similar cost, and are in fact slower than @@ -732,6 +759,76 @@ outside the loop: Adding ``__getitem__`` to the function object types does not change how functions are called, so calls that do not use subscription are not expected to become slower. +How to Teach This +----------------- + +Prerequisites +^^^^^^^^^^^^^ + +The user needs to be familiar with generic classes and functions using the +:pep:`695` syntax, and with specializing a generic class: for example, +``Box[int]`` for a class declared as ``class Box[T]: ...``. + +Basics +^^^^^^ + +Take a generic function, for example: + +.. code-block:: python + + def func[T](a: T, b: T) -> T: + return a + +The new feature allows writing e.g. ``func[int]`` to specialize the function +with a particular type argument for ``T``. Type checkers substitute ``int`` for +``T`` when checking calls through this specialization. For example, +``func[int](1, 2)`` is valid, but ``func[int](1, "")`` is rejected by the type +checker. + +The rules for matching type arguments to type parameters follow those for +generic classes (see `Type checking subscriptions`_). Type checkers reject +specialization of functions that are not generic. + +A specialization can be saved and reused: + +.. code-block:: python + + int_func = func[int] + int_func(1, 2) # OK + int_func(1, "") # Type checker error + +Specialization affects type checking. It does not enforce types at runtime or +make the supplied type arguments available inside the function. + +When inference is used +^^^^^^^^^^^^^^^^^^^^^^ + +Explicit specialization binds the function's own type parameters, using +defaults for omitted arguments. Without specialization, normal type inference +applies. + +Caveats +^^^^^^^ + +Functions wrapped by ``functools.cache`` or ``functools.lru_cache``, and +objects created by ``functools.partial(...)``, do not support specialization. +See `Callable types`_ for details about decorators and other callable objects. + +When to use this, and when not +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Usually, type inference is sufficient. Explicit type arguments are useful when +inference does not express the intended types. + +It can be useful for selecting an overload, specifying element types for empty +containers, or inferring parameter types for lambda arguments to a generic +function. + +Another use is checking arguments against a chosen type rather than allowing +inference to select a broader one. For example, ``func[int](x, y)`` requires +arguments assignable to ``int``; mypy instead infers ``T = object`` for +``func(1, "")``. + Reference Implementation ------------------------ @@ -838,9 +935,9 @@ for some motivating examples. Change History -------------- -* 24-Apr-2026: Added examples, rationale and binding rules. * 21-Oct-2026: Addressed discussion feedback, clarified runtime and typing rules, and added use cases, performance notes and rejected ideas. +* 24-Apr-2026: Added examples, rationale and binding rules. Copyright ---------