Thanks for thinking more deeply about how we can make type annotations for callables more user friendly.
The intersection between the syntax for general Python code and type annotations has been a topic of discussion within the Steering Council (on and off) for quite some time. We intend to provide more clarity about our thinking, probably in the form of an informational PEP, around this topic, and my apologies because I haven’t had time to draft it yet.
But since this came up, I want to at least say that we are unanimous in our belief that the type annotation language must also be standard Python syntax. In other words, we are not in favor of letting type annotation syntax deviate from Python syntax. As a effect of this, the PSC does not intend to issue a blanket delegation of type-focused PEPs. Of course, individual typing PEPs can be delegated just like any other PEP, but especially where syntax change proposals are made, I expect that the PSC will want keep final decision making.
This means that any PEP which proposes syntactic changes to support typing features must also address the implications of those syntax changes for the general Python language. PEP 646 (Variadic Generics) is a good example of this. Early on we recognized that the proposed syntax change had implications for Python, and we asked the PEP authors to address those implications for Python, which they added:
This is the kind of analysis the PSC needs in order to weigh the cost and benefits of making such changes to Python.
TL;RA - One Syntax to Rule Them All
On Oct 7, 2021, at 09:41, S Pradeep Kumar email@example.com wrote:
Typing-sig has been discussing user-friendly syntax for the type used to represent callables.  Since this affects the Python language syntax, we wanted to get some high-level feedback from you before putting up a detailed PEP.
TL;DR: We want to propose syntax for Callable types, e.g., `(int, str) -> bool` instead of `typing.Callable[[int, str], bool]`. Questions: 1. Are there concerns we need to keep in mind about such a syntax change? 2. Should we propose syntax for additional features in the same PEP or in a future PEP?
Guido has pointed out that `Callable` is one of the most frequently used type forms but has the least readable syntax. For example, say we have a callback `formatter` that accepts a record and a list of permissions:
def print_purchases( user, formatter, # <-- callback ): <...> output = formatter(record, permissions) print(output)
To give it a type, we currently have to write:
from typing import Callable def print_purchases( user: User, formatter: Callable[[PurchaseRecord, List[AuthPermission]], FormattedItem] # Hard to read. ) -> None: <...> output = formatter(record, permissions) print(output)
`Callable` can be hard to understand for new users since it doesn't resemble existing function signatures and there can be multiple square brackets. It also requires an import from `typing`, which is inconvenient. Around 40% of the time , users just give up on precisely typing the parameters and return type of their callbacks and just leave it as a blank Callable. In other cases, they don't add a type for their callback at all. Both mean that they lose the safety guarantees from typing and leave room for bugs.
We believe that adding friendly syntax for Callable types will improve readability and encourage users to type their callbacks more precisely. Other modern, gradually-typed languages like TypeScript (JS), Hack (PHP), etc. have special syntax for Callable types.
(As a precedent, PEP 604 recently added clean, user-friendly syntax for the widely-used `Union` type. Instead of importing `Union` and writing `expr: Union[AddExpr, SubtractExpr], we can just write `expr: AddExpr | SubtractExpr`.)
# Proposal and Questions
We have a two-part proposal and questions for you:
- Syntax to replace Callable
After a lot of discussion, there is strong consensus in typing-sig about adding syntax to replace Callable. So, the above example would be written as:
def print_purchases( user: User, formatter: (PurchaseRecord, List[AuthPermission]) -> FormattedItem, ) -> None: <...> output = formatter(record, permissions) print(output)
This feels more natural and succinct.
Async callbacks currently need to be written as
from typing import Callable async_callback: Callable[[HttpRequest], Awaitable[HttpResponse]]
With the new syntax, they would be written as
async_callback: async (HttpRequest) -> HttpResponse
which again seems more natural. There is similar syntax for the type of decorators that pass on *args and **kwargs to the decorated function.
Note that we considered and rejected using a full def-signature syntax like
(record: PurchaseRecord, permissions: List[AuthPermission], /) -> FormattedItem
because it would be more verbose for common cases and could lead to subtle bugs; more details in .
The Callable type is also usable as an expression, like in type aliases `IntOperator = (int, int) -> int` and `cast((int) -> int, f)` calls.
**Question 1**: Are there concerns we should keep in mind about such a syntax proposal?
- Syntax for callback types beyond Callable
`Callable` can't express the type of all possible callbacks. For example, it doesn't support callbacks where some parameters have default values: `formatter(record)` (the user didn't pass in `permissions`). It *is* possible to express these advanced cases using Callback Protocols (PEP 544)  but it gets verbose.
There are two schools of thought on typing-sig on adding more syntax on top of (1):
(a) Some, including Guido, feel that it would be a shame to not have syntax for core Python features like default values, keyword arguments, etc.
One way to represent default values or optionally name parameters would be: ``` # permissions is optional formatter: (PurchaseRecord, List[AuthPermission]=...) -> FormattedItem # permissions can be called using a keyword argument. formatter: (PurchaseRecord, permissions: List[AuthPermission]) -> FormattedItem ``` There are also alternative syntax proposals.
(b) Others want to wait till we have more real-world experience with the syntax in (1).
The above cases occur < 5% of the time in typed or untyped code . And the syntax in (1) is forward-compatible with the additional proposals. So we could add them later if needed or leave them out, since we can always use callback protocols.
**Question 2**: Do you have preferences either way? Do we propose (1) alone or (1) + (2)?
Once we get some high-level feedback here, we will draft a PEP going into the details for various use cases.
Best, Pradeep Kumar Srinivasan Steven Troxler Eric Traut
PS: We've linked to more details below. Happy to provide more details as needed.
: typing-sig thread about the proposal: https://firstname.lastname@example.org/message/JZLYRAXJ... : Stats about loosely-typed Callables: https://github.com/pradeep90/annotation_collector#typed-projects---callable-... : Comparison and rejection of proposals: https://www.dropbox.com/s/sshgtr4p30cs0vc/Python%20Callable%20Syntax%20Propo... : Callback protocols: https://www.python.org/dev/peps/pep-0544/#callback-protocols : Stats on callbacks not expressible with Callable: https://drive.google.com/file/d/1k_TqrNKcbWihRZdhMGf6K_GcLmn9ny3m/view?usp=s... _______________________________________________ Python-Dev mailing list -- email@example.com To unsubscribe send an email to firstname.lastname@example.org https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://email@example.com/message/VBHJOS3L... Code of Conduct: http://python.org/psf/codeofconduct/