-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
PEP 718: Specify binding, parametrisation and overload interactions #4649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a33030f
475bd8e
bd36a62
1ba67e9
4d33729
7e2dd28
6ac59eb
c6b4588
382eb92
3baa5be
8311622
75ca1ad
109fddd
9e581a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| 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 | ||
|
|
@@ -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 | ||
| 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] | ||
|
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]: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be augmented with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
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]: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, this signature is inconsistent with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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: | ||
|
|
@@ -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. | ||
|
|
@@ -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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the complexity and ambiguity already present in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.