Skip to content

asyncio.TaskGroup does not close unawaited coroutines #115957

Closed
@arthur-tacca

Description

@arthur-tacca

Bug report

Bug description:

Consider the following code:

import asyncio

async def wait_and_raise():
    await asyncio.sleep(0.5)
    raise RuntimeError(1)

async def wait_and_start(tg):
    try:
        await asyncio.sleep(1)
    finally:
        try:
            tg.create_task(asyncio.sleep(1))
        except RuntimeError as e:
            print(f"wait_and_start() caught {e!r}")

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(wait_and_start(tg))
            tg.create_task(wait_and_raise())
    except Exception as e:
        print(f"main() caught {e!r}")

    try:
        tg.create_task(asyncio.sleep(1))
    except RuntimeError as e:
        print(f"main() caught {e!r}")

asyncio.run(main())

This gives the following output

wait_and_start() caught RuntimeError('TaskGroup <TaskGroup tasks=1 errors=1 cancelling> is shutting down')
C:\code\taskgrouptest.py:16: RuntimeWarning: coroutine 'sleep' was never awaited
  print(f"wait_and_start() caught {e!r}")
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
main() caught ExceptionGroup('unhandled errors in a TaskGroup', [RuntimeError(1)])
main() caught RuntimeError('TaskGroup <TaskGroup cancelling> is finished')
C:\code\taskgrouptest.py:29: RuntimeWarning: coroutine 'sleep' was never awaited
  print(f"main() caught {e!r}")
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

Arguably, when you call tg.create_task() on a task group that is shutting down or has finished, the calling code "knows" about the error because it gets a RuntimeError exception (as you can see above), so there is no need to get a warning about a coroutine that was not awaited. So, when a TaskGroup encounters this situation, it should close the coroutine before raising the error.

The other argument would be that this still represents a design mistake so should still get the warning. I can see both points of view but I'm raising this issue so a conscious decision can be made.

For comparison, when you do this on a Trio Nursery or AnyIO TaskGroup that has already closed, a coroutine never even gets created in the first place, because you use a different syntax (nursery.start_soon(foo, 1, 2) rather than tg.create_task(foo(1, 2))), so it's a lot like if asyncio were to close the coroutine. The situation is a bit different for a nursery that is shutting down: then it runs till the first (unshielded) await and is cancelled at that point, which is possible because they use level-based cancellation rather than edge-based cancellation.

CPython versions tested on:

3.12

Operating systems tested on:

Windows

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions