Skip to content
v0.1.1latest release34 skills2 categories4 sub-categories79 tested scripts157 reference docsContact us

API Mapping: asyncio -> anyio -> trio

Reference for python-trio-anyio-migrator · 237 lines

Programming / Pythonv1.0.0Updated OCT 21 20267 references1 script

The complete translation table. "R" marks rows that scripts/migrate_async.py rewrites automatically (when the call site is safe); every other row is reported as a manual item with the rule id shown. anyio entries are for anyio 4.x (verified against 4.14.2 and 4.15.1); trio entries are for current trio (0.25+). Rows marked 4.14+ / 4.15+ exist only in recent anyio releases.

Contents

Running and time

asyncioanyiotrioRule
asyncio.run(main())anyio.run(main)trio.run(main)R MIG002
asyncio.run(main(a, b))anyio.run(main, a, b)trio.run(main, a, b)R MIG002
asyncio.run(main(a, k=1))anyio.run(functools.partial(main, a, k=1))trio.run(functools.partial(main, a, k=1))R MIG002
asyncio.run(main(), debug=True)anyio.run(main, backend_options={"debug": True})(no debug mode; use instruments)MIG002 manual
asyncio.run(main(), loop_factory=uvloop.new_event_loop) (3.12+)anyio.run(main, backend_options={"use_uvloop": True})n/aMIG002 manual
asyncio.Runner() reused for several callswith anyio.from_thread.start_blocking_portal() as portal: portal.call(f)trio.run per callMIG018
choose loop at run timeanyio.run(main, backend="trio")n/a-
await asyncio.sleep(s)await anyio.sleep(s)await trio.sleep(s) (s >= 0)R MIG003
await asyncio.sleep(0)await anyio.sleep(0) or await anyio.lowlevel.checkpoint()await trio.lowlevel.checkpoint()R MIG003
await asyncio.sleep(s, result)await anyio.sleep(s); x = resultsameMIG003 manual
await asyncio.Event().wait() (sleep forever)await anyio.sleep_forever()await trio.sleep_forever()-
loop.time()anyio.current_time()trio.current_time()MIG018
sleep until a loop-time deadlineawait anyio.sleep_until(deadline)await trio.sleep_until(deadline)-

anyio's backend_options for asyncio are debug, use_uvloop and loop_factory; on trio they are passed to trio.run (for example clock=).

Tasks and task groups

asyncioanyiotrioRule
async with asyncio.TaskGroup() as tg: (3.11+)async with anyio.create_task_group() as tg:async with trio.open_nursery() as nursery:R MIG007
tg.create_task(f(a, b)) (result unused)tg.start_soon(f, a, b)nursery.start_soon(f, a, b)R MIG008
tg.create_task(f(a, k=1), name="x")tg.start_soon(functools.partial(f, a, k=1), name="x")same with nurseryR MIG008
t = tg.create_task(f()) then t.result()write into a container in the child; 4.14+: h = tg.create_task(f()), await h / h.return_valuecontainerMIG007 manual
asyncio.create_task(f()) fire-and-forgettg.start_soon(f) in a group that outlives itnursery.start_soon(f)MIG014
asyncio.ensure_future(coro)same as create_tasksameMIG014
loop.create_task(coro)samesameMIG014
await asyncio.gather(a(), b())task group + results list; 4.15+: await anyio.gather(a(), b())nursery + results listMIG011
gather(..., return_exceptions=True)each child catches Exception and stores itsameMIG012
asyncio.wait(ts, return_when=FIRST_COMPLETED)children store result and cancel tg.cancel_scopesame with nursery.cancel_scopeMIG022
asyncio.wait(ts, return_when=FIRST_EXCEPTION)default task-group behaviourdefault nursery behaviourMIG022
asyncio.as_completed(aws)children send results into a memory stream; 4.15+: async with anyio.as_completed(*aws) as handles:memory channelMIG023
Event-based readiness ("server started")value = await tg.start(f); child calls task_status.started(value)await nursery.start(f)-
tg type annotation asyncio.TaskGroupanyio.abc.TaskGrouptrio.NurseryMIG007 manual
cancel all childrentg.cancel_scope.cancel() (4.14+: tg.cancel())nursery.cancel_scope.cancel()-
Task.cancel() on one taskgive that child its own CancelScope, return it via tg.start, call scope.cancel()sameMIG030

Timeouts, cancellation and shielding

asyncioanyiotrioRule
async with asyncio.timeout(s): (3.11+)with anyio.fail_after(s):with trio.fail_after(s):R MIG006
async with asyncio.timeout(None):with anyio.fail_after(None):with trio.fail_after(math.inf):R MIG006 (anyio) / manual (trio)
async with asyncio.timeout(s) as cm: cm.reschedule(t)with anyio.fail_after(s) as scope: scope.deadline = twith trio.fail_after(s) as scope: scope.deadline = tMIG032
asyncio.timeout_at(when)with anyio.CancelScope(deadline=when): / 4.15+ anyio.fail_at(when)with trio.fail_at(when):MIG032
await asyncio.wait_for(aw, s)with anyio.fail_after(s): r = await awwith trio.fail_after(s): r = await awMIG013
give up silently after swith anyio.move_on_after(s) as scope: then scope.cancelled_caughtwith trio.move_on_after(s) as scope:-
await asyncio.shield(aw)with anyio.CancelScope(shield=True): await awwith trio.CancelScope(shield=True): await awMIG024
except asyncio.CancelledError:except anyio.get_cancelled_exc_class():except trio.Cancelled:R MIG009
raise asyncio.CancelledErrorcancel a scope: scope.cancel()sameMIG009 manual
task.cancelling() / uncancel() (3.11+)not needed: scopes know their own cancellation (scope.cancel_called, cancelled_caught)sameMIG099
asyncio.TimeoutErrorbuiltin TimeoutErrortrio.TooSlowErrorR MIG010
nearest deadlineanyio.current_effective_deadline()trio.current_effective_deadline()-

fail_after, move_on_after and CancelScope are synchronous context managers (with, not async with); the rewriter turns async with asyncio.timeout(x): into with anyio.fail_after(x):.

Synchronisation primitives

asyncioanyiotrioRule / caveat
asyncio.Lock()anyio.Lock()trio.Lock()R MIG004. Must be released by the task that acquired it (RuntimeError otherwise); explicit .release() makes the item manual
lock.locked()lock.locked()lock.locked()-
asyncio.Event()anyio.Event()trio.Event()R MIG004. No clear(): replace the object; wait() returns None (asyncio returns True)
asyncio.Semaphore(n) / Semaphore()anyio.Semaphore(n) / anyio.Semaphore(1)trio.Semaphore(n)R MIG004. initial value is required; no locked(), use sem.value == 0
asyncio.BoundedSemaphore(n)anyio.Semaphore(n, max_value=n)trio.Semaphore(n, max_value=n)R MIG004
"at most N concurrent" (Semaphore as limiter)anyio.CapacityLimiter(n) (borrower-aware, total_tokens adjustable at run time)trio.CapacityLimiter(n)-
asyncio.Condition(lock)anyio.Condition(lock)trio.Condition(lock)R MIG004
await cond.wait_for(pred)await cond.wait_for(pred) (anyio 4.11+)loop: while not pred(): await cond.wait()manual on trio
asyncio.Barrier(n) (3.11+)no primitive: counter + anyio.Eventsame with trio.EventMIG099
defaultdict(asyncio.Lock)defaultdict(anyio.Lock)defaultdict(trio.Lock)R MIG004 (Event/Lock/Condition as factories)
exclusive use of a resourceanyio.ResourceGuard("reading")trio's ConflictDetector is internal; use a Lock-

Queues and memory object streams

asyncioanyiotrioRule
q = asyncio.Queue(maxsize=n)send, recv = anyio.create_memory_object_stream[T](max_buffer_size=n)send, recv = trio.open_memory_channel(n)MIG015
asyncio.Queue() (unbounded)create_memory_object_stream[T](math.inf) (think twice: no backpressure)trio.open_memory_channel(math.inf)MIG015
maxsize=0 means unbounded in asynciomax_buffer_size=0 means rendezvous (send waits for a receiver)same as anyioMIG015
await q.put(x)await send.send(x)await send.send(x)MIG015
q.put_nowait(x) / QueueFullsend.send_nowait(x) / anyio.WouldBlocksend.send_nowait(x) / trio.WouldBlockMIG015
await q.get()await recv.receive()await recv.receive()MIG015
q.get_nowait() / QueueEmptyrecv.receive_nowait() / anyio.WouldBlockrecv.receive_nowait() / trio.WouldBlockMIG015
loop with sentinel Noneasync for item in recv: ends when every send clone is closedsameMIG015
q.task_done() / await q.join()close send clones; leaving the task group means all consumers finishedsameMIG016
q.shutdown() (3.13+)await send.aclose() (consumers drain, then stop); recv.close() makes senders failsameMIG015
several producerssend.clone() per producer; stream ends when the last clone closessame-
several consumersrecv.clone() per consumer (each item goes to one consumer)same-
q.qsize()send.statistics().current_buffer_usedsend.statistics().current_buffer_used-
asyncio.PriorityQueue / LifoQueueno stream equivalent: heapq + anyio.Condition, or a Lock-guarded list + EventsameMIG015

Errors on streams: receive() after all senders closed -> anyio.EndOfStream (trio: trio.EndOfChannel); send() after all receivers closed -> anyio.BrokenResourceError (trio: trio.BrokenResourceError); using a handle you closed yourself -> anyio.ClosedResourceError (trio: trio.ClosedResourceError). anyio warns with a ResourceWarning when a stream object is garbage-collected without being closed.

Threads, processes and the loop from outside

asyncioanyiotrioRule
await asyncio.to_thread(f, *a)await anyio.to_thread.run_sync(f, *a)await trio.to_thread.run_sync(f, *a)R MIG005
await asyncio.to_thread(f, *a, k=v)await anyio.to_thread.run_sync(functools.partial(f, *a, k=v))same with trioR MIG005
loop.run_in_executor(None, f, *a)await anyio.to_thread.run_sync(f, *a)await trio.to_thread.run_sync(f, *a)MIG017
loop.run_in_executor(pool_of_8, f)limiter = anyio.CapacityLimiter(8); run_sync(f, limiter=limiter)trio.CapacityLimiter(8); run_sync(f, limiter=...)MIG017
loop.run_in_executor(ProcessPoolExecutor(), f)await anyio.to_process.run_sync(f, *a) (or keep a tuned pool and call it via to_thread)trio-parallel (third-party) or a pool via to_threadMIG017
default executor sizeanyio.to_thread.current_default_thread_limiter().total_tokens (40)trio.to_thread.current_default_thread_limiter() (40)-
cancellation while the thread runswaits for the thread; abandon_on_cancel=True returns immediately (anyio 4.1+)abandon_on_cancel=True (trio 0.23+)note
asyncio.run_coroutine_threadsafe(coro, loop).result() from a worker threadanyio.from_thread.run(async_fn, *a)trio.from_thread.run(async_fn, *a)MIG028
loop.call_soon_threadsafe(f, *a) from a worker threadanyio.from_thread.run_sync(f, *a)trio.from_thread.run_sync(f, *a)MIG019
call async code from a foreign thread (Celery, Streamlit, CLI)with anyio.from_thread.start_blocking_portal() as portal: portal.call(f, *a)trio.from_thread.run(f, *a, trio_token=token) with token = trio.lowlevel.current_trio_token()MIG028
schedule without waiting from a foreign threadportal.start_task_soon(f, *a) (returns concurrent.futures.Future)trio.from_thread.run_sync(nursery.start_soon, f)MIG028
worker thread checks for cancellationanyio.from_thread.check_cancelled()trio.from_thread.check_cancelled()-
subinterpreters (3.14 InterpreterPoolExecutor)anyio.to_interpreter.run_sync(f, *a) (recent anyio; see its docs for Python-version caveats)n/a-

Subprocesses

asyncioanyiotrioRule
p = await asyncio.create_subprocess_exec(*cmd, stdout=PIPE); out, _ = await p.communicate()r = await anyio.run_process(cmd); r.stdout (bytes), check=True by defaultr = await trio.run_process(cmd, capture_stdout=True)MIG025
`create_subprocess_shell("ab")``await anyio.run_process("ab")(astr` command runs through the shell)
streaming outputasync with await anyio.open_process(cmd) as p: async for chunk in p.stdout:p = await nursery.start(trio.run_process, cmd, ...)MIG025
p.kill() / p.terminate() / await p.wait()same names on anyio.abc.Processsame names on trio.Process-
asyncio.subprocess.PIPEsubprocess.PIPE (stdlib)subprocess.PIPEMIG025
cancellationanyio kills the child when the awaiting scope is cancelledtrio terminates then kills (deliver_cancel)-

Sockets, streams and TLS

asyncioanyiotrioRule
reader, writer = await asyncio.open_connection(h, p)stream = await anyio.connect_tcp(h, p)stream = await trio.open_tcp_stream(h, p)MIG026
open_connection(h, p, ssl=ctx)await anyio.connect_tcp(h, p, tls=True, ssl_context=ctx)await trio.open_ssl_over_tcp_stream(h, p, ssl_context=ctx)MIG026
writer.write(b); await writer.drain()await stream.send(b)await stream.send_all(b)MIG026
await reader.read(n)await stream.receive(n)await stream.receive_some(n)MIG026
await reader.readline() / readuntil(b"\n")BufferedByteReceiveStream(stream).receive_until(b"\n", max_bytes)manual bufferMIG026
writer.close(); await writer.wait_closed()await stream.aclose() (or async with stream:)await stream.aclose()MIG026
await asyncio.start_server(handler, h, p)listener = await anyio.create_tcp_listener(local_port=p); await listener.serve(handler)await trio.serve_tcp(handler, p)MIG026
loop.create_datagram_endpointawait anyio.create_udp_socket(...)trio.socketMIG026
unix socketsanyio.connect_unix(path) / create_unix_listener(path)trio.open_unix_socket(path)MIG026
text decodinganyio.streams.text.TextReceiveStream(stream)decode manually-

Files and paths

asyncio (or aiofiles)anyiotrio
await asyncio.to_thread(path.read_text)await anyio.Path(path).read_text()await trio.Path(path).read_text()
aiofiles.open(p)async with await anyio.open_file(p) as f:async with await trio.open_file(p) as f:
iterate linesasync for line in f:async for line in f:
wrap an open file objectanyio.wrap_file(fobj)trio.wrap_file(fobj)
temp filesanyio.TemporaryDirectory(), anyio.NamedTemporaryFile() (recent anyio)trio has none; use to_thread

File I/O in all three runs in worker threads under the hood (operating systems have no general async file API usable here), so it shares the thread limiter.

Signals

asyncioanyiotrioRule
loop.add_signal_handler(SIGTERM, cb)with anyio.open_signal_receiver(SIGTERM, SIGINT) as sigs: async for s in sigs:with trio.open_signal_receiver(SIGTERM) as sigs: async for s in sigs:MIG027
loop.remove_signal_handler(SIGTERM)leave the with blocksameMIG027
Ctrl-Casyncio backend: the main task sees CancelledError (cleanup runs), then anyio.run raises KeyboardInterrupt (verified)KeyboardInterrupt raised in the main task (only at checkpoints with restrict_keyboard_interrupt_to_checkpoints=True)-

Signal receivers work on Unix only (asyncio's add_signal_handler is Unix-only as well).

Introspection

asyncioanyiotrioRule
asyncio.current_task()anyio.get_current_task() (TaskInfo: id, name, parent_id, coro)trio.lowlevel.current_task()MIG029
asyncio.all_tasks()anyio.get_running_tasks()walk trio.lowlevel.current_root_task() childrenMIG029
task.get_name()anyio.get_current_task().name; set with start_soon(f, name="x")trio.lowlevel.current_task().nameMIG029
asyncio.iscoroutinefunction(f)inspect.iscoroutinefunction(f)sameMIG033
test helper: let everything settleawait anyio.wait_all_tasks_blocked()await trio.testing.wait_all_tasks_blocked()-
which library am I on?sniffio.current_async_library() or anyio's backend name in testssniffio-

Exceptions

asyncioanyiotrio
asyncio.CancelledError (BaseException)anyio.get_cancelled_exc_class() (= asyncio.CancelledError on asyncio)trio.Cancelled (BaseException)
asyncio.TimeoutError / TimeoutErrorTimeoutError from fail_aftertrio.TooSlowError from fail_after
ExceptionGroup from TaskGroupExceptionGroup from every task group (anyio 4)ExceptionGroup from every nursery (trio 0.25+ default)
asyncio.QueueEmpty / QueueFullanyio.WouldBlocktrio.WouldBlock
asyncio.IncompleteReadErroranyio.IncompleteRead / anyio.EndOfStreamtrio.EndOfChannel / empty receive_some
asyncio.InvalidStateErrorn/a (no futures)n/a
ConnectionResetError on a streamanyio.BrokenResourceErrortrio.BrokenResourceError
use of a closed streamanyio.ClosedResourceErrortrio.ClosedResourceError
two tasks receiving on one byte streamanyio.BusyResourceErrortrio.BusyResourceError

On Python 3.9/3.10 install the exceptiongroup backport (anyio depends on it there) and use exceptiongroup.catch() instead of except*.

asyncio APIs with no direct equivalent

asyncio APIWhy there is no equivalentStructured replacement
loop.call_soon(f) / call_later(d, f) / call_atno callback scheduling on the loopa child task: async def later(): await anyio.sleep(d); f(); cancel via its scope
fut.add_done_callback(cb)no futures/tasks to observedo the follow-up at the end of the child (try/finally) or wrap: async def run_then(cb): try: return await f() finally: cb()
asyncio.Future() / loop.create_future()one-shot cross-task valueEvent + value slot, or create_memory_object_stream(1) (4.15+: anyio.Future)
Task.cancel() from outsidecancellation targets scopes, not taskschild opens with CancelScope() as scope: and reports it via task_status.started(scope); the parent calls scope.cancel()
Queue.join() / task_done()completion is structuralclose the send side; the task group exit is the join
asyncio.get_event_loop() / global loopno loop object in the portable APIpass what you need explicitly; anyio.current_time(); portals for foreign threads
asyncio.wait(..., FIRST_COMPLETED)needs task objectsrace pattern: each child stores its result and cancels the group's scope
loop.set_exception_handlererrors are never orphaned in task groupscatch at the group (except*) or inside the child
asyncio.set_event_loop_policy / eager_task_factory (3.12+)loop customisation is backend-specificbackend_options (asyncio backend) or keep that module asyncio-only
contextvars set in a task and read by a siblingsame rules: each child gets a copy of the parent context at spawn timepass values explicitly

Runnable versions of every replacement are in migration-playbook.md.

More in Python

python-async-deadlock-debugger

Diagnose and fix hangs, deadlocks, livelocks and starvation in asyncio, threaded and multiprocessing Python: classify CPU spin vs blocked IO vs deadlock vs lost wakeup vs event loop blocked by sync code, collect evidence with faulthandler, py-spy dump, gdb py-bt, asyncio.all_tasks, python -m asyncio ps/pstree, debug mode and sys._current_frames, then fix lock-order cycles, non-reentrant asyncio.Lock re-acquisition, run_coroutine_threadsafe().result() on the loop thread, Queue.join without task_done, Condition lost wakeups, semaphore leaks, futures nobody resolves, nested event loops, thread pool exhaustion, multiprocessing Queue/fork deadlocks and shutdown hangs. Ships a static hang scanner and a watchdog that dumps every thread and asyncio task.

python-asyncio-pattern-advisor

Recommend, implement and review Python asyncio patterns: async vs threads vs processes (IO vs CPU-bound), coroutines vs tasks vs futures, TaskGroup, gather and return_exceptions, wait and as_completed, Semaphore and Queue worker pools with backpressure, asyncio.timeout vs wait_for, cancellation, shield and cleanup, token-bucket RPM/TPM rate limits for LLM APIs, retries with backoff and jitter, async generators and SSE token streaming, to_thread/run_in_executor, background task references, eager tasks, asyncio.run/Runner, uvloop, Jupyter, FastAPI async vs def routes, httpx.AsyncClient lifespan, async DB/vector-store clients, debug mode and pytest-asyncio.

python-beginner-concept-tutor

Teach Python concepts to beginners heading into AI engineering (data scientists, analysts, students, career switchers): plain-language definitions, analogies, minimal examples, line-by-line walkthroughs, common mistakes, check-your-understanding questions and mini exercises; Socratic hints before solutions; explaining tracebacks; mutable vs immutable, references and aliasing, is vs ==, integer division, floating point, slicing and off-by-one, indentation errors; a staged learning path from variables to calling an LLM API, embeddings and a tiny RAG. Ships a script that lists the concepts a snippet uses in learning order and traces execution step by step like Python Tutor.

python-comprehension-refactorer

Refactor Python loops into list, dict and set comprehensions and generator expressions, and unroll comprehensions that are too clever: filtering, mapping, nested-loop flattening, conditional expressions, walrus in comprehensions, dict inversion, grouping with defaultdict/Counter, lazy generators for sum/any/all/max, streaming large files, itertools (chain, islice, groupby, batched, pairwise, accumulate, product), scoping rules, side-effect anti-patterns and performance (when map/filter or numpy/pandas vectorization win). AI-engineering focus: building prompt lists, batching texts for embeddings, filtering retrieved chunks by score, id->embedding dicts, flattening tool-call results, lazy dataset streaming and token counting.

python-context-manager-writer

Write correct Python context managers and find resource-handling bugs — __enter__/__exit__ semantics and exception suppression, contextlib (contextmanager, asynccontextmanager, ExitStack, AsyncExitStack, closing, aclosing, suppress, redirect_stdout, nullcontext, chdir), async with, re-entrancy, leaked files/connections/clients/pools/locks, and AI-engineering recipes: torch.no_grad/inference_mode/autocast, eval()/train() switches, GPU cache cleanup, token and latency meters, tracing spans, temp env vars, rate-limit semaphores, httpx/aiohttp client lifecycle, FastAPI lifespan, DB/vector-store transactions, atomic checkpoint writes, seeds and agent sandboxes.

python-dataclass-converter

Convert boilerplate Python classes, dicts, tuples and JSON payloads into dataclasses, or into pydantic v2 BaseModel, TypedDict, NamedTuple, attrs or msgspec when that fits better. Covers every @dataclass option (frozen, slots, kw_only, order, unsafe_hash, match_args, weakref_slot), field() (default_factory, init=False, repr, compare, hash, metadata), __post_init__, InitVar, KW_ONLY, inheritance and default ordering, replace(), asdict deep-copy pitfalls, hashing/equality, serialization and validation. AI focus: LLM request/response and chat Message types, RAG chunks with embeddings (numpy eq gotcha), eval records, configs, typed OpenAI/Anthropic JSON.