typing
typing copied to clipboard
Shorthand for async function
Following on from #424, it would be nice to have something like AsyncFunction[[Args], ReturnType] instead of Callable[[Args], Coroutine[ReturnType]].
TBH, I don't think this is a big win in terms of readability or number of keystrokes. I think however we can keep this open to see if anyone else is interested in such "shorthand", since implementing this is pretty easy.
If this does ever get approved, it should be noted that GeneratorFunction could be useful too.
I would like to provide a simple sample that literally cries for a shorthand.
This is a sample from aiohttp.web docs https://docs.aiohttp.org/en/stable/web_advanced.html#middleware-factory which when annotated looks like this:
import typing
from aiohttp import web
def middleware_factory(
text: str
) -> typing.Callable[
[web.Request, typing.Callable[[web.Request], typing.Awaitable[web.Response]]],
typing.Awaitable[web.Response],
]:
@web.middleware
async def sample_middleware(
request: web.Request,
handler: typing.Callable[[web.Request], typing.Awaitable[web.Response]],
) -> web.Response:
resp = await handler(request)
resp.text = (resp.text if resp.text is not None else '') + text
return resp
return sample_middleware
The shorthand would probably remove only typing.Awaitable things but it looks like the code will look quite a bit more understandable.
I think an additional readability benefit beyond character count is that the async vs not-async distinction is front-loaded in the spec. Currently you must look at the end of the Callable declaration to know whether it's an async function. Consider the following type specs:
F1 = Callable[[Dict[str, Any], str, Any], Iterable[str]]
F2 = Callable[[Dict[str, Any], str, Any], Awaitable[str]]
When I read this, by the time I get through the parameter spec my brain is telling me that F1 and F2 are really similar and can be called the same way, even though I have to read one step further to learn that the return value indicates that the latter must be awaited. IMO we should front load this information so that the difference is much more obvious:
F1 = Callable[[Dict[str, Any], str, Any], Iterable[str]]
F2 = AsyncCallable[[Dict[str, Any], str, Any], str]
To get a better idea of how big the impact might be, it would be interesting to see some statistics about how frequently this could be used in some non-trivial real-world async codebase. For example, if there are two instances in a 5000-line codebase where this would help, the benefit would be pretty marginal, but if there are 50 instances, this could be a big potential win.
Example Codebase: Starlette
$ find starlette/ -name '*.py' | xargs wc -l
[...]
10151 total
$ grep -re 'Callable\[.*Awaitable.*\]' starlette/ | wc -l
6
6 instances in a 10k line codebase, so perhaps not a massive priority, but I see no reason not to add it?
Example Codebase: Faust
$ find faust/ -name '*.py' | xargs wc -l
[...]
62709 total
$ grep -re 'Callable\[.*Awaitable.*\]' faust/ | wc -l
26
26 instances in a 62k line codebase, so like @retnikt said, perhaps not a massive priority.
That said, @rmorshea's argument really speaks to me. Even if AsyncCallable[[Args], ReturnType] is just a shorthand for Callable[[Args], Awaitable[ReturnType]] I think there are some major readability benefits. I'd personally prefer to write the former if possible.
Unless I'm mistaken there may already be some prior art in the form of names like AsyncIterable, AsyncContextManager, AsyncGenerator and friends.
The question is how often there is # type: ignore in such a place as its almost impossible to guess this correctly or write directly from one's memory.