The right way to type hint a Coroutine function?
Asked Answered
H

3

32

I cannot wrap my head around type hinting a Coroutine. As far as I understand, when we declare a function like so:

async def some_function(arg1: int, arg2: str) -> list:
    ...

we effectively declare a function, which returns a coroutine, which, when awaited, returns a list. So, the way to type hint it would be:

f: Callable[[int, str], Coroutine[???]] = some_function

But Coroutine generic type has 3 arguments! We can see it if we go to the typing.py file:

...
Coroutine = _alias(collections.abc.Coroutine, 3)
...

There is also Awaitable type, which logically should be a parent of Coroutine with only one generic parameter (the return type, I suppose):

...
Awaitable = _alias(collections.abc.Awaitable, 1)
...

So maybe it would be more or less correct to type hint the function this way:

f: Callable[[int, str], Awaitable[list]] = some_function

Or is it?

So, basically, the questions are:

  1. Can one use Awaitable instead of Coroutine in the case of type hinting an async def function?
  2. What are the correct parameters for the Coroutine generic type and what are its use-cases?
Hittel answered 4/8, 2022 at 18:17 Comment(1)
1. See an example of type hinting of async function in docs. So, you're doing it right. 2. Check the doc with an example. See alsoNettie
S
21

As the docs state:

Coroutine objects and instances of the Coroutine ABC are all instances of the Awaitable ABC.

And for the Coroutine type:

A generic version of collections.abc.Coroutine. The variance and order of type variables correspond to those of Generator.

Generator in turn has the signature Generator[YieldType, SendType, ReturnType]. So if you want to preserve that type information, use Coroutine, otherwise Awaitable should suffice.

Scribbler answered 4/8, 2022 at 18:29 Comment(1)
The big problem with Awaitable is that it's not recognised by type hinters in places where a coroutine is expected. For example loop.create_task(). This has a knock on effect that linters will warn you if you call a coroutine without awaiting it but they will NOT warn if you call an regular function returning Awaitable if you fail to await it. IE you should avoid Awaitable for code safety.Buddhi
I
16

The Coroutine type takes the same signature as the Generator type:

Generator[YieldType, SendType, ReturnType]

Since the result of an unawaited call to a coroutine is an awaitable (it can be used in an await expression), it can be type hinted with:

Awaitable[ReturnType]

Example:

async def some_function(arg1: int, arg2: str) -> List[str]:
    return ['foo']

coro: Awaitable[List[str]] = some_function(1, 'bar')
result = await coro
print(result)
# prints ['foo']

However, for type hinting a coroutine, I do not find either of these useful on their own. Instead, I opt for something similar to what you stated in your last example:

def return_coro() -> Callable[[int, str], Awaitable[List[str]]]:
    async def some_function(arg1: int, arg2: str) -> List[str]:
        return ['foo']
    return some_function

Note mypy will get upset if you try to pass this into a function which explicitly expects a Coroutine (such as asyncio.run()).

Incorporeity answered 13/2, 2023 at 16:18 Comment(0)
T
5

The corresponding type for a coroutine function

async def some_function(arg1: A, arg2: B, /) -> R: ...

is Callable[[A, B], Coroutine[Any, Any, R]]. The return type is the third argument of Coroutine; the other arguments are implementation details of the async framework used so Any is appropriate for portability and compatibility.
This can simply be checked by using typing.reveal_type(some_function) with a type checker. For example, MyPy reveals (using its Callable shorthand notation):

Revealed type is "def [A, B, R] (A`-1, B`-2) -> typing.Coroutine[Any, Any, R`-3]"

If the full parameter specification, documentation, or simply no reliance on Coroutine is desired, a Protocol with async def __call__ can be used instead:

class SomeFunctionType(Protocol):
    async def __call__(self, arg1: A, arg2: B, /) -> R: ...

While it is often possible to use Awaitable[R] instead of Coroutine[Any, Any, R] this should be avoided when explicitly describing coroutine functions: An Awaitable[R] only expresses the limited feature set that Coroutine[Any, Any, R] shares with all other awaitables. Various important async framework functions, such as asyncio.run or asyncio.create_task, rely on the full feature set and thus only work with actual Coroutines.

As a rule of thumb, use Awaitable to be lenient with parameters accepted but use Coroutine to be precise for objects provided.

Terhune answered 30/1, 2024 at 12:11 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.