mrkeyoor.com_
Sun 20 Sept 14:46 UTC
PyPIWeb Backendupdated 20 Sept 2026

trio review

Trio 0.34.0 is an async I/O runtime whose nurseries bind every child task to an explicit parent scope. Leaving a nursery waits for its children, and one child's failure cancels siblings before the errors return as an exception group. Cancel scopes carry deadlines through nested calls. The package also supplies bounded memory channels, capacity limiters, TCP and TLS streams, subprocess support, thread bridges, and a test clock. It runs its own event loop, so asyncio drivers do not become compatible merely because both APIs use `await`. Version 0.34.0 adds peak channel-buffer statistics, Python 3.15 beta support, better nursery-start error context, and a free-threaded worker fix.

Verdict

Trio 0.34.0 installed 6 packages using 4 MB in 0.5 seconds and imported in 0.63 seconds in our sandbox, but choosing it also chooses a non-asyncio event loop for the whole call path. Use it when structured task lifetime is worth auditing every driver; reusable libraries should usually expose AnyIO instead.

We installed it

Lab card: what happened when we installed trioScreenshot of trio documentation
Install✓ · 0.5s6 packages on disk · 4 MB
Importimport trio in 0.63s · pure Python · py.typed · requires Python >=3.10
Known vulns0(pip-audit)

Answers from our run

Does trio install cleanly?

Yes. In a fresh container with an empty cache, pip install trio finished in 0.5s, leaving 6 packages and 4 MB on disk. pip-audit reported no known vulnerabilities.

What does trio need to run?

Python >=3.10, and nothing compiled: it is pure Python. In our run import trio succeeded in 0.63s, and the package ships py.typed for type checkers.

trio or anyio: which should you use?

anyio: Choose it when library code should keep structured scopes while supporting both asyncio and Trio. Trio 0.34.0 installed 6 packages using 4 MB in 0.5 seconds and imported in 0.63 seconds in our sandbox, but choosing it also chooses a non-asyncio event loop for the whole call path.

When should you not use trio?

A required PostgreSQL, Redis, cloud, or HTTP client only runs on asyncio. A bridge or replacement would sit on every important path.

API stability4/5The contracts around `trio.run`, nurseries, cancellation scopes, channels, capacity limits, and thread bridges have remained recognizable with published deprecation windows. Pre-1.0 migrations are still material: standard exception groups replaced MultiError, strict grouping became normal, and 0.34.0 finally errors when code assigns an absolute deadline to a scope constructed with relative time.
Docs5/5The official site starts with a full tutorial, then documents nurseries, cancellation and checkpoints, streams, memory channels, threads, subprocesses, low-level primitives, testing, design choices, and release history. Explanations show why behavior exists rather than listing signatures alone. Integration lookup is still scattered because asyncio-only dependencies rarely document whether or how Trio bridging is safe.
Maintenance4/5Version 0.34.0 shipped on August 11, 2026, and GitHub records a push on August 24, 7,306 stars, an unarchived repository, and 324 open issues and pull requests. The release adds Python 3.15 beta support plus channel telemetry while fixing nursery exception context, subprocess wording, pathlib compatibility, and context leakage in free-threaded worker threads.
Ecosystem3/5The stored current snapshot reports 14,632,590 weekly downloads. AnyIO, HTTPX, Hypercorn, pytest-trio, and trio-websocket cover important HTTP, ASGI, test, and protocol paths. Many database clients, cloud SDKs, observability integrations, and deployment guides remain asyncio-first, so a high indirect install count cannot establish compatibility with a real backend dependency graph.

Use it if

  • A network service controls its runtime and wants task lifetime plus cancellation visible in nested scopes.
  • Tests contain long deadlines that a fake clock can skip once all tasks block.
  • Workers need bounded channels, explicit readiness, backpressure, and sibling cancellation on failure.
  • The dependency set already supports AnyIO or Trio and has no required asyncio-only driver.
Skip it if

Setup reality

We installed trio 0.34.0 without a cache in a clean Python 3.12 Bookworm sandbox. pip finished in 0.5 seconds and left 6 packages occupying 4 MB. The pure-Python distribution declares 7 direct dependencies, requires Python >=3.10, and includes py.typed. import trio worked in 0.63 seconds, and pip-audit returned 0 known vulnerabilities. PyPI did not expose a recognized license value, although the repository offers MIT or Apache 2.0 terms.

Trio owns the running event loop. An asyncio-only database or cloud client cannot run directly under it; select a Trio-native implementation, an AnyIO interface, or a deliberately narrow bridge. An ASGI deployment also needs a server backend that supports Trio, followed by middleware tests on that exact backend. Shared async def syntax does not imply runtime compatibility.

Cancellation is delivered at checkpoints. CPU work that never awaits can monopolize the loop and pass its deadline without noticing. Send blocking functions through trio.to_thread.run_sync or restructure them to yield. If cancellation abandons a worker thread, the Python function underneath may continue running, so do not treat cancellation as proof that an external side effect stopped.

Start children inside nurseries and use nursery.start() when a parent needs a readiness handshake. Current Python reports multiple child failures with exception groups handled through except*. Tests can use pytest-trio or call trio.run() directly. Trio's fake clock advances only when every task is blocked; a spinning task still hangs. Version 0.34.0's peak channel statistic gives tests and production telemetry a concrete buffer high-water mark.

Patterns

Tie workers to one nursery run-tasks-in-a-nursery

import trio

async def worker(name: str) -> None:
    await trio.sleep(1)
    print("done", name)

async def main() -> None:
    async with trio.open_nursery() as nursery:
        nursery.start_soon(worker, "a")
        nursery.start_soon(worker, "b")
    print("both finished")

trio.run(main)

The context waits for every child and cancels siblings on an uncaught error. `start_soon` takes positional arguments, so bind keywords first.

Apply an outer and inner deadline timeouts-with-cancel-scopes

import trio

async def main() -> None:
    with trio.move_on_after(5) as scope:
        await slow_call()
    if scope.cancelled_caught:
        print("gave up after 5s")

    with trio.fail_after(5):        # raises trio.TooSlowError
        await slow_call()

    # extend a deadline from inside
    with trio.move_on_after(1) as scope:
        scope.deadline = trio.current_time() + 30
        await slow_call()

These are synchronous context managers around async work. Expiry is observed at a checkpoint, not during uninterrupted CPU execution.

Catch selected nursery failures handle-exception-groups

import trio

async def main() -> None:
    try:
        async with trio.open_nursery() as nursery:
            nursery.start_soon(might_fail)
            nursery.start_soon(might_also_fail)
    except* ValueError as group:      # Python 3.11+
        for exc in group.exceptions:
            print("task failed:", exc)
    except* OSError:
        print("network problem")

Strict grouping can wrap even one child error. Python 3.10 cannot parse `except*` and needs compatibility helpers.

Backpressure producers with a channel memory-channels

import trio

async def producer(send_channel) -> None:
    async with send_channel:
        for i in range(100):
            await send_channel.send(i)

async def consumer(receive_channel) -> None:
    async with receive_channel:
        async for item in receive_channel:
            await handle(item)

async def main() -> None:
    send, receive = trio.open_memory_channel(max_buffer_size=10)
    async with trio.open_nursery() as nursery:
        nursery.start_soon(producer, send)
        nursery.start_soon(consumer, receive)

trio.run(main)

Receiver iteration finishes after all send clones close. Close unused original endpoints or they keep the channel logically open.

Offload a blocking function run-blocking-code-in-a-thread

import functools
import trio

async def main() -> None:
    rows = await trio.to_thread.run_sync(query_database, "select 1")

    # keyword arguments need partial
    body = await trio.to_thread.run_sync(
        functools.partial(requests.get, url, timeout=10)
    )

    # let the thread keep running if we are cancelled
    await trio.to_thread.run_sync(flush_to_disk, abandon_on_cancel=True)

Thread work cannot be forcibly stopped. Normal cancellation waits; abandon mode frees the Trio task while the function continues alone.

Call async code from a worker thread call-trio-from-a-thread

import trio

def blocking_worker(paths):
    for path in paths:
        data = process(path)
        trio.from_thread.run(report_progress, path)   # await an async fn
        trio.from_thread.run_sync(counter.increment)  # sync fn on the loop

async def main() -> None:
    await trio.to_thread.run_sync(blocking_worker, paths)

A Trio-created worker inherits the required token. Arbitrary external threads need an explicit token or supported portal.

Serve TCP connections concurrently tcp-server-and-client

import trio

async def echo(stream: trio.SocketStream) -> None:
    async with stream:
        async for chunk in stream:
            await stream.send_all(chunk)

async def server() -> None:
    await trio.serve_tcp(echo, 9000)

async def client() -> None:
    stream = await trio.open_tcp_stream("127.0.0.1", 9000)
    async with stream:
        await stream.send_all(b"hello\n")
        print(await stream.receive_some(1024))

Hostname dialing races candidate addresses. Keep the long-lived server in the application nursery so shutdown cancels it predictably.

Wait until a service is ready wait-for-task-startup

import trio

async def serve(port: int, *, task_status=trio.TASK_STATUS_IGNORED) -> None:
    listeners = await trio.open_tcp_listeners(port)
    task_status.started(listeners[0].socket.getsockname())
    await trio.serve_listeners(handler, listeners)

async def main() -> None:
    async with trio.open_nursery() as nursery:
        address = await nursery.start(serve, 0)
        print("bound to", address)  # guaranteed listening now

The parent resumes after `started()` supplies readiness. Version 0.34.0 keeps cause and context if startup fails earlier.

Give cleanup a protected timeout shield-cleanup-from-cancellation

import trio

async def handle(conn) -> None:
    try:
        await serve_requests(conn)
    finally:
        with trio.CancelScope(shield=True) as scope:
            scope.deadline = trio.current_time() + 2
            await conn.send_goodbye()
            await conn.aclose()

Shielding blocks the outer cancellation while cleanup awaits. The nested 5-second scope prevents cleanup itself from hanging shutdown.

Time-limit a subprocess run-a-subprocess

import trio

async def main() -> None:
    with trio.move_on_after(30):
        result = await trio.run_process(
            ["ffmpeg", "-i", "in.mov", "out.mp4"],
            capture_stdout=True,
            capture_stderr=True,
        )
        print(result.returncode, result.stdout[:200])

`check=True` raises on a nonzero status, and cancellation terminates the process. Streaming pipes need the lower-level process API.

Cap concurrent resource users limit-concurrency

import trio

async def main(urls) -> None:
    limiter = trio.CapacityLimiter(8)

    async def fetch(url: str) -> None:
        async with limiter:
            await download(url)

    async with trio.open_nursery() as nursery:
        for url in urls:
            nursery.start_soon(fetch, url)

The limiter constrains borrowers while any number of tasks may exist. Its statistics expose both capacity and the waiting queue.

Skip idle time in a test test-with-a-fake-clock

import pytest
import trio
from trio.testing import MockClock, wait_all_tasks_blocked

@pytest.mark.trio
async def test_retry_backoff(autojump_clock):
    start = trio.current_time()
    await retry_with_backoff(always_fails, attempts=5)
    assert trio.current_time() - start == pytest.approx(31.0)

def test_standalone():
    trio.run(main, clock=MockClock(autojump_threshold=0))

pytest-trio injects this clock. It jumps only after every runnable task blocks, so a checkpoint-free loop still exposes starvation by hanging.

Alternatives

PackageRegistryPick it when
anyioPyPIChoose it when library code should keep structured scopes while supporting both asyncio and Trio.
trio-asyncioPyPIChoose it to isolate a small, unavoidable asyncio dependency inside an otherwise Trio application.
curioPyPIChoose it for a controlled project that accepts Curio's smaller runtime and ecosystem.
uvloopPyPIChoose it when compatibility keeps the app on asyncio and only the event-loop implementation should change.

More web backend guides

urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.