Skip to content

Commit 16e35f5

Browse files
Gobot1234RosuavJelleZijlstradavidfstr
authored
PEP 718: Subscriptable functions (#3179)
Co-authored-by: Chris Angelico <rosuav@gmail.com> Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com> Co-authored-by: David Foster <david@dafoster.net>
1 parent 1453e26 commit 16e35f5

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@ pep-0712.rst @ericvsmith
596596
pep-0713.rst @ambv
597597
pep-0714.rst @dstufft
598598
pep-0715.rst @dstufft
599+
pep-0718.rst @gvanrossum
599600
pep-0719.rst @Yhg1s
600601
pep-0720.rst @FFY00
601602
pep-0721.rst @encukou

pep-0718.rst

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
PEP: 718
2+
Title: Subscriptable functions
3+
Author: James Hilton-Balfe <gobot1234yt@gmail.com>
4+
Sponsor: Guido van Rossum <guido@python.org>
5+
Discussions-To: https://discuss.python.org/t/26463/
6+
Status: Draft
7+
Type: Standards Track
8+
Topic: Typing
9+
Content-Type: text/x-rst
10+
Created: 23-Jun-2023
11+
Python-Version: 3.13
12+
Post-History: `24-Jun-2023 <https://discuss.python.org/t/28457/>`__
13+
14+
Abstract
15+
--------
16+
17+
This PEP proposes making function objects subscriptable for typing purposes. Doing so
18+
gives developers explicit control over the types produced by the type checker where
19+
bi-directional inference (which allows for the types of parameters of anonymous
20+
functions to be inferred) and other methods than specialisation are insufficient.
21+
22+
Motivation
23+
----------
24+
25+
Currently, it is not possible to infer the type parameters to generic functions in
26+
certain situations:
27+
28+
.. code-block:: python
29+
30+
def make_list[T](*args: T) -> list[T]: ...
31+
reveal_type(make_list()) # type checker cannot infer a meaningful type for T
32+
33+
Making instances of ``FunctionType`` subscriptable would allow for this constructor to
34+
be typed:
35+
36+
.. code-block:: python
37+
38+
reveal_type(make_list[int]()) # type is list[int]
39+
40+
Currently you have to use an assignment to provide a precise type:
41+
42+
.. code-block:: python
43+
44+
x: list[int] = make_list()
45+
reveal_type(x) # type is list[int]
46+
47+
but this code is unnecessarily verbose taking up multiple lines for a simple function
48+
call.
49+
50+
Similarly, ``T`` in this example cannot currently be meaningfully inferred, so ``x`` is
51+
untyped without an extra assignment:
52+
53+
.. code-block:: python
54+
55+
def factory[T](func: Callable[[T], Any]) -> Foo[T]: ...
56+
57+
reveal_type(factory(lambda x: "Hello World" * x))
58+
59+
If function objects were subscriptable, however, a more specific type could be given:
60+
61+
.. code-block:: python
62+
63+
reveal_type(factory[int](lambda x: "Hello World" * x)) # type is Foo[int]
64+
65+
Currently, with unspecialised literals, it is not possible to determine a type for
66+
situations similar to:
67+
68+
.. code-block:: python
69+
70+
def foo[T](x: list[T]) -> T: ...
71+
reveal_type(foo([])) # type checker cannot infer T (yet again)
72+
73+
.. code-block:: python
74+
75+
reveal_type(foo[int]([])) # type is int
76+
77+
It is also useful to be able to specify in cases in which a certain type must be passed
78+
to a function beforehand:
79+
80+
.. code-block:: python
81+
82+
words = ["hello", "world"]
83+
foo[int](words) # Invalid: list[str] is incompatible with list[int]
84+
85+
Allowing subscription makes functions and methods consistent with generic classes where
86+
they weren't already. Whilst all of the proposed changes can be implemented using
87+
callable generic classes, syntactic sugar would be highly welcome.
88+
89+
Due to this, specialising the function and using it as a new factory is fine
90+
91+
.. code-block:: python
92+
93+
make_int_list = make_list[int]
94+
reveal_type(make_int_list()) # type is list[int]
95+
96+
This proposal also opens the door to
97+
`monomorphisation <https://en.wikipedia.org/wiki/Monomorphization>`_ and
98+
`reified types <https://en.wikipedia.org/wiki/Reification_(computer_science)>`_
99+
100+
Rationale
101+
---------
102+
103+
Function objects in this PEP is used to refer to ``FunctionType``\ , ``MethodType``\ ,
104+
``BuiltinFunctionType``\ , ``BuiltinMethodType`` and ``MethodWrapperType``\ .
105+
106+
For ``MethodType`` you should be able to write:
107+
108+
.. code-block:: python
109+
110+
class Foo:
111+
def make_list[T](self, *args: T) -> list[T]: ...
112+
113+
Foo().make_list[int]()
114+
115+
and have it work similarly to a ``FunctionType``.
116+
117+
For ``BuiltinFunctionType``, so builtin generic functions (e.g. ``max`` and ``min``)
118+
work like ones defined in Python. Built-in functions should behave as much like
119+
functions implemented in Python as possible.
120+
121+
``BuiltinMethodType`` is the same type as ``BuiltinFunctionType``.
122+
123+
``MethodWrapperType`` (e.g. the type of ``object().__str__``) is useful for
124+
generic magic methods.
125+
126+
Specification
127+
-------------
128+
129+
Function objects should implement ``__getitem__`` to allow for subscription at runtime
130+
and return an instance of ``types.GenericAlias`` with ``__origin__`` set as the
131+
callable and ``__args__`` as the types passed.
132+
133+
Type checkers should support subscripting functions and understand that the parameters
134+
passed to the function subscription should follow the same rules as a generic callable
135+
class.
136+
137+
Setting ``__orig_class__``
138+
^^^^^^^^^^^^^^^^^^^^^^^^^^
139+
140+
Currently, ``__orig_class__`` is an attribute set in ``GenericAlias.__call__`` to the
141+
instance of the ``GenericAlias`` that created the called class e.g.
142+
143+
.. code-block:: python
144+
145+
class Foo[T]: ...
146+
147+
assert Foo[int]().__orig_class__ == Foo[int]
148+
149+
Currently, ``__orig_class__`` is unconditionally set; however, to avoid potential
150+
erasure on any created instances, this attribute should not be set if ``__origin__`` is
151+
an instance of any function object.
152+
153+
The following code snippet would fail at runtime without this change as
154+
``__orig_class__`` would be ``bar[str]`` and not ``Foo[int]``.
155+
156+
.. code-block:: python
157+
158+
def bar[U]():
159+
return Foo[int]()
160+
161+
assert bar[str]().__orig_class__ is Foo[int]
162+
163+
Backwards Compatibility
164+
-----------------------
165+
Currently these classes are not subclassable and so there are no backwards
166+
compatibility concerns with regards to classes already implementing
167+
``__getitem__``.
168+
169+
Reference Implementation
170+
------------------------
171+
172+
The runtime changes proposed can be found here
173+
https://github.com/Gobot1234/cpython/tree/function-subscript
174+
175+
Acknowledgements
176+
----------------
177+
178+
Thank you to Alex Waygood and Jelle Zijlstra for their feedback on this PEP and Guido
179+
for some motivating examples.
180+
181+
Copyright
182+
---------
183+
184+
This document is placed in the public domain or under the CC0-1.0-Universal license,
185+
whichever is more permissive.

0 commit comments

Comments
 (0)