Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 718 – Subscriptable functions

PEP 718 – Subscriptable functions

Author:
James Hilton-Balfe <gobot1234yt at gmail.com>, Pablo Ruiz Cuevas <pablo.r.c at live.com>
Sponsor:
Guido van Rossum <guido at python.org>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Topic:
Typing
Created:
23-Jun-2023
Python-Version:
3.16
Post-History:
24-Jun-2023, 24-Apr-2026, 21-Oct-2026

Table of Contents

Abstract

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.

my_integer_list = list[int]()
reveal_type(my_integer_list)  # type is list[int]

At runtime list[int] returns a GenericAlias that can be later called, returning an empty list.

Another example of this is creating a specialised dict type for a section of our code where we want to ensure that keys are str and values are int:

NameNumberDict = dict[str, int]

NameNumberDict(
   one=1,
   two=2,
   three="3"  # Invalid: Literal["3"] is not of type int
)

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.

def my_list[T](arr: Iterable[T]) -> list[T]:
   # do something...
   return list(arr)

my_integer_list = my_list[int]([])  # TypeError: 'function' object is not subscriptable

There are a few workarounds:

  1. Making a generic class whose __new__ returns the result (this is accepted by some type checkers, but not by all of them):
class my_list[T]:
    def __new__(cls, arr: Iterable[T]) -> list[T]:
        # do something...
        return list(arr)

my_string_list = my_list[str]([])
  1. Using PEP 747’s TypeForm, with an extra argument that is only there for the type checker:
from typing import TypeForm

def my_list[T](arr: Iterable[T], typ: TypeForm[T]) -> list[T]:
   # do something...
   return list(arr)

my_string_list = my_list([], str)

As we can see this solution increases the complexity with an extra argument. Additionally it requires the user to understand a new concept TypeForm.

  1. Annotating the assignment:
my_integer_list: list[int] = my_list([])

This solution isn’t optimal as the return type is repeated, is more verbose and would require the type updating in multiple places if the return type changes. Additionally, it adds unnecessary and distracting verbosity when the intention is to pass the specialised value into another call.

In conclusion, the current workarounds are too complex or verbose, especially compared to syntax that is consistent with the rest of the language.

Generic Specialisation

As in the previous example currently we can create generic aliases for different specialised usages:

NameNumberDict = dict[str, int]
NameNumberDict(one=1, two=2, three="3")  # Invalid: Literal["3"] is not of type int

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:

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:

# where we work exclusively with ints
int_addition = constrained_addition[int]
int_addition(2, 4+8j)  # Invalid: complex is not of type int

Unknown Types

Currently, it is not possible to infer the type parameters to generic functions in certain situations.

In this example T cannot currently be meaningfully inferred, so x is untyped without an extra assignment:

def factory[T](func: Callable[[T], Any]) -> Foo[T]: ...

reveal_type(factory(lambda x: "Hello World" * x)) # type is Foo[Unknown]

If function objects were subscriptable, however, a more specific type could be given:

reveal_type(factory[int](lambda x: "Hello World" * x))  # type is Foo[int]

Unsolvable Type Parameters

Currently, with unspecialised literals, it is not possible to determine a type for 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:

reveal_type(foo[int]([]))  # type is int

It is also useful to be able to specify in cases in which a certain type must be passed to a function beforehand:

words = ["hello", "world"]
foo[int](words)  # Invalid: list[str] is incompatible with list[int]

Allowing subscription makes functions and methods consistent with generic classes where they weren’t already. Some of this can be emulated with generic classes, but only with extra boilerplate and not in every type checker, so syntactic sugar would be highly welcome.

The proposed syntax also allows a specialised function to be reused as a factory:

def make_list[T](*args: T) -> list[T]: ...

make_int_list = make_list[int]
reveal_type(make_int_list())  # type is list[int]

Use Cases

The following GitHub issues illustrate concrete situations where subscriptable functions are needed. They are grouped by the problem pattern each one represents.

Generic factory functions

Users regularly want to specialize a generic factory function at the call site, mirroring the dict[str, int](...) syntax already available for classes. The current workaround — wrapping the function in a callable class — is widely described as unnecessarily complex.

  • python/typing#1410 — core feature request for making functions/methods subscriptable
  • python/mypy#6073 — user wants open_channel_pair[str](); the class-with-__new__ workaround is described as “gross”
  • python/typing#938 — user hits TypeVar inference ambiguity and wants func[T](args) for generic functions

Explicit type argument provision

Type checkers cannot always infer T from the arguments alone (e.g. empty containers, lambdas, or factory callbacks). Users need a way to supply type arguments explicitly at the call site.

  • python/mypy#4226 — mypy cannot infer or accept types on lambdas used as generic arguments
  • python/typing#1938 — users cannot annotate a variable holding a generic function because T has no expressible outer scope

Inconsistency with PEP 695 generic syntax

After PEP 695 introduced def func[T](x: T) -> T syntax in Python 3.12, users naturally attempted func[int](...) and were met with confusing errors, since the syntax looks valid but is rejected at runtime and by type checkers.

  • python/mypy#20927 — mypy rejects func[int](100) for a PEP 695 generic function with the misleading message “Type application is only supported for generic classes”
  • microsoft/pyright#9216 — Pyright raises reportIndexIssue for func[str](var=1) on a PEP 695 generic function

Rationale

This proposal improves the consistency of the type system, by allowing syntax that already looks and feels like a natural extension of the existing syntax for classes.

If accepted, this syntax will reduce the necessity to learn about PEP 747’s TypeForm, and reduce the verbosity and cognitive load of safely typed Python.

Specification

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:

class Foo:
    def make_list[T](self, *args: T) -> list[T]: ...

Foo().make_list[int]()

and have it work similarly to a FunctionType.

BuiltinFunctionType (e.g. max and min) should work like its pure Python counterparts.

BuiltinMethodType is the same type as BuiltinFunctionType.

MethodWrapperType (e.g. the type of object().__str__) is useful for generic magic methods.

Runtime behaviour

Function objects should implement __getitem__ to allow for subscription at runtime and return an instance of types.FunctionGenericAlias (see FunctionGenericAlias) with __origin__ set as the callable and __args__ as the types passed.

The runtime __getitem__ does not validate the subscription: it does not check whether the function is generic, nor the number or kinds of the type arguments. This is the same behaviour as generic builtins such as list, where list[int, str] is not caught at runtime. Checking subscriptions is left entirely to type checkers.

This is needed because many functions that type checkers treat as generic have no type parameters at runtime, for example functions using old-style TypeVars and functions that are only generic in a stub file:

# untyped.py: no type parameters at runtime
def ident(x, /):
    return x
# untyped.pyi: generic only in the stub
def ident[T](x: T, /) -> T: ...
from typing import TypeVar

from untyped import ident

T = TypeVar("T")

def legacy(x: T) -> T: ...      # legacy.__type_params__ == ()
def modern[T](x: T) -> T: ...

ident_int = ident[int]           # all three are valid at runtime
legacy_int = legacy[int]         # and for type checkers
modern_int = modern[int]

modern[int, str]  # valid at runtime, but a type checker reports too many type arguments

Subscripting an object that is not a function object is not affected by this PEP: obj[x] always calls type(obj).__getitem__ (or __class_getitem__ for a class), even if obj has a generic __call__ method.

Type checking subscriptions

Type checkers should support subscripting generic functions. The type arguments follow the same rules as type arguments to a generic class with the same type parameters, in declaration order, including the rules for ParamSpec, TypeVarTuple and type parameter defaults. Subscripting a function that has no type parameters is an error.

def wrap[**P, R](f: Callable[P, R]) -> Callable[P, R]: ...

wrap[[int, str], bool]   # P=[int, str], R=bool
wrap[..., bool]          # P=..., R=bool

def variadic[*Ts](*args: *Ts) -> tuple[*Ts]: ...

variadic[int, str]       # Ts=(int, str)
variadic[()]             # Ts=()

def suffix[*Ts, T](*args: *Ts, last: T) -> tuple[*Ts, T]: ...

suffix[int, str, bytes]  # Ts=(int, str), T=bytes

For functions using old-style TypeVars, 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 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.

S = TypeVar("S")
T = TypeVar("T")

def legacy(x: T, y: S) -> tuple[S, T]: ...

legacy[int, str]  # T=int, S=str

All type parameters declared in the function’s type parameter list count, including those the signature does not use. A function declared as def f[T]() -> None is generic and subscriptable, matching f.__type_params__ at runtime and the behaviour of generic classes:

def f[S, T](x: T) -> T: ...

f[str, int]  # OK: S=str, T=int
f[int]       # Invalid: too few type arguments

A type parameter that only appears in the return type (such as T in def f[T]() -> T) is not an error at the definition, since explicit subscription can now bind it. Type checkers should instead report an error at a call that leaves such a type parameter unsolved: one that is not bound by a subscription, has no default, and cannot be solved from the arguments or from the expected type of the call. Type parameters that do not appear in the signature at all are not affected by this rule.

def f[T]() -> T: ...  # OK

x = f[int]()       # OK: x is int
y: int = f()       # OK: solved from the expected type
z = f()            # Invalid: cannot solve T

Type parameter defaults

This PEP specifies the semantics of function type parameter defaults (PEP 696) precisely, and thus requires a change to the typing specification, which currently leaves these semantics up to the individual type checker. Defaults on an unsubscripted function call should be used when inference provides no binding. On subscripted function calls all of the function’s own type parameters are bound by the subscription (using defaults for the omitted ones) and the call does no inference for them; type parameters of an enclosing class are still inferred as usual (see Binding Rules):

def g[T, U = bytes](x: T, y: U): ...

g("", "")          # inferred: T=str, U=str
g[str]             # T=str, U=bytes
g[str, int]        # T=str, U=int
g[str]("", b"")    # OK
g[str]("", "")     # Invalid: y is str, expected bytes

Callable types

Type checkers should allow subscription of any value whose type is a generic callable type, that is, a callable type with its own type parameters, such as the declared type of a generic function or method. This is not limited to values known to be function objects, and includes functions whose generic signature is preserved by a decorator. Values of instance types are not included, even when the class or callback protocol has a generic __call__ method: subscripting them uses their __getitem__ as before. For example, a decorator that returns its argument unchanged keeps the function subscriptable:

def keep[F: Callable[..., object]](f: F) -> F: ...

@keep
def first[T](items: list[T]) -> T: ...

first[int]([])  # OK

At runtime only function objects implement __getitem__, so a value with a generic callable type that is not a function object raises TypeError when subscripted, for example when a decorator is annotated as returning a generic Callable but returns an instance of a callable class. This PEP accepts that risk:

  • Requiring the type checker to prove that a value is a function object would make most decorated functions unsubscriptable, because most decorators can only be annotated as returning a Callable.
  • Common non-function callables in the standard library, such as functools.partial, functools.lru_cache and functools.cache objects, are typed as instances of generic classes rather than as generic callables, so type checkers already reject subscripting them. This was a concern in the discussion, but it does not apply to these types (checked with both mypy prototypes).
  • Authors of decorators that return non-function callables can give those objects a __getitem__, or annotate the decorator’s return type precisely.

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[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.

At runtime, a decorated function can be subscripted only if the decorator returns a function object:

  • Wrappers created with functools.wraps and functions decorated with contextlib.contextmanager are function objects, so they work:
    with session[Admin]() as s: ...  # session is a @contextmanager function
    

    For the type checker, the decorator must give the result a generic callable type, for example by being annotated as returning Callable[P, R]. A wrapper whose type is inferred as an instance (such as functools._Wrapped in typeshed) cannot be subscripted, even though it works at runtime. Decorators using ParamSpec, such as contextmanager, should also keep type parameters that only appear in the return type, so that session above stays generic.

  • functools.cache, functools.lru_cache and functools.partial return other objects, which raise TypeError (type checkers already reject these, as above).

Using an alias as the wrapper, as in functools.wraps(f)(f[int]), is not supported: aliases have no __dict__ for functools.wraps to copy attributes to. Using an alias as the wrapped function, as in functools.wraps(f[int]), works, since aliases forward attribute access to the function.

The stricter alternative is discussed in Rejected Ideas.

Python version

Type checkers should report subscription of a function as an error when the target Python version is earlier than 3.16, since it fails at runtime there. This also covers builtins and other functions declared as generic in typeshed.

FunctionGenericAlias

Subscripting a function object returns an instance of types.FunctionGenericAlias, a new subclass of types.GenericAlias. This gives a public way to tell whether an alias was created from a function or from a class, without checking __origin__ against each function object type:

def foo[T](x: T) -> T: ...

isinstance(foo[int], types.FunctionGenericAlias)  # True
isinstance(list[int], types.FunctionGenericAlias)  # False

Apart from __call__ (below), it behaves like types.GenericAlias: for example foo[int] | None creates a union, and subscripting an alias that still has type parameters, such as foo[T][int], substitutes them. Unlike class aliases, it cannot be used as a base class.

Currently, __orig_class__ is an attribute set in GenericAlias.__call__ to the instance of the GenericAlias that created the called class e.g.

class Foo[T]: ...

assert Foo[int]().__orig_class__ == Foo[int]

FunctionGenericAlias.__call__ does not set __orig_class__, as the object returned by a function is not an instance created from the alias, and setting the attribute would overwrite a value the function itself may have set.

The following code snippet would fail at runtime without this rule as __orig_class__ would be bar[str] and not Foo[int].

def bar[U]():
    return Foo[int]()

assert bar[str]().__orig_class__  == Foo[int]

Interactions with @typing.overload

This PEP allows type checkers to do overloading based on type parameters:

@overload
def serializer_for[T: str]() -> StringSerializer: ...
@overload
def serializer_for[T: list]() -> ListSerializer: ...

def serializer_for[T: str | list]():
    ...

When an overloaded function is subscripted, type checkers apply the subscription to each overload separately, using the rules in Type checking subscriptions: the number of type arguments (taking type parameter defaults and TypeVarTuples into account) and the bounds and constraints of each type parameter. Overloads for which the subscription is invalid are discarded without reporting an error. If no overload remains, the subscription is an error. If one remains, the result is its specialised signature; otherwise the result is an overloaded function made of the remaining specialised signatures, and a call to it is evaluated with the usual overload rules.

Because a subscription can select between overloads that differ only in the bounds or constraints of their type parameters, type checkers should not report such overloads as overlapping or unreachable. A call without subscription still picks the first matching overload as usual, so an unsubscripted serializer_for() returns StringSerializer without any error; such overloads are meant to be used with a subscription.

@overload
def pick[T](x: T) -> list[T]: ...
@overload
def pick[S, U](x: S, y: U) -> dict[S, U]: ...
def pick[S, U](x: S, y: U | None = None): ...

pick[int]             # only the first overload remains
pick[str, int]("a", 1)  # only the second overload remains: dict[str, int]
pick[int, str, bytes]   # Invalid: no overload accepts 3 type arguments

serializer_for[str]()    # StringSerializer
serializer_for[bytes]()  # Invalid: bytes satisfies neither bound

Only the implementation of an overloaded function exists at runtime, and the runtime __getitem__ does not look at its type parameters, so the subscription works at runtime whatever the implementation declares. To keep the runtime __type_params__ consistent with the overloads, type checkers should report an error when the overloads declare type parameters using PEP 695 syntax and the implementation declares none:

@overload
def serializer_for[T: str]() -> StringSerializer: ...
@overload
def serializer_for[T: list]() -> ListSerializer: ...

def serializer_for():  # Invalid: the overloads declare type parameters but the implementation does not
    ...

Functions Parameterized by TypeVarTuples

Currently, type checkers disallow the use of multiple TypeVarTuples in their generic parameters; however, it is currently valid to have a function as such:

def foo[*T, *U](bar: Bar[*T], baz: Baz[*U]): ...
def spam[*T](bar: Bar[*T]): ...

This PEP does not allow functions like foo to be subscripted, for the same reason as defined in PEP 646, the type variables cannot be resolved unambiguously with the current syntax.

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:

spam[*tuple[int, str]](Bar())     # OK: same as spam[int, str]
spam[*tuple[int, ...]](Bar())     # OK: *T is any number of ints

Binding Rules

Method subscription (including classmethods and staticmethods), should only allow their function’s type parameters and not the enclosing class’s. Subscription should follow the rules specified in PEP 696; methods should bind type parameters on attribute access.

class C[T]:
    def method[U](self, x: T, y: U): ...
    @classmethod
    def cls[U](cls, x: T, y: U): ...
    @staticmethod
    def stat[U](y: U): ...

C[int]().method[str](0, "")  # OK
C[int].cls[str](0, "")       # OK
C.cls[int, str](0, "")       # Invalid: too many type parameters
C.cls[str](0, "")            # OK: U=str, T is solved from the call (int)
C[int].stat[str]("")         # OK: U=str

When a method is accessed on the unspecialised class, as in C.cls[str](0, ""), the subscription binds only the method’s own type parameters; the class’s type parameters are solved from the call, as they would be without the subscription.

Kinds of functions

Generator functions, coroutine functions and asynchronous generator functions are function objects, so they can be subscripted like any other function; calling the result returns the generator, coroutine or asynchronous generator as usual. This is especially useful for generators that receive values through send(), whose send type often cannot be inferred from the arguments:

class SupportsAdd(Protocol):
    def __add__(self, other: Self, /) -> Self: ...

def running_sum[T: SupportsAdd](start: T) -> Generator[T, T, None]:
    total = start
    while True:
        total += yield total

acc = running_sum(0)         # T is inferred as int
next(acc)
acc.send(1.5)                # Invalid: float is not assignable to int

acc = running_sum[float](0)  # T=float
next(acc)
acc.send(1.5)                # OK

Lambdas cannot declare type parameters, so they are never generic: subscripting a lambda 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

Currently these classes are not subclassable and so there are no backwards compatibility concerns with regards to classes already implementing __getitem__.

Code that tries obj[key] and falls back to attribute access when it raises TypeError will behave differently for functions: the subscription now succeeds and returns an alias. For example, template engines that look up obj.name by trying obj["name"] first (such as Django’s template variable resolution) would get an alias of the function instead of its name attribute, and may then call it. Such code needs to check for function objects (or types.FunctionGenericAlias results) before relying on the TypeError.

Runtime Performance

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 function subscriptions.

Measurements posted in the discussion, on an unoptimised build of the original implementation (a Python 3.13 alpha branch), give an idea of the cost (time per call):

Expression Time
foo() 34 ns
cast(int, foo()) 69 ns
foo[int]() 128 ns

So a subscripted call is about twice as slow as a cast and about four times as slow as a plain call. Annotating the assignment instead (x: int = foo()) has no runtime cost and remains available where this matters.

Repeatedly subscripting the same function inside a hot loop is unnecessarily wasteful, since the alias is recreated on every iteration. Users should hoist the specialisation outside the loop:

# Avoid: allocates an alias on every iteration
for item in data:
    result = process[int](item)

# Prefer: allocate once, call many times
process_int = process[int]
for item in data:
    result = process_int(item)

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:

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:

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

  • python/cpython#156201: the runtime changes for function, method and builtin function objects, a port to the main branch of the original function-subscript branch. It predates types.FunctionGenericAlias (it returns a plain types.GenericAlias) and does not yet cover MethodWrapperType.
  • python/mypy#21880: a type checker prototype of the behaviour specified here, behind --enable-incomplete-feature=SubscriptableFunctions. It confirmed that ParamSpec, TypeVarTuple and type parameter defaults can follow the rules for generic classes by reusing the same code, and implements the overload filtering described above.
  • python/mypy#21881: a variant of the prototype that only allows subscripting callables traceable to a def (see Rejected Ideas). It cannot be fully sound either, since type checkers have to assume that decorators preserve the kind of object they wrap.

The following rules are not yet implemented in either prototype: reporting unsolvable type parameters at the call instead of the definition, not reporting overloads that differ only in type parameter bounds as overlapping, the consistency rule for overload implementations, the Python version check, and keeping return-only type parameters through ParamSpec decorators (mypy currently solves them to Never, so a contextmanager function like session above cannot be subscripted).

Rejected Ideas

Only allowing subscription of known function objects

Type checkers could allow subscription only of values they know to be function objects, rejecting values that are only known to have a generic Callable type. This would rule out more runtime errors, but most decorated functions could then never be subscripted. The python/mypy#21881 prototype shows that keeping decorated functions subscriptable requires assuming that decorators return function objects, which is the same risk this PEP accepts, with a more complicated rule.

A property to tell function aliases from class aliases

A property on types.GenericAlias reporting whether __origin__ is a function object was considered. GenericAlias forwards attribute access to its __origin__, so adding a new attribute could break existing code; a subclass, types.FunctionGenericAlias, is used instead.

New syntax

Angle brackets (f<int>(x)) or a dedicated operator (f::[int](x)) would avoid reusing [], and a dedicated syntax could let the compiler treat the type arguments specially. However, f<int>(x) is already valid syntax with a different meaning, and any new syntax is a much larger change than adding __getitem__ to function objects, which works for all existing functions and only needs a library-level change.

Validating subscriptions at runtime

The runtime __getitem__ could check that the function is generic and that the number of type arguments matches. This would break functions that type checkers treat as generic but that have no type parameters at runtime, such as functions using old-style TypeVars or functions only typed in stub files. It would also be inconsistent with generic builtins, which do not validate their type arguments.

Deferred Ideas

Accessing type arguments at runtime

This PEP does not give a function any way to access its type arguments at runtime (sometimes called reification). For classes this is possible, but limited, through __orig_class__:

>>> class Foo[T]: ...
>>> Foo[int]()
<__main__.Foo object at 0x...>
>>> Foo[int]().__orig_class__.__args__
(<class 'int'>,)

There is currently no equivalent for functions, and related requests exist for classes (see python/typing#629, about type arguments inside class methods of generic classes). Designing it is left to a future PEP.

Generic lambdas

Lambdas cannot declare type parameters, and there is no inline syntax for a generic callable type. A syntax such as lambda[T](arg: T) -> T was suggested in the discussion. This is left to a future PEP.

Acknowledgements

Thank you to Alex Waygood and Jelle Zijlstra for their feedback on this PEP and Guido for some motivating examples.

Change History

  • 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.