PythiloLesson 8 of 12 · progress kept in this browser

TaskGroup and structured concurrency

A task started with create_task can outlive the code that started it, and if nobody awaits it, its exception goes unnoticed. asyncio.TaskGroup ties tasks to a block: the block does not end until every task in it has finished.

import asyncio

async def double(n):
    await asyncio.sleep(0.01)
    return n * 2

async def main():
    async with asyncio.TaskGroup() as tg:
        a = tg.create_task(double(1))
        b = tg.create_task(double(2))
    print(a.result(), b.result())  # 2 4

asyncio.run(main())

If any task fails, the group cancels the others, waits for them, and raises an ExceptionGroup that holds every error. Handle it with except*, which matches error types inside the group:

import asyncio

async def fail():
    raise ValueError("bad row")

async def run():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(fail())

try:
    asyncio.run(run())
except* ValueError as group:
    print(group.exceptions)
Your task

Write async def run_all(jobs) that runs every job in jobs, a dict from names to async functions with no arguments, at once in an asyncio.TaskGroup, and returns a dict from each name to its result. If a job fails, the others must be cancelled and the group's ExceptionGroup must reach the caller.