Skip to content

Commit 17dcc42

Browse files
Gobot1234JelleZijlstrahugovkgvanrossumPabloRuizCuevas
authored
PEP 718: Specify binding, parametrisation and overload interactions (#4649)
* hopefully final round of changes * I think fix the build issues? Sorry I can't test I can't get make installed * Fix title too short * fix some grammar typos and build * Update peps/pep-0718.rst Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com> * final round of changes * Split long sentence in two for style Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com> * Update pep-0718.rst Adresed @gvanrossum comments * Update pep-0718.rst Small change * fix line wrap * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * more line wrap --------- Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: Guido van Rossum <gvanrossum@gmail.com> Co-authored-by: Pablo <48098178+PabloRuizCuevas@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 238e117 commit 17dcc42

1 file changed

Lines changed: 167 additions & 38 deletions

File tree

peps/pep-0718.rst

Lines changed: 167 additions & 38 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,68 +17,132 @@ 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``:
3140

3241
.. code-block:: python
3342
34-
def make_list[T](*args: T) -> list[T]: ...
35-
reveal_type(make_list()) # type checker cannot infer a meaningful type for T
43+
NameNumberDict = dict[str, int]
3644
37-
Making instances of ``FunctionType`` subscriptable would allow for this constructor to
38-
be typed:
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.
3953

4054
.. code-block:: python
4155
42-
reveal_type(make_list[int]()) # type is list[int]
56+
def my_list[T](arr: Iterable[T]) -> list[T]:
57+
# do something...
58+
return list(arr)
59+
60+
my_integer_list = my_list[int]() # TypeError: 'function' object is not subscriptable
4361
44-
Currently you have to use an assignment to provide a precise type:
62+
There are a few workarounds:
63+
64+
1. Making a callable class:
4565

4666
.. code-block:: python
4767
48-
x: list[int] = make_list()
49-
reveal_type(x) # type is list[int]
68+
class my_list[T]:
69+
def __call__(self, arr: Iterable[T]) -> list[T]:
70+
# do something...
71+
return list(arr)
5072
51-
but this code is unnecessarily verbose taking up multiple lines for a simple function
52-
call.
73+
my_string_list = my_list[str]([])
5374
54-
Similarly, ``T`` in this example cannot currently be meaningfully inferred, so ``x`` is
55-
untyped without an extra assignment:
75+
2. Using :pep:`747`\'s TypeForm, with an extra unused argument:
5676

5777
.. code-block:: python
5878
59-
def factory[T](func: Callable[[T], Any]) -> Foo[T]: ...
79+
from typing import TypeForm
6080
61-
reveal_type(factory(lambda x: "Hello World" * x))
81+
def my_list(*arr: Iterable[T], typ: TypeForm[T]) -> list[T]:
82+
# do something...
83+
return list(arr)
6284
63-
If function objects were subscriptable, however, a more specific type could be given:
85+
my_string_list = my_list([], str)
86+
87+
As we can see this solution increases the complexity with an extra argument.
88+
Additionally it requires the user to understand a new concept ``TypeForm``.
89+
90+
3. Annotating the assignment:
6491

6592
.. code-block:: python
6693
67-
reveal_type(factory[int](lambda x: "Hello World" * x)) # type is Foo[int]
94+
my_integer_list: list[int] = my_list()
95+
96+
This solution isn't optimal as the return type is repeated, is more verbose and would
97+
require the type updating in multiple places if the return type changes. Additionally,
98+
it adds unnecesary and distracting verbossity when the intention is to pass the
99+
specialized value into another call.
100+
101+
In conclusion, the current workarounds are too complex or verbose, especially compared
102+
to syntax that is consistent with the rest of the language.
68103

69-
Undecidable Inference
70-
^^^^^^^^^^^^^^^^^^^^^
104+
Generic Specialisation
105+
^^^^^^^^^^^^^^^^^^^^^^
71106

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.
107+
As in the previous example currently we can create generic aliases for different
108+
specialised usages:
74109

75110
.. code-block:: python
76111
77-
def foo[T](x: Sequence[T] | T) -> list[T]: ...
112+
NameNumberDict = dict[str, int]
113+
NameNumberDict(one=1, two=2, three="3") # Invalid: Literal["3"] is not of type int``
78114
79-
reveal_type(foo[bytes](b"hello"))
115+
This not currently possible for functions but if allowed we could easily
116+
specialise operations in certain sections of the codebase:
80117

81-
Currently, type checkers do not consistently synthesise a type here.
118+
.. code-block:: python
119+
120+
def constrained_addition[T](a: T, b: T) -> T: ...
121+
122+
# where we work exclusively with ints
123+
int_addition = constrained_addition[int]
124+
int_addition(2, 4+8j) # Invalid: complex is not of type int
125+
126+
Unknown Types
127+
^^^^^^^^^^^^^
128+
129+
Currently, it is not possible to infer the type parameters to generic functions in
130+
certain situations.
131+
132+
In this example ``T`` cannot currently be meaningfully inferred, so ``x`` is
133+
untyped without an extra assignment:
134+
135+
.. code-block:: python
136+
137+
def factory[T](func: Callable[[T], Any]) -> Foo[T]: ...
138+
139+
reveal_type(factory(lambda x: "Hello World" * x)) # type is Foo[Unknown]
140+
141+
If function objects were subscriptable, however, a more specific type could be given:
142+
143+
.. code-block:: python
144+
145+
reveal_type(factory[int](lambda x: "Hello World" * x)) # type is Foo[int]
82146
83147
Unsolvable Type Parameters
84148
^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -138,7 +202,16 @@ The syntax for such a feature may look something like:
138202
Rationale
139203
---------
140204

141-
Function objects in this PEP is used to refer to ``FunctionType``\ , ``MethodType``\ ,
205+
This proposal improves the consistency of the type system, by allowing syntax that
206+
already looks and feels like a natural of the existing syntax for classes.
207+
208+
If accepted, this syntax will reduce the necessity to learn about :pep:`747`\s
209+
``TypeForm``, reduce verbosity and cognitive load of safely typed python.
210+
211+
Specification
212+
-------------
213+
214+
In this PEP "Function objects" is used to refer to ``FunctionType``\ , ``MethodType``\ ,
142215
``BuiltinFunctionType``\ , ``BuiltinMethodType`` and ``MethodWrapperType``\ .
143216

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

164-
Specification
165-
-------------
166-
167237
Function objects should implement ``__getitem__`` to allow for subscription at runtime
168238
and return an instance of ``types.GenericAlias`` with ``__origin__`` set as the
169239
callable and ``__args__`` as the types passed.
@@ -201,10 +271,69 @@ The following code snippet would fail at runtime without this change as
201271
Interactions with ``@typing.overload``
202272
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
203273

204-
Overloaded functions should work much the same as already, since they have no effect on
205-
the runtime type. The only change is that more situations will be decidable and the
206-
behaviour/overload can be specified by the developer rather than leaving it to ordering
207-
of overloads/unions.
274+
This PEP allows type checkers to do overloading based on type variables:
275+
276+
.. code-block:: python
277+
278+
@overload
279+
def serializer_for[T: str]() -> StringSerializer: ...
280+
@overload
281+
def serializer_for[T: list]() -> ListSerializer: ...
282+
283+
def serializer_for():
284+
...
285+
286+
For overload resolution a new step will be required previous to any other, where the resolver
287+
will match only the overloads where the subscription may succeed.
288+
289+
.. code-block:: python
290+
291+
@overload
292+
def make[*Ts]() -> float: ...
293+
@overload
294+
def make[T]() -> int: ...
295+
296+
make[int] # matches first and second overload
297+
make[int, str] # matches only first
298+
299+
300+
Functions Parameterized by ``TypeVarTuple``\ s
301+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
302+
Currently, type checkers disallow the use of multiple ``TypeVarTuple``\s in their
303+
generic parameters; however, it is currently valid to have a function as such:
304+
305+
.. code-block:: python
306+
307+
def foo[*T, *U](bar: Bar[*T], baz: Baz[*U]): ...
308+
def spam[*T](bar: Bar[*T]): ...
309+
310+
This PEP does not allow functions like ``foo`` to be subscripted, for the same reason
311+
as defined in :pep:`PEP 646<646#multiple-type-variable-tuples-not-allowed>`, the type
312+
variables cannot be resolved unambiguously with the current syntax.
313+
314+
.. code-block:: python
315+
316+
foo[int, str, bool, complex](Bar(), Baz()) # Invalid: cannot determine which parameters are passed to *T and *U. Explicitly parameterise the instances individually
317+
spam[int, str, bool, complex](Bar()) # OK
318+
319+
Binding Rules
320+
^^^^^^^^^^^^^
321+
Method subscription (including ``classmethods`` and ``staticmethods``), should only
322+
allow their function's type parameters and not the enclosing class's.
323+
Subscription should follow the rules specified in :pep:`PEP 696<696#binding-rules>`;
324+
methods should bind type parameters on attribute access.
325+
326+
.. code-block:: python
327+
328+
class C[T]:
329+
def method[U](self, x: T, y: U): ...
330+
@classmethod
331+
def cls[U](cls, x: T, y: U): ...
332+
333+
C[int].method[str](0, "") # OK
334+
C[int].cls[str](0, "") # OK
335+
C.cls[int, str](0, "") # Invalid: too many type parameters
336+
C.cls[str](0, "") # OK, U will be matched to str
208337
209338
Backwards Compatibility
210339
-----------------------

0 commit comments

Comments
 (0)