Skip to content
Merged
Prev Previous commit
Next Next commit
final round of changes
  • Loading branch information
Gobot1234 committed Mar 5, 2026
commit 6ac59ebf55200084bf02bc9c5fb259dc8d6d05fd
156 changes: 123 additions & 33 deletions peps/pep-0718.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PEP: 718
Title: Subscriptable functions
Author: James Hilton-Balfe <gobot1234yt@gmail.com>
Author: James Hilton-Balfe <gobot1234yt@gmail.com>, Pablo Ruiz Cuevas <pablo.r.c@live.com>
Sponsor: Guido van Rossum <guido@python.org>
Discussions-To: https://discuss.python.org/t/28457/
Status: Draft
Expand All @@ -17,41 +17,113 @@ This PEP proposes making function objects subscriptable for typing purposes. Doi
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 brings functions in line with regular classes in their ability to be
subscriptable.
also makes functions consistent with regular classes in their ability to be
subscripted.

Motivation
----------

Unknown Types
^^^^^^^^^^^^^
Currently, classes allow passing type annotations for generic containers, this
Comment thread
gvanrossum marked this conversation as resolved.
Outdated
is especially useful in common constructors such as ``list``\, ``tuple`` and ``dict``
etc.

Currently, it is not possible to infer the type parameters to generic functions in
certain situations:
.. code-block:: python

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``:

.. code-block:: python

NameNumberDict = dict[str, int]
Comment thread
gvanrossum marked this conversation as resolved.

NameNumberDict(
one=1,
two=2,
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.

.. code-block:: python

def my_list[T](arr) -> list[T]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be augmented with arr: Iterable[T] (which reflects what list(arr) expects) or is that a distraction?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changing

# do something...
return list(arr)

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

There are a few workarounds:
Comment thread
gvanrossum marked this conversation as resolved.

1. Making a callable class:

.. code-block:: python

class my_list[T]:
def __call__(self, *args: T) -> list[T]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this signature is inconsistent with make_list(arr) above.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

# do something...
return list(args)

2. Using :pep:`747`\'s TypeForm, with an extra unused argument:

.. code-block:: python

from typing import TypeForm

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

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

3. Annotating the assignment:

.. code-block:: python

def make_list[T](*args: T) -> list[T]: ...
reveal_type(make_list()) # type checker cannot infer a meaningful type for T
my_integer_list: list[int] = my_list()

This solution isn't optimal as the return type is repeated and is more verbose and
would require the type updating in multiple places if the return type changes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, it's verbose when the intention is to pass the specialized value into another call -- now the reader has to deal with the distraction of seeing a temporary variable declaration.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added


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

Making instances of ``FunctionType`` subscriptable would allow for this constructor to
be typed:
Generic Specialisation
^^^^^^^^^^^^^^^^^^^^^^

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

.. code-block:: python

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

Currently you have to use an assignment to provide a precise type:
This not currently possible for functions but if allowed we could easily
specialise operations in certain sections of the codebase:

.. code-block:: python

x: list[int] = make_list()
reveal_type(x) # type is list[int]
def constrained_addition[T](a: T, b: T) -> T: ...

but this code is unnecessarily verbose taking up multiple lines for a simple function
call.
# where we work exclusively with ints
int_addition = constrained_addition[int]
int_addition(2, 4+8j) # Invalid: complex is not of type int

Similarly, ``T`` in this example cannot currently be meaningfully inferred, so ``x`` is
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:

.. code-block:: python
Expand All @@ -66,11 +138,11 @@ If function objects were subscriptable, however, a more specific type could be g

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

Undecidable Inference
^^^^^^^^^^^^^^^^^^^^^
Undecidable Inference and Type Narrowing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

There are even cases where subclass relations make type inference impossible. However,
if you can specialise the function type checkers can infer a meaningful type.
There are cases where subclass relations make type inference impossible. However, if
you can specialise the function type checkers can infer a meaningful type.

.. code-block:: python

Expand Down Expand Up @@ -138,7 +210,16 @@ The syntax for such a feature may look something like:
Rationale
---------

Function objects in this PEP is used to refer to ``FunctionType``\ , ``MethodType``\ ,
This proposal improves the consistency of the type system, by allowing syntax that
already looks and feels like a natural of the existing syntax for classes.

If accepted, this syntax will reduce the necessity to learn about :pep:`747`\s
``TypeForm``, reduce verbosity and cognitive load of safely typed python.

Specification
-------------

In this PEP "Function objects" is used to refer to ``FunctionType``\ , ``MethodType``\ ,
``BuiltinFunctionType``\ , ``BuiltinMethodType`` and ``MethodWrapperType``\ .

For ``MethodType`` you should be able to write:
Expand All @@ -161,9 +242,6 @@ functions implemented in Python as possible.
``MethodWrapperType`` (e.g. the type of ``object().__str__``) is useful for
generic magic methods.

Specification
-------------

Function objects should implement ``__getitem__`` to allow for subscription at runtime
and return an instance of ``types.GenericAlias`` with ``__origin__`` set as the
callable and ``__args__`` as the types passed.
Expand Down Expand Up @@ -201,19 +279,31 @@ The following code snippet would fail at runtime without this change as
Interactions with ``@typing.overload``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Overloaded functions should work much the same as they already do, since they do not
affect the runtime type. Explicit specialisation will restrict the set of available
overloads.
This PEP opens the door to overloading based on type variables:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the complexity and ambiguity already present in @overload I'm not sure that "opens the door" is enough of a specification.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changing to "this pep allows"


.. code-block:: python

@overload
def make[T](x: T) -> T: ...
def serializer_for[T: str]() -> StringSerializer: ...
@overload
def make(x: str, y: str) -> tuple[int, int]: ...
def serializer_for[T: list]() -> ListSerializer: ...

def serializer_for():
...

For overload resolution a new step will be required previous to any other, where the resolver
will match only the overloads where the subscription may succeed.

.. code-block:: python

@overload
def make[*Ts]() -> float: ...
@overload
def make[T]() -> int: ...

make[int] # matches first and second overload
make[int, str] # matches only first

reveal_type(make[int](1)) # type is int
reveal_type(make[int]("foo", "bar")) # Invalid: no overload for `make[int](x: str, y: str)` found, a similar overload exists but explicit specialisation prevented its use

Functions Parameterized by ``TypeVarTuple``\ s
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
Loading