Hm, and only two of these issues (if I’m not mistaken) are related to overload
(so have implementation body to check). Thanks for this context - it really means that something hould be solved on a deeper level.
Now, what will go wrong if we declare that {AsyncIterator, AsyncGenerator}
and probably Coroutine
and Awaitable
as well are never wrapped? If the spec can be formulated as “any return type except those is assumed to be implicitly wrapped with Coroutine
”, it obviously breaks backwards compat, but brings more freedom in definitions.
First of all, anyone who is now using async def fn() -> Coroutine[...]
to indicate a “twice awaitable” something will be harmed. However, this is hardly a popular scenario, and probably breakage to this extent is permitted.
The main question is: can any user writing an async
function with non-generator body (we can’t say “trivial” any longer if we want to apply that logic unconditionally) and AsyncIterator
return type really mean implicit Coroutine
? If we also include Coroutine
itself into non-wrapped types, can any user reasonably want a function that returns a “twice awaitable” object? Are there any use cases for this pattern? If there are, how difficult it is to recover from this behaviour change?
Trying to self-answer this: I can’t imagine a production setting where such pathological cases arise. Any such use can be trivially recovered by wrapping with Coroutine
by hand. IMO, this change can be justified, perhaps hidden under a typechecker flag for a couple of minor releases.
Extra benefit: new/inexperienced users coming from TypeScript background are used to explicit Promise
for any async
function return type. So telling them that it is mandatory in some cases won’t be a big surprise.
This introduces a point of confusion. Two definitions below will become equivalent under new rules:
async def fn() -> Coroutine[Any, Any, int]: ...
async def fn() > int: ...
However, this confusion is at least “safe”. Shall someone decide to wrap everything async
with a Coroutine
or Awaitable
, it won’t do any harm. This may be deferred to linters like ruff
or flake8
, warning about unnecessary explicit Coroutine
wrapper. Since this will be purely a style issue, not providing “one and preferably only one way” looks justified to me.
I just attempted an implementation, and mypy_primer
quickly reminded me that things aren’t that trivial. When we decide to apply this rule conditionally, a lot of problems arise. What about unbound typevars, where we don’t know if something is awaitable? What about union types of mixed kinds (e.g. Iterator[Any] | AsyncIterator[Any]
, like here sorry wrong link here, where ContentStream
is a typing.Iterable[Content] | typing.AsyncIterable[Content]
, and it could easily have been Iterator | AsyncIterator
instead)? What about Any
itself? I started to think that this issue mandates a whole PEP to agree on all corner cases…