Async Patterns with anyio: Backend-Agnostic Async in Python
Master anyio for backend-agnostic async I/O in Python.
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
- ✓Basic understanding of async/await in Python
- ✓Familiarity with asyncio or trio is helpful but not required
- ✓Python 3.7+ installed
- anyio provides a unified async API that works across asyncio, trio, and curio backends.
- Use
anyio.run()instead ofasyncio.run()to start your async program. - Replace
asyncio.sleep()withanyio.sleep()for backend-agnostic delays. - Use
anyio.create_task_group()for structured concurrency. - Use
anyio.Queuefor async producer-consumer patterns.
Think of anyio as a universal remote that works with any TV brand. Instead of learning separate remotes for asyncio, trio, and curio, you learn one set of buttons that works everywhere. This means you can write async code once and run it on any backend.
Asynchronous programming in Python has evolved rapidly, but the ecosystem remains fragmented. The standard library's asyncio is the most popular, but alternatives like trio and curio offer different philosophies—structured concurrency, better cancellation, and simpler error handling. However, choosing one backend locks you into its ecosystem. Enter anyio: a backend-agnostic async library that provides a unified API across all major async backends. With anyio, you can write portable async code that runs on asyncio, trio, or curio without modification. This is a game-changer for library authors who want to support multiple backends and for developers who want to future-proof their code. In this tutorial, you'll learn the core patterns of anyio: starting an async program, structured concurrency with task groups, cancellation and timeouts, synchronization primitives, and networking. You'll also see a real-world production incident where anyio's portability saved the day. By the end, you'll be able to write robust, backend-agnostic async code that works everywhere.
Getting Started with anyio
To begin, install anyio with pip: pip install anyio. The simplest anyio program uses to execute an async function. This replaces anyio.run() and works with any backend. By default, anyio uses asyncio, but you can switch to trio by passing asyncio.run()backend='trio'. Here's a basic example:
backend = os.getenv('ANYIO_BACKEND', 'asyncio').anyio.run() as the entry point for your async program. It abstracts away the backend selection.Structured Concurrency with Task Groups
One of anyio's most powerful features is structured concurrency via TaskGroup. Unlike raw , a task group ensures that all child tasks are completed or cancelled before the group exits. This prevents orphaned tasks and makes error handling predictable. If any task raises an exception, all sibling tasks are cancelled, and the exception is propagated. Here's an example:asyncio.create_task()
anyio.create_task_group() for structured concurrency. It ensures all tasks complete or are cancelled before moving on.Cancellation and Timeouts
anyio provides a unified cancellation model through CancelScope. Unlike asyncio's CancelledError, anyio's cancellation is more predictable. You can create a cancel scope and cancel it manually, or use for timeouts. Here's an example:fail_after()
fail_after() as a context manager for fine-grained control.CancelScope for manual cancellation and fail_after() for timeouts. Always catch get_cancelled_exc_class() if you need cleanup.Async Synchronization Primitives
anyio provides backend-agnostic synchronization primitives like Lock, Event, Semaphore, and Queue. These work identically across backends. Here's an example using anyio.Queue for a producer-consumer pattern:
anyio.create_memory_object_stream() which provides a more efficient channel-based communication.Async Networking with anyio
anyio provides high-level networking APIs for TCP and UDP. The anyio.connect_tcp() and anyio.create_tcp_listener() functions are backend-agnostic. Here's a simple echo server and client:
anyio.connect_tcp() and anyio.create_tcp_listener() for backend-agnostic networking.Running Blocking Code with anyio
anyio provides to_thread.run_sync() to run blocking code in a separate thread without blocking the event loop. This is essential for integrating with synchronous libraries. Here's an example:
anyio.to_process.run_sync() to run in a separate process. This avoids the GIL and leverages multiple cores.anyio.to_thread.run_sync() to offload blocking operations to a thread pool.The Great Backend Migration: How anyio Prevented a Rewrite
- Use anyio for backend-agnostic async code to avoid vendor lock-in.
- anyio's structured concurrency simplifies cancellation and error handling.
- Test your async code with multiple backends to catch backend-specific bugs.
- anyio's
TaskGroupensures all tasks are cleaned up on cancellation. - Adopt anyio early to future-proof your async code.
anyio.fail_after() to set timeouts.anyio.create_queue(10) to limit capacity.anyio.run(backend='trio') and compare.async with anyio.CancelScope() as scope:scope.cancel()| File | Command / Code | Purpose |
|---|---|---|
| hello_anyio.py | async def main(): | Getting Started with anyio |
| task_group.py | async def worker(name, delay): | Structured Concurrency with Task Groups |
| cancellation.py | async def long_running(): | Cancellation and Timeouts |
| queue.py | async def producer(queue, n): | Async Synchronization Primitives |
| echo_client.py | async def main(): | Async Networking with anyio |
| blocking.py | def blocking_io(): | Running Blocking Code with anyio |
Key takeaways
anyio.run() as the entry point and anyio.create_task_group() for structured concurrency.CancelScope and fail_after() for cancellation and timeouts.anyio.to_thread.run_sync().Interview Questions on This Topic
What is structured concurrency and how does anyio implement it?
TaskGroup, which cancels all tasks if one fails and propagates exceptions.Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
That's Advanced Python. Mark it forged?
3 min read · try the examples if you haven't