Modern Async Practices¶
Use async code for concurrent I/O under an async call path. Keep task ownership, cancellation, and resource limits explicit.
Structured Concurrency¶
Prefer asyncio.TaskGroup on Python 3.11+ when sibling tasks form one operation:
async with asyncio.TaskGroup() as task_group:
user_task = task_group.create_task(fetch_user(user_id))
orders_task = task_group.create_task(fetch_orders(user_id))
user = user_task.result()
orders = orders_task.result()
The context owns the tasks, waits for completion, and cancels siblings when one fails. Use asyncio.gather when its result ordering or independent-failure semantics are specifically required, not as the automatic fan-out primitive.
Cancellation and Timeouts¶
- Treat cancellation as control flow. Cleanup may run, but normally re-raise
asyncio.CancelledErrorafter cleanup. - Prefer
asyncio.timeouton Python 3.11+ for a bounded block containing one or more awaits. - Place timeouts at the layer that knows the operation's latency budget. Nested library timeouts should remain narrower implementation safeguards.
- Use
asyncio.shieldsparingly for work that must outlive caller cancellation, and retain a strong reference to the task.
Blocking Work and Backpressure¶
- Never call blocking network, file, subprocess, or lock APIs on the event-loop thread.
- Use native async libraries for sustained I/O. Use
asyncio.to_threadfor bounded blocking I/O that has no async API; it does not make CPU-bound Python work parallel. - Bound task creation with worker pools, semaphores, or bounded queues. Unlimited fan-out turns downstream latency into memory and connection exhaustion.
- Do not hold a
threading.Lockacrossawait. Useasyncio.Lockfor event-loop coordination and minimize the protected region.
Task and Resource Ownership¶
- Keep a strong reference to background tasks and define how their exceptions are observed.
- Do not create fire-and-forget tasks for work whose completion affects correctness. Put durable work behind a supervised worker or queue.
- Use
async withfor clients, streams, sessions, locks, and other asynchronous resources. - At synchronous entry points, prefer
asyncio.run; useasyncio.Runnerwhen several top-level async calls must share one loop and context.
Reference: asyncio documentation.