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 finishThe 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:
- Cancels all other tasks running inside that nursery.
- Waits for those tasks to exit and clean up.
- 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 waitingThis 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 completeIf 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
- Trio: Mandates structured concurrency. There is no
public API equivalent to
create_task()that allows spawning tasks outside an explicit nursery. - Asyncio: Treats structured concurrency as an opt-in
pattern. Legacy APIs (
asyncio.create_task(),asyncio.gather(),asyncio.shield()) remain fully accessible and widely used, meaning codebases often mix structured and unstructured patterns.
2. Cancellation Semantics
- Trio: Features clear cancellation semantics centered around checkpoints. Every blocking call is a checkpoint where cancellation can occur. If a task is cancelled, cancellation stays active throughout the scope unless explicitly shielded using a dedicated cancellation scope.
- Asyncio: Implements cancellation by injecting a
CancelledErrorinto the coroutine. Becauseasyncio.CancelledErrorinherits fromBaseException(as of Python 3.8), improper exception handling or third-party libraries catchingBaseExceptioncan inadvertently swallow or delay cancellation signals.
3. API Surface and Cognitive Load
- Trio: Has a minimal, consistent API designed
specifically around structured concurrency. Features like timeouts,
deadlines, and shields all use the same unified
CancelScopeabstraction. - Asyncio: Has a larger, dual API surface resulting
from a decade of backward compatibility. Developers must choose between
older functions like
asyncio.wait(),asyncio.gather(), and modernTaskGroupconstructs, each with differing cancellation and error propagation behaviors.
4. Ecosystem and Compatibility
- Trio: Requires a Trio-native ecosystem (e.g.,
asksorhttpxfor HTTP,trio-websocket). While libraries likeanyiobridge compatibility, Trio's ecosystem is smaller thanasyncio's. - Asyncio: Standard library inclusion guarantees universal availability and extensive support from modern asynchronous frameworks (such as FastAPI, aiohttp, and Tortoise ORM).
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.