Skip to content

Commit 6ac59eb

Browse files
committed
final round of changes
1 parent 7e2dd28 commit 6ac59eb

File tree

1 file changed

+123
-33
lines changed

1 file changed

+123
-33
lines changed

peps/pep-0718.rst

Lines changed: 123 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
PEP: 718
22
Title: Subscriptable functions
3-
Author: James Hilton-Balfe <gobot1234yt@gmail.com>
3+
Author: James Hilton-Balfe <gobot1234yt@gmail.com>, Pablo Ruiz Cuevas <pablo.r.c@live.com>
44
Sponsor: Guido van Rossum <guido@python.org>
55
Discussions-To: https://discuss.python.org/t/28457/
66
Status: Draft
@@ -17,41 +17,113 @@ This PEP proposes making function objects subscriptable for typing purposes. Doi
1717
gives developers explicit control over the types produced by the type checker where
1818
bi-directional inference (which allows for the types of parameters of anonymous
1919
functions to be inferred) and other methods than specialisation are insufficient. It
20-
also brings functions in line with regular classes in their ability to be
21-
subscriptable.
20+
also makes functions consistent with regular classes in their ability to be
21+
subscripted.
2222

2323
Motivation
2424
----------
2525

26-
Unknown Types
27-
^^^^^^^^^^^^^
26+
Currently, classes allow passing type annotations for generic containers, this
27+
is especially useful in common constructors such as ``list``\, ``tuple`` and ``dict``
28+
etc.
2829

29-
Currently, it is not possible to infer the type parameters to generic functions in
30-
certain situations:
30+
.. code-block:: python
31+
32+
my_integer_list = list[int]()
33+
reveal_type(my_integer_list) # type is list[int]
34+
35+
At runtime ``list[int]`` returns a ``GenericAlias`` that can be later called, returning
36+
an empty list.
37+
38+
Another example of this is creating a specialised ``dict`` type for a section of our
39+
code where we want to ensure that keys are ``str`` and values are ``int``:
40+
41+
.. code-block:: python
42+
43+
NameNumberDict = dict[str, int]
44+
45+
NameNumberDict(
46+
one=1,
47+
two=2,
48+
three="3" # Invalid: Literal["3"] is not of type int
49+
)
50+
51+
In spite of the utility of this syntax, when trying to use it with a function, an error
52+
is raised, as functions are not subscriptable.
53+
54+
.. code-block:: python
55+
56+
def my_list[T](arr) -> list[T]:
57+
# do something...
58+
return list(arr)
59+
60+
my_integer_list = my_list[int]() # TypeError: 'function' object is not subscriptable
61+
62+
There are a few workarounds:
63+
64+
1. Making a callable class:
65+
66+
.. code-block:: python
67+
68+
class my_list[T]:
69+
def __call__(self, *args: T) -> list[T]:
70+
# do something...
71+
return list(args)
72+
73+
2. Using :pep:`747`\'s TypeForm, with an extra unused argument:
74+
75+
.. code-block:: python
76+
77+
from typing import TypeForm
78+
79+
def my_list(*args: T, typ: TypeForm[T]) -> list[T]:
80+
# do something...
81+
return list(args)
82+
83+
As we can see this solution increases the complexity with an extra argument.
84+
Additionally it requires the user to understand a new concept ``TypeForm``.
85+
86+
3. Annotating the assignment:
3187

3288
.. code-block:: python
3389
34-
def make_list[T](*args: T) -> list[T]: ...
35-
reveal_type(make_list()) # type checker cannot infer a meaningful type for T
90+
my_integer_list: list[int] = my_list()
91+
92+
This solution isn't optimal as the return type is repeated and is more verbose and
93+
would require the type updating in multiple places if the return type changes.
94+
95+
In conclusion, the current workarounds are too complex or verbose, especially compared
96+
to syntax that is consistent with the rest of the language.
3697

37-
Making instances of ``FunctionType`` subscriptable would allow for this constructor to
38-
be typed:
98+
Generic Specialisation
99+
^^^^^^^^^^^^^^^^^^^^^^
100+
101+
As in the previous example currently we can create generic aliases for different
102+
specialised usages:
39103

40104
.. code-block:: python
41105
42-
reveal_type(make_list[int]()) # type is list[int]
106+
NameNumberDict = dict[str, int]
107+
NameNumberDict(one=1, two=2, three="3") # Invalid: Literal["3"] is not of type int``
43108
44-
Currently you have to use an assignment to provide a precise type:
109+
This not currently possible for functions but if allowed we could easily
110+
specialise operations in certain sections of the codebase:
45111

46112
.. code-block:: python
47113
48-
x: list[int] = make_list()
49-
reveal_type(x) # type is list[int]
114+
def constrained_addition[T](a: T, b: T) -> T: ...
50115
51-
but this code is unnecessarily verbose taking up multiple lines for a simple function
52-
call.
116+
# where we work exclusively with ints
117+
int_addition = constrained_addition[int]
118+
int_addition(2, 4+8j) # Invalid: complex is not of type int
53119
54-
Similarly, ``T`` in this example cannot currently be meaningfully inferred, so ``x`` is
120+
Unknown Types
121+
^^^^^^^^^^^^^
122+
123+
Currently, it is not possible to infer the type parameters to generic functions in
124+
certain situations.
125+
126+
In this example ``T`` cannot currently be meaningfully inferred, so ``x`` is
55127
untyped without an extra assignment:
56128

57129
.. code-block:: python
@@ -66,11 +138,11 @@ If function objects were subscriptable, however, a more specific type could be g
66138
67139
reveal_type(factory[int](lambda x: "Hello World" * x)) # type is Foo[int]
68140
69-
Undecidable Inference
70-
^^^^^^^^^^^^^^^^^^^^^
141+
Undecidable Inference and Type Narrowing
142+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
71143

72-
There are even cases where subclass relations make type inference impossible. However,
73-
if you can specialise the function type checkers can infer a meaningful type.
144+
There are cases where subclass relations make type inference impossible. However, if
145+
you can specialise the function type checkers can infer a meaningful type.
74146

75147
.. code-block:: python
76148
@@ -138,7 +210,16 @@ The syntax for such a feature may look something like:
138210
Rationale
139211
---------
140212

141-
Function objects in this PEP is used to refer to ``FunctionType``\ , ``MethodType``\ ,
213+
This proposal improves the consistency of the type system, by allowing syntax that
214+
already looks and feels like a natural of the existing syntax for classes.
215+
216+
If accepted, this syntax will reduce the necessity to learn about :pep:`747`\s
217+
``TypeForm``, reduce verbosity and cognitive load of safely typed python.
218+
219+
Specification
220+
-------------
221+
222+
In this PEP "Function objects" is used to refer to ``FunctionType``\ , ``MethodType``\ ,
142223
``BuiltinFunctionType``\ , ``BuiltinMethodType`` and ``MethodWrapperType``\ .
143224

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

164-
Specification
165-
-------------
166-
167245
Function objects should implement ``__getitem__`` to allow for subscription at runtime
168246
and return an instance of ``types.GenericAlias`` with ``__origin__`` set as the
169247
callable and ``__args__`` as the types passed.
@@ -201,19 +279,31 @@ The following code snippet would fail at runtime without this change as
201279
Interactions with ``@typing.overload``
202280
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
203281

204-
Overloaded functions should work much the same as they already do, since they do not
205-
affect the runtime type. Explicit specialisation will restrict the set of available
206-
overloads.
282+
This PEP opens the door to overloading based on type variables:
207283

208284
.. code-block:: python
209285
210286
@overload
211-
def make[T](x: T) -> T: ...
287+
def serializer_for[T: str]() -> StringSerializer: ...
212288
@overload
213-
def make(x: str, y: str) -> tuple[int, int]: ...
289+
def serializer_for[T: list]() -> ListSerializer: ...
290+
291+
def serializer_for():
292+
...
293+
294+
For overload resolution a new step will be required previous to any other, where the resolver
295+
will match only the overloads where the subscription may succeed.
296+
297+
.. code-block:: python
298+
299+
@overload
300+
def make[*Ts]() -> float: ...
301+
@overload
302+
def make[T]() -> int: ...
303+
304+
make[int] # matches first and second overload
305+
make[int, str] # matches only first
214306
215-
reveal_type(make[int](1)) # type is int
216-
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
217307
218308
Functions Parameterized by ``TypeVarTuple``\ s
219309
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

0 commit comments

Comments
 (0)