11PEP: 718
22Title: 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>
44Sponsor: Guido van Rossum <guido@python.org>
55Discussions-To: https://discuss.python.org/t/28457/
66Status: Draft
@@ -17,41 +17,113 @@ This PEP proposes making function objects subscriptable for typing purposes. Doi
1717gives developers explicit control over the types produced by the type checker where
1818bi-directional inference (which allows for the types of parameters of anonymous
1919functions 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
2323Motivation
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
55127untyped 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
144225For ``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
162243generic magic methods.
163244
164- Specification
165- -------------
166-
167245Function objects should implement ``__getitem__ `` to allow for subscription at runtime
168246and return an instance of ``types.GenericAlias `` with ``__origin__ `` set as the
169247callable 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