anyio to_thread and from_thread bridging sync work under pytest-asyncio: working versions (tested 2026-10-15)
Run blocking code in worker threads with anyio.to_thread, call back into async code from those threads with anyio.from_thread, and test the whole bridge offline with pytest-asyncio.
passed6 passed · 0 failed2026-10-15 15:56 UTC- Status
- passed
- Last passed
- 2026-10-15 15:56 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38764699834
The code
Exactly the files that passed. Your agent can pull them with get_recipe("anyio-to-thread-bridge") (install), or see them on GitHub.
anyio==4.15.1 pytest-asyncio==1.4.0 pytest==9.1.1
import threading
from anyio import from_thread, to_thread
EVENTS: list[tuple[str, int]] = []
async def emit(label: str, count: int) -> None:
EVENTS.append((label, count))
def crunch(nums: list[int], label: str) -> int:
total = 0
for done, value in enumerate(nums, start=1):
total += value * value
from_thread.run(emit, label, done)
return total
async def squares_with_progress(nums: list[int], label: str = "job") -> int:
return await to_thread.run_sync(crunch, nums, label)
def loop_thread_ident() -> int:
return from_thread.run_sync(threading.get_ident)
import threading
import time
import anyio
import pytest
from anyio import from_thread, to_thread
from anyio.from_thread import start_blocking_portal
import bridge
from bridge import EVENTS, emit, squares_with_progress
@pytest.fixture(autouse=True)
def _clear_events():
EVENTS.clear()
yield
EVENTS.clear()
async def test_to_thread_returns_result():
assert await squares_with_progress([1, 2, 3]) == 14
async def test_worker_can_call_async_callback_in_order():
await squares_with_progress([1, 2, 3], "scan")
assert EVENTS == [("scan", 1), ("scan", 2), ("scan", 3)]
async def test_from_thread_run_sync_runs_on_loop_thread():
loop_ident = threading.get_ident()
worker_ident = await to_thread.run_sync(threading.get_ident)
assert worker_ident != loop_ident
assert await to_thread.run_sync(bridge.loop_thread_ident) == loop_ident
async def test_plain_thread_cannot_use_from_thread():
errors: list[BaseException] = []
def target():
try:
from_thread.run(emit, "x", 0)
except BaseException as exc:
errors.append(exc)
thread = threading.Thread(target=target)
thread.start()
thread.join()
assert len(errors) == 1 and isinstance(errors[0], RuntimeError)
async def test_cancel_waits_for_worker_by_default():
finished: list[bool] = []
def slow():
time.sleep(0.1)
finished.append(True)
return 1
with anyio.move_on_after(0.01):
await to_thread.run_sync(slow)
assert finished == [True]
def test_blocking_portal_bridges_plain_threads():
with start_blocking_portal() as portal:
assert portal.call(squares_with_progress, [2, 3], "portal") == 13
assert ("portal", 2) in EVENTS
[pytest] pythonpath = . asyncio_mode = auto
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Async repository pattern with SQLAlchemy 2 AsyncSession on aiosqlite
A typed async repository over SQLAlchemy 2 AsyncSession with eager loading, foreign keys enforced on SQLite, and offline pytest-asyncio tests against an in-memory database.
asyncio TaskGroup, timeouts and semaphores tested with pytest-asyncio
Bounded concurrent fetches with asyncio.Semaphore, per-call asyncio.timeout and TaskGroup error handling, tested offline with pytest-asyncio loop scope config and async fixtures.
FastAPI with async SQLAlchemy 2 on aiosqlite, tested offline with per-test in-memory DBs
A FastAPI app using AsyncSession and async_sessionmaker on aiosqlite, tested with httpx.AsyncClient over ASGITransport, overriding the session dependency with a fresh in-memory database for each test.
SQLAlchemy 2 async ORM + aiosqlite with pytest-asyncio
SQLAlchemy 2.x typed async ORM (Mapped, mapped_column, async_sessionmaker, selectinload) on aiosqlite, with pytest-asyncio fixtures.