python/cpython · error · RuntimeError

TaskGroup {self!r} 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.

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)

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.