Trio vs Asyncio: Structured Concurrency in Python

This article examines how the third-party library Trio compares to Python's built-in asyncio regarding structured concurrency. While Trio was designed from its inception around strict structured concurrency using its "nursery" paradigm, asyncio originally relied on detached, fire-and-forget tasks before later retrofitting structured concurrency through asyncio.TaskGroup in Python 3.11. The following sections break down the core principles, architectural differences, error propagation, and practical implications of both approaches.

Understanding Structured Concurrency

Structured concurrency is an asynchronous programming paradigm where concurrent tasks are bound to explicit control-flow scopes. Just as structured programming replaced arbitrary goto statements with explicit blocks like if, while, and functions, structured concurrency eliminates unmanaged, free-floating background tasks. If a code block initiates concurrent operations, that block cannot exit until all initiated operations have terminated. This guarantees that background operations do not silently outlive their parent context, leak resources, or drop unhandled exceptions.

Trio: Structured Concurrency as an Invariant

Trio was built by Nathaniel J. Smith specifically to implement structured concurrency in Python. In Trio, structured concurrency is not an optional feature; it is an enforced invariant of the runtime.

The Nursery Pattern

Trio enforces task containment through a concept called a nursery. You cannot spawn a background task globally in Trio; tasks must be spawned inside a nursery context:

import trio

async def child():
    await trio.sleep(1)

async def main():
    async with trio.open_nursery() as nursery:
        nursery.start_soon(child)
        nursery.start_soon(child)
    # The block will not exit until both child tasks finish

The async with trio.open_nursery() block serves as the task boundary. Execution cannot proceed past the block until all tasks registered to nursery complete.

Strict Cancellation and Error Handling

If any task inside a nursery raises an unhandled exception, Trio automatically:

  1. Cancels all other tasks running inside that nursery.
  2. Waits for those tasks to exit and clean up.
  3. Propagates the exception (or combines multiple exceptions using an ExceptionGroup) up the call stack to the parent.

Trio's cancellation mechanism uses unified cancellation scopes rather than ad-hoc flags or low-level exception injection, making cancellation deterministic and reliable.

Asyncio: An Evolution from Unstructured to Structured

Historically, Python's asyncio was built around an unstructured paradigm heavily influenced by older event-driven frameworks.

The Legacy Model

Prior to Python 3.11, the standard way to run concurrent code in asyncio was via functions like asyncio.create_task() or asyncio.gather():

import asyncio

async def child():
    await asyncio.sleep(1)

async def main():
    task = asyncio.create_task(child())
    # task runs independently in the background; main can exit without waiting

This model permits "fire-and-forget" execution. If an unhandled exception occurs in a task created via create_task(), it often gets logged to stderr after garbage collection, rather than being handled deterministically by the code that created it. Similarly, managing cancellation across multiple tasks traditionally required writing complex boilerplate using asyncio.gather(..., return_exceptions=True).

The Modern Model: asyncio.TaskGroup

Recognizing the limitations of unstructured tasks, Python 3.11 introduced asyncio.TaskGroup, an API directly inspired by Trio's nurseries:

import asyncio

async def child():
    await asyncio.sleep(1)

async def main():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(child())
        tg.create_task(child())
    # Exits only after both tasks complete

If one task within a TaskGroup raises an exception, the remaining tasks are cancelled, and all unhandled exceptions are raised together via ExceptionGroup.

Key Differences Between Trio and Asyncio

1. Enforcement vs. Convention

2. Cancellation Semantics

3. API Surface and Cognitive Load

4. Ecosystem and Compatibility

Summary

Trio offers an uncompromising, mathematically sound implementation of structured concurrency where unstructured execution is structurally forbidden. asyncio, starting with Python 3.11's TaskGroup, successfully brings the core benefits of structured concurrency to the standard library, but maintains backward compatibility with older, unstructured patterns. For strict safety and cleaner cancellation mechanics, Trio remains the benchmark, while asyncio provides a practical implementation of the same principles within the standard library.