Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

asyncio.create_task hints say you can pass any Awaitable, but you cannot pass asyncio.Tasks #6770

Closed
alkasm opened this issue Jan 1, 2022 · 6 comments · Fixed by #6779
Closed
Labels
stubs: false negative Type checkers do not report an error, but should

Comments

@alkasm
Copy link
Contributor

alkasm commented Jan 1, 2022

The following program passes mypy type checking, but fails at runtime:

import asyncio
from typing import Awaitable

async def main() -> None:
    async def coro() -> Awaitable[None]: pass
    task = asyncio.create_task(coro())
    asyncio.create_task(task)

asyncio.run(main())
(venv) ➜ python --version
Python 3.9.6
(venv) ➜ mypy --version
mypy 0.930
(venv) ➜ mypy --strict example.py 
Success: no issues found in 1 source file
(venv) ➜ python example.py
Traceback (most recent call last):
  File ".../create-task-types/example.py", line 10, in <module>
    asyncio.run(main())
  File ".../python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File ".../python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
  File ".../create-task-types/example.py", line 7, in main
    asyncio.create_task(task)
  File ".../python3.9/asyncio/tasks.py", line 361, in create_task
    task = loop.create_task(coro)
  File ".../python3.9/asyncio/base_events.py", line 433, in create_task
    task = tasks.Task(coro, loop=self, name=name)
TypeError: a coroutine was expected, got <Task pending name='Task-2' coro=<main.<locals>.coro() running at .../create-task-types/example.py:4>>
@AlexWaygood
Copy link
Member

In the absence of a Not type, a clunky workaround is to use an overload returning NoReturn, like we do in datetime.date.__sub__. If we change asyncio.create_task, though, we should also change asyncio.events.AbstractEventLoop.create_task, and any subclasses of that that appear in the stdlib.

@alkasm, I don't know much about asyncio -- is a Task object the only kind of awaitable that is not accepted by create_task, or are there others?

@AlexWaygood
Copy link
Member

AlexWaygood commented Jan 1, 2022

From my reading of the source code, it looks like asyncio.create_task passes the coroutine to the create_task method of a concrete subclass of asyncio.events.AbstractEventLoop. Judging by the source code of asyncio.base_events.BaseEventLoop, which inherits from AbstractEventLoop, it looks like the create_task method of an event loop will then pass the coroutine to the constructor of an asyncio.tasks.Task object. The Task constructor raises an error unless the coroutine passed to it passes an inspect.iscoroutine test. inspect.iscoroutine only returns True if the object passed to it is an instance of types.CoroutineType.

So, from my reading of the source code, it looks like it is probably not true that a Task object is the only Awaitable that cannot be passed to asyncio.create_task. There are probably other awaitables that would also fail if you passed them to asyncio.create_task.

@alkasm
Copy link
Contributor Author

alkasm commented Jan 2, 2022

Yeah, it seems the type is simply too permissive---I believe the input type should be a Coroutine, not any Awaitable. In addition to the Task constructor explicitly checking for a coroutine as @AlexWaygood mentions, the documentation and the parameter name for create_task explicitly request coroutines, not awaitables:

asyncio.create_task(coro, *, name=None)
Wrap the coro coroutine into a Task and schedule its execution. Return the Task object.

@srittau srittau added the stubs: false negative Type checkers do not report an error, but should label Jan 2, 2022
@AlexWaygood
Copy link
Member

AlexWaygood commented Jan 2, 2022

The Task constructor raises an error unless the coroutine passed to it passes an inspect.iscoroutine test. 

As @Akuli points out here, I misread the source code — Task.__init__ passes the coroutine to asyncio.coroutines.iscoroutine, a completely different function to inspect.iscoroutine!!

@alkasm
Copy link
Contributor Author

alkasm commented Feb 4, 2022

Thanks so much for digging into and solving this rather nontrivial bug!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stubs: false negative Type checkers do not report an error, but should
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants