A TaskGroup owns asynchronous child tasks and waits for them to finish or be cancelled when its scope exits.
Python asyncio TaskGroup: cancel sibling work and retain failure evidence
Operation contract
The receipt import starts one waiting worker and one worker that fails after yielding control. The failure cancels the waiting sibling, whose finally block records cleanup. The caller catches the ValueError part of the resulting exception group and prints stable evidence rather than relying on task completion order.
Failure and ownership boundary
Cancellation is cooperative: code must reach an await boundary and must not swallow cancellation merely to continue work. Cancelling a coroutine does not undo a database commit or a payment already performed. This fixture has no network I/O and uses a finally marker, not a claim about closing a production client. Python context managers: clean up on success and failure stays separate from scheduling.
Working program
import asyncio
async def import_batch():
events = []
started = asyncio.Event()
async def waiting_import():
started.set()
try:
await asyncio.Event().wait()
finally:
events.append("sibling cleaned")
async def failed_import():
await started.wait()
raise ValueError("invalid receipt")
try:
async with asyncio.TaskGroup() as tasks:
tasks.create_task(waiting_import())
tasks.create_task(failed_import())
except* ValueError:
events.append("failure observed")
print(events)
if __name__ == "__main__":
asyncio.run(import_batch())Output
['sibling cleaned', 'failure observed']Costs and limits
The group retains two tasks and their exception state. Creating one task per unbounded input still grows memory with input count; TaskGroup itself is not an admission limit.
Common Mistakes
- Do not hide cancellation to keep failed sibling work running.
- Task ownership does not create a task-count limit.
Connected lessons
Python context managers: clean up on success and failure, Python thread pools: collect results and observe worker failures, Java cancellation: timed waits and cooperative interruption.
Apply this boundary
Python asyncio.Queue: backpressure and completion accounting, Python asyncio timeout: cancellation and cleanup ownership.
Follow the related contract
Python ContextVar: task-local labels without sharing one global binding, Python concurrency interview: cancellation must still run cleanup.
Trace the related workflow
Python asyncio.gather: a sibling can continue after the first failure.
Related boundary
Python asyncio Semaphore: bound active work, not queued tasks
Continue with Python ContextVar tasks: capture request context at task creation.
Continue with Python asyncio call graphs: inspect a live task without retaining frames.
