python/cpython · error · RuntimeError

TaskGroup is finished

Error message

TaskGroup {self!r} is finished

What it means

RuntimeError raised by TaskGroup.create_task() when the group is exiting with no remaining tasks (_exiting and not self._tasks): the body has finished and __aexit__ is winding down, so new work cannot join. The passed coroutine is closed before raising.

Solutions

  1. Keep spawn logic inside the live body of the 'async with' block and await completion there
  2. For fire-and-forget work that must outlive the group, use asyncio.create_task on the loop (and track it separately) rather than the dying group
  3. Restructure continuation patterns: the task that finishes should itself schedule its successor before returning, while the group is still active

Example fix

// before
async with asyncio.TaskGroup() as tg:
    t = tg.create_task(job())
    t.add_done_callback(lambda _: tg.create_task(cleanup()))  # may fire during exit -> RuntimeError

// after
async def job_then_cleanup():
    await job()
    await cleanup()
async with asyncio.TaskGroup() as tg:
    tg.create_task(job_then_cleanup())
Defensive patterns

Strategy: validation

Validate before calling

null

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: A task-finished callback or scheduled call that invokes tg.create_task() while the 'async with' body is completing; e.g. calling create_task from on_done callbacks, from a sibling task's finally block racing the exit, or from code woken by the last task completing.

Common situations: Chained/continuation spawning inside the group ('when X finishes, spawn Y') without awaiting Y within the body; fire-and-forget helpers that lazily create tasks and get invoked at teardown; races between a worker finishing and a supervisor trying to enqueue a replacement at shutdown.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/429abe36d77e14b4. Report an issue: GitHub.

Appendix: source

Thrown at Lib/asyncio/taskgroups.py:223

                    ) from None
            finally:
                exc = None

        # Suppress any remaining exception (exceptions deserving to be raised
        # were raised above).
        return True

    def create_task(self, coro, **kwargs):
        """Create a new task in this group and return it.

        Similar to `asyncio.create_task`.
        """
        if not self._entered:
            coro.close()
            raise RuntimeError(f"TaskGroup {self!r} has not been entered")
        if self._exiting and not self._tasks:
            coro.close()
            raise RuntimeError(f"TaskGroup {self!r} is finished")
        if self._aborting:
            coro.close()
            raise RuntimeError(f"TaskGroup {self!r} is shutting down")
        task = self._loop.create_task(coro, **kwargs)

        futures.future_add_to_awaited_by(task, self._parent_task)

        # Always schedule the done callback even if the task is
        # already done (e.g. if the coro was able to complete eagerly),
        # otherwise if the task completes with an exception then it will cancel
        # the current task too early. gh-128550, gh-128588
        self._tasks.add(task)
        task.add_done_callback(self._on_task_done)
        try:
            return task
        finally:
            # gh-128552: prevent a refcycle of
            # task.exception().__traceback__->TaskGroup.create_task->task

View on GitHub (pinned to bc6749cc3b)