1

In my research, I see the general consensus for the correct way to typehint an async function is Callable[..., Awaitable[Any]].

In Pycharm, I try this and have this issue when passing to asyncio.create_task

import asyncio
from typing import Callable, Awaitable, Any

def fff(ccc: Callable[..., Awaitable[Any]]):
    return asyncio.create_task(ccc())

enter image description here

Is this an issue with Pycharm, or should I be typehinting my async functions another way?

Shuri2060
  • 729
  • 6
  • 21

1 Answers1

2

Let's break things down to pieces:

  1. When we declare an asynchronous function that returns some value - we basically create a function as this example shows (Python 3.11):
async def ccc():
    return "Hello World"

type(ccc) # <--- function
  1. This function returns an object of type coroutine
async def ccc():
    return "Hello World"

type(ccc()) # <--- coroutine, not str!

That is because this asynchronous function needs to be awaited in order to really return what we've initially wanted it to return - a string.

Effectively, calling a coroutine inside an asynchronous function will not do anything as the docs state:

We say that an object is an awaitable object if it can be used in an await expression. Many asyncio APIs are designed to accept awaitables.

Meaning that your ccc function is indeed callable which gets some "%d" parameters (hence the ellipsis), which returns a coroutine.

So to say the least, the type hinting is something as such:

Callable[..., Coroutine]

And you can be more explicit with the Coroutine as I don't know what ccc returns.

Moreover, I am not sure how you want to call fff as it is not asynchronous, you will need to make it such and await the asyncio.create_task as such:

import asyncio
from typing import Callable, Coroutine


async def ccc():
    print("Hello")
    await asyncio.sleep(5)
    print("World")
    return 12345


async def fff(random_callable: Callable[..., Coroutine]):
    result = await asyncio.create_task(random_callable())
    print(f"{result=}")


if __name__ == '__main__':
    el = asyncio.get_event_loop()
    el.run_until_complete(fff(ccc))
    el.close()

Note: I created ccc randomly for my own case of explanation, as you did not provide it in the question itself :-)

CodeCop
  • 1
  • 2
  • 15
  • 37
  • Right understood, thank you for the clarification. `ccc` was meant to be a generic `async def` function (like your example). `asyncio.create_task` not being awaited and `fff` not being `async` is because I was creating a minimal example of a background task (in actuality `ccc` the task will be garbage collected if I don't do anything with it). Edited OP so that `fff` returns it – Shuri2060 Jan 10 '23 at 19:16
  • But what I also wanted to know is why answers (and comments) such as this say you 'should' use Awaitable instead of Coroutine https://stackoverflow.com/a/59177557/5805389 – Shuri2060 Jan 10 '23 at 19:16
  • 1
    In my example, `random_callable` is indeed a callable, and it returns a coroutine which is itself awaitable in the sense that "we can await it" - but the actual pythonic type of it is `Coroutine`. I would suggest reading this: https://stackoverflow.com/questions/36342899/asyncio-ensure-future-vs-baseeventloop-create-task-vs-simple-coroutine thread as it explains nicely the differences between coroutines, tasks, ensure_future, create_tasks etc... (we could change it to `random_callable: Callable[..., Awaitable]` and just await it directly: `result = await random_callable()` – CodeCop Jan 10 '23 at 19:32
  • According to docs, the typehint for `Coroutine` should have this form `Coroutine[YieldType, SendType, ReturnType]` like a Generator. Do you know why this is the case? Unless you are defining an async generator, I don't understand this. Maybe I'll open a new question if this isn't a simple one line answer – Shuri2060 Jan 10 '23 at 19:52
  • I think it should be another thread :-) – CodeCop Jan 10 '23 at 22:09
  • 1
    @Shuri2060 Here's a [GVR comment on Awaitable vs Coroutine](https://github.com/python/mypy/issues/3569). `async def`-ed functions have `send`, `throw` and `close` methods in addition to `__await__`, and they are required by `asyncio.create_task`. Check [good explanation here](https://stackoverflow.com/questions/34469060/python-native-coroutines-and-send), read the [PEP](https://peps.python.org/pep-0492/). Its interaction with generators [can be even more funny](https://peps.python.org/pep-0525/). And play in REPL, of course: `async def foo(): {yield|return} None; dir(foo())` – STerliakov Jan 11 '23 at 15:18