[{"data":1,"prerenderedAt":1716},["ShallowReactive",2],{"page-\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002F":3},{"id":4,"title":5,"body":6,"description":1675,"extension":1676,"meta":1677,"navigation":356,"path":1712,"seo":1713,"stem":1714,"__hash__":1715},"content\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Findex.md","Testing with AnyIO & Trio",{"type":7,"value":8,"toc":1655},"minimark",[9,22,27,79,83,99,267,286,290,295,323,336,340,369,373,419,433,437,537,556,560,629,635,639,646,670,677,694,698,837,841,851,916,937,945,1029,1033,1036,1039,1068,1075,1079,1085,1247,1257,1267,1271,1274,1327,1364,1427,1433,1436,1449,1453,1459,1514,1520,1528,1532,1548,1566,1580,1599,1605,1609,1644,1651],[10,11,12,13,17,18,21],"p",{},"A library that advertises asyncio and Trio support has, in practice, two implementations and one test suite — or two test suites that drift. ",[14,15,16],"code",{},"AnyIO"," removes that choice by defining async primitives once and implementing them on both runtimes, so a single test body can be executed against each. For application code that will only ever run on asyncio the value is smaller but not zero: AnyIO's cancel scopes and task groups have cleaner semantics than the asyncio equivalents, and a test written against them is explicit about scope in a way ",[14,19,20],{},"wait_for"," never is.",[23,24,26],"h2",{"id":25},"prerequisites","Prerequisites",[28,29,30,41,51,66],"ul",{},[31,32,33,36,37,40],"li",{},[14,34,35],{},"anyio >= 4.0"," (the release that made task groups and cancel scopes the primary API) plus ",[14,38,39],{},"trio"," as a test-only dependency.",[31,42,43,46,47,50],{},[14,44,45],{},"pytest >= 8.0",". AnyIO ships its own pytest plugin; no separate ",[14,48,49],{},"pytest-anyio"," package exists.",[31,52,53,54,57,58,61,62,65],{},"Python 3.9+, though 3.11+ is worth having so ",[14,55,56],{},"ExceptionGroup"," and ",[14,59,60],{},"except*"," are available natively rather than through the ",[14,63,64],{},"exceptiongroup"," backport.",[31,67,68,69,72,73,78],{},"An understanding of what ",[14,70,71],{},"pytest-asyncio"," does to collection, since the two plugins compete — see ",[74,75,77],"a",{"href":76},"\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002F","pytest-asyncio in depth",".",[23,80,82],{"id":81},"core-concept-the-backend-is-a-fixture","Core concept: the backend is a fixture",[10,84,85,86,89,90,94,95,98],{},"AnyIO's pytest integration is built from one idea. Tests marked with ",[14,87,88],{},"@pytest.mark.anyio"," are run by the plugin, and ",[91,92,93],"em",{},"how"," they are run is decided by a fixture called ",[14,96,97],{},"anyio_backend",". Because it is an ordinary fixture, parametrising it parametrises the runtime: one test function, two test items, one per backend.",[100,101,104,255],"figure",{"className":102},[103],"diagram",[105,106,113,114,113,118,113,122,113,140,113,148,113,157,113,166,113,172,113,175,113,182,113,189,113,192,113,195,113,201,113,204,113,212,113,218,113,222,113,227,113,230,113,234,113,238,113,241,113,247,113,251],"svg",{"viewBox":107,"role":108,"ariaLabelledBy":109,"xmlns":112},"0 0 820 276","img",[110,111],"anyio-t","anyio-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[115,116,117],"title",{"id":110},"One test body expanded into two backend items",[119,120,121],"desc",{"id":111},"A single marked test function passes through the anyio_backend fixture, which is parametrised with asyncio and trio. Collection produces two test items with backend suffixes in their identifiers, each executed on its own runtime, and both report independently.",[123,124,125,126,113],"defs",{},"\n    ",[127,128,135],"marker",{"id":129,"viewBox":130,"refX":131,"refY":132,"markerWidth":133,"markerHeight":133,"orient":134},"anyio-a","0 0 10 10","9","5","7","auto-start-reverse",[136,137],"path",{"d":138,"fill":139},"M0 0 L10 5 L0 10 z","#3d405b",[141,142],"rect",{"x":143,"y":143,"width":144,"height":145,"rx":146,"fill":147},"0","820","276","14","#fffdf8",[149,150,156],"text",{"x":151,"y":152,"textAnchor":153,"fontSize":154,"fontWeight":155,"fill":139},"410","28","middle","16","700","Parametrising the runtime, not the data",[141,158],{"x":159,"y":160,"width":161,"height":162,"rx":163,"fill":164,"stroke":139,"strokeWidth":165},"30","104","196","70","11","#f4f1de","1.6",[149,167,171],{"x":168,"y":169,"textAnchor":153,"fontSize":170,"fontWeight":155,"fill":139},"128","132","12.5","async def test_stream()",[149,173,88],{"x":168,"y":174,"textAnchor":153,"fontSize":163,"fill":139},"152",[176,177],"line",{"x1":178,"y1":179,"x2":180,"y2":179,"stroke":139,"strokeWidth":165,"markerEnd":181},"230","139","266","url(#anyio-a)",[141,183],{"x":184,"y":160,"width":185,"height":162,"rx":163,"fill":186,"stroke":187,"strokeWidth":188},"272","190","#f7f0da","#f2cc8f","2",[149,190,97],{"x":191,"y":169,"textAnchor":153,"fontSize":170,"fontWeight":155,"fill":139},"367",[149,193,194],{"x":191,"y":174,"textAnchor":153,"fontSize":163,"fill":139},"params=[asyncio, trio]",[176,196],{"x1":197,"y1":198,"x2":199,"y2":200,"stroke":139,"strokeWidth":165,"markerEnd":181},"462","126","520","86",[176,202],{"x1":197,"y1":174,"x2":199,"y2":203,"stroke":139,"strokeWidth":165,"markerEnd":181},"198",[141,205],{"x":206,"y":207,"width":208,"height":209,"rx":163,"fill":210,"stroke":211,"strokeWidth":188},"526","52","262","72","#e6f0ea","#81b29a",[149,213,217],{"x":214,"y":215,"textAnchor":153,"fontSize":216,"fontWeight":155,"fill":139},"657","78","12","test_stream[asyncio]",[149,219,221],{"x":214,"y":220,"textAnchor":153,"fontSize":163,"fill":139},"98","runs on the asyncio loop",[149,223,226],{"x":214,"y":224,"textAnchor":153,"fontSize":163,"fill":225},"116","#2a5f49","reported independently",[141,228],{"x":206,"y":229,"width":208,"height":209,"rx":163,"fill":210,"stroke":211,"strokeWidth":188},"166",[149,231,233],{"x":214,"y":232,"textAnchor":153,"fontSize":216,"fontWeight":155,"fill":139},"192","test_stream[trio]",[149,235,237],{"x":214,"y":236,"textAnchor":153,"fontSize":163,"fill":139},"212","runs on the Trio nursery",[149,239,240],{"x":214,"y":178,"textAnchor":153,"fontSize":163,"fill":225},"failure names the backend",[141,242],{"x":159,"y":161,"width":161,"height":243,"rx":244,"fill":147,"stroke":245,"strokeWidth":246},"54","10","rgba(61,64,91,0.35)","1.5",[149,248,250],{"x":168,"y":249,"textAnchor":153,"fontSize":163,"fill":139},"220","one body, one set of",[149,252,254],{"x":168,"y":253,"textAnchor":153,"fontSize":163,"fill":139},"238","assertions to maintain",[256,257,258,259,262,263,266],"figcaption",{},"Because the backend arrives through the fixture system, everything that works for ordinary parametrization works here — narrowing with ",[14,260,261],{},"-k trio",", marking one backend ",[14,264,265],{},"xfail",", or pinning a single test to one runtime.",[10,268,269,270,273,274,277,278,281,282,285],{},"The second idea follows from the first: anything the test touches must exist on both runtimes. ",[14,271,272],{},"asyncio.Event"," does not exist in Trio, so portable tests use ",[14,275,276],{},"anyio.Event",". ",[14,279,280],{},"asyncio.wait_for"," has no Trio equivalent, so portable code uses ",[14,283,284],{},"anyio.fail_after",". This is not overhead imposed by the test framework — it is the same constraint the library under test is already living with, made visible.",[23,287,289],{"id":288},"step-by-step-implementation","Step-by-step implementation",[291,292,294],"h3",{"id":293},"_1-install-both-backends","1. Install both backends",[296,297,302],"pre",{"className":298,"code":299,"language":300,"meta":301,"style":301},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml\n[project.optional-dependencies]\ntest = [\"pytest>=8.0\", \"anyio[trio]>=4.0\"]\n","toml","",[14,303,304,311,317],{"__ignoreMap":301},[305,306,308],"span",{"class":176,"line":307},1,[305,309,310],{},"# pyproject.toml\n",[305,312,314],{"class":176,"line":313},2,[305,315,316],{},"[project.optional-dependencies]\n",[305,318,320],{"class":176,"line":319},3,[305,321,322],{},"test = [\"pytest>=8.0\", \"anyio[trio]>=4.0\"]\n",[10,324,325,328,329,331,332,335],{},[14,326,327],{},"anyio[trio]"," pulls Trio in as an extra. Without it the ",[14,330,39],{}," parameter raises ",[14,333,334],{},"ImportError"," at test time rather than skipping, which is the right behaviour — a matrix entry that silently disappears is worse than one that fails loudly.",[291,337,339],{"id":338},"_2-mark-the-module","2. Mark the module",[296,341,345],{"className":342,"code":343,"language":344,"meta":301,"style":301},"language-python shiki shiki-themes github-light github-dark","import pytest\n\n# Every coroutine test in this module is run by AnyIO rather than pytest-asyncio.\npytestmark = pytest.mark.anyio\n","python",[14,346,347,352,358,363],{"__ignoreMap":301},[305,348,349],{"class":176,"line":307},[305,350,351],{},"import pytest\n",[305,353,354],{"class":176,"line":313},[305,355,357],{"emptyLinePlaceholder":356},true,"\n",[305,359,360],{"class":176,"line":319},[305,361,362],{},"# Every coroutine test in this module is run by AnyIO rather than pytest-asyncio.\n",[305,364,366],{"class":176,"line":365},4,[305,367,368],{},"pytestmark = pytest.mark.anyio\n",[291,370,372],{"id":371},"_3-parametrise-the-backend","3. Parametrise the backend",[296,374,376],{"className":342,"code":375,"language":344,"meta":301,"style":301},"# conftest.py\nimport pytest\n\n\n@pytest.fixture(params=[\"asyncio\", \"trio\"])\ndef anyio_backend(request):\n    # Overriding the plugin's default fixture turns one test into two items.\n    return request.param\n",[14,377,378,383,387,391,395,401,407,413],{"__ignoreMap":301},[305,379,380],{"class":176,"line":307},[305,381,382],{},"# conftest.py\n",[305,384,385],{"class":176,"line":313},[305,386,351],{},[305,388,389],{"class":176,"line":319},[305,390,357],{"emptyLinePlaceholder":356},[305,392,393],{"class":176,"line":365},[305,394,357],{"emptyLinePlaceholder":356},[305,396,398],{"class":176,"line":397},5,[305,399,400],{},"@pytest.fixture(params=[\"asyncio\", \"trio\"])\n",[305,402,404],{"class":176,"line":403},6,[305,405,406],{},"def anyio_backend(request):\n",[305,408,410],{"class":176,"line":409},7,[305,411,412],{},"    # Overriding the plugin's default fixture turns one test into two items.\n",[305,414,416],{"class":176,"line":415},8,[305,417,418],{},"    return request.param\n",[10,420,421,422,425,426,429,430,78],{},"The plugin's default implementation returns ",[14,423,424],{},"\"asyncio\""," as a plain string. Returning a tuple lets you pass backend options — ",[14,427,428],{},"(\"asyncio\", {\"use_uvloop\": True})"," is the usual reason — and the same tuple form works inside ",[14,431,432],{},"params",[291,434,436],{"id":435},"_4-use-portable-primitives","4. Use portable primitives",[296,438,440],{"className":342,"code":439,"language":344,"meta":301,"style":301},"import anyio\n\n\nasync def test_worker_signals_completion():\n    done = anyio.Event()                      # not asyncio.Event\n    results: list[int] = []\n\n    async def worker():\n        results.append(42)\n        done.set()\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(worker)\n        # fail_after is a cancel scope: it bounds everything in the block.\n        with anyio.fail_after(1.0):\n            await done.wait()\n\n    assert results == [42]\n",[14,441,442,447,451,455,460,465,470,474,479,485,491,496,502,508,514,520,526,531],{"__ignoreMap":301},[305,443,444],{"class":176,"line":307},[305,445,446],{},"import anyio\n",[305,448,449],{"class":176,"line":313},[305,450,357],{"emptyLinePlaceholder":356},[305,452,453],{"class":176,"line":319},[305,454,357],{"emptyLinePlaceholder":356},[305,456,457],{"class":176,"line":365},[305,458,459],{},"async def test_worker_signals_completion():\n",[305,461,462],{"class":176,"line":397},[305,463,464],{},"    done = anyio.Event()                      # not asyncio.Event\n",[305,466,467],{"class":176,"line":403},[305,468,469],{},"    results: list[int] = []\n",[305,471,472],{"class":176,"line":409},[305,473,357],{"emptyLinePlaceholder":356},[305,475,476],{"class":176,"line":415},[305,477,478],{},"    async def worker():\n",[305,480,482],{"class":176,"line":481},9,[305,483,484],{},"        results.append(42)\n",[305,486,488],{"class":176,"line":487},10,[305,489,490],{},"        done.set()\n",[305,492,494],{"class":176,"line":493},11,[305,495,357],{"emptyLinePlaceholder":356},[305,497,499],{"class":176,"line":498},12,[305,500,501],{},"    async with anyio.create_task_group() as tg:\n",[305,503,505],{"class":176,"line":504},13,[305,506,507],{},"        tg.start_soon(worker)\n",[305,509,511],{"class":176,"line":510},14,[305,512,513],{},"        # fail_after is a cancel scope: it bounds everything in the block.\n",[305,515,517],{"class":176,"line":516},15,[305,518,519],{},"        with anyio.fail_after(1.0):\n",[305,521,523],{"class":176,"line":522},16,[305,524,525],{},"            await done.wait()\n",[305,527,529],{"class":176,"line":528},17,[305,530,357],{"emptyLinePlaceholder":356},[305,532,534],{"class":176,"line":533},18,[305,535,536],{},"    assert results == [42]\n",[10,538,539,540,543,544,547,548,551,552,555],{},"Note what is absent: no ",[14,541,542],{},"create_task",", no explicit ",[14,545,546],{},"await task",", no ",[14,549,550],{},"gather",". The task group owns its children, and the ",[14,553,554],{},"async with"," block does not exit until every child has finished or been cancelled. That is the property worth testing against, because it is the one that stops tasks leaking.",[291,557,559],{"id":558},"_5-pin-the-tests-that-cannot-be-portable","5. Pin the tests that cannot be portable",[296,561,563],{"className":342,"code":562,"language":344,"meta":301,"style":301},"import pytest\n\n\n@pytest.fixture\ndef anyio_backend():\n    # This module reaches for asyncio internals; run it on asyncio only.\n    return \"asyncio\"\n\n\nasync def test_uses_loop_internals():\n    import asyncio\n\n    loop = asyncio.get_running_loop()\n    assert loop.get_debug() is True\n",[14,564,565,569,573,577,582,587,592,597,601,605,610,615,619,624],{"__ignoreMap":301},[305,566,567],{"class":176,"line":307},[305,568,351],{},[305,570,571],{"class":176,"line":313},[305,572,357],{"emptyLinePlaceholder":356},[305,574,575],{"class":176,"line":319},[305,576,357],{"emptyLinePlaceholder":356},[305,578,579],{"class":176,"line":365},[305,580,581],{},"@pytest.fixture\n",[305,583,584],{"class":176,"line":397},[305,585,586],{},"def anyio_backend():\n",[305,588,589],{"class":176,"line":403},[305,590,591],{},"    # This module reaches for asyncio internals; run it on asyncio only.\n",[305,593,594],{"class":176,"line":409},[305,595,596],{},"    return \"asyncio\"\n",[305,598,599],{"class":176,"line":415},[305,600,357],{"emptyLinePlaceholder":356},[305,602,603],{"class":176,"line":481},[305,604,357],{"emptyLinePlaceholder":356},[305,606,607],{"class":176,"line":487},[305,608,609],{},"async def test_uses_loop_internals():\n",[305,611,612],{"class":176,"line":493},[305,613,614],{},"    import asyncio\n",[305,616,617],{"class":176,"line":498},[305,618,357],{"emptyLinePlaceholder":356},[305,620,621],{"class":176,"line":504},[305,622,623],{},"    loop = asyncio.get_running_loop()\n",[305,625,626],{"class":176,"line":510},[305,627,628],{},"    assert loop.get_debug() is True\n",[10,630,631,632,634],{},"A local ",[14,633,97],{}," override shadows the parametrised one for that module. This is the clean way to keep three or four genuinely backend-specific tests without abandoning portability for the rest.",[23,636,638],{"id":637},"verification","Verification",[10,640,641,642,645],{},"Backend coverage is visible in the test identifiers, so ",[14,643,644],{},"--collect-only"," is the check:",[296,647,651],{"className":648,"code":649,"language":650,"meta":301,"style":301},"language-bash shiki shiki-themes github-light github-dark","pytest tests\u002Ftest_streams.py --collect-only -q\n","bash",[14,652,653],{"__ignoreMap":301},[305,654,655,659,663,667],{"class":176,"line":307},[305,656,658],{"class":657},"sScJk","pytest",[305,660,662],{"class":661},"sZZnC"," tests\u002Ftest_streams.py",[305,664,666],{"class":665},"sj4cs"," --collect-only",[305,668,669],{"class":665}," -q\n",[296,671,675],{"className":672,"code":674,"language":149,"meta":301},[673],"language-text","tests\u002Ftest_streams.py::test_worker_signals_completion[asyncio]\ntests\u002Ftest_streams.py::test_worker_signals_completion[trio]\ntests\u002Ftest_streams.py::test_uses_loop_internals[asyncio]\n3 tests collected\n",[14,676,674],{"__ignoreMap":301},[10,678,679,680,683,684,687,688,690,691,693],{},"Two items for the portable test and one for the pinned test is exactly the intended shape. If every test shows a single ",[14,681,682],{},"[asyncio]"," item, the ",[14,685,686],{},"conftest.py"," override is not being picked up — usually because it sits in a directory above a nearer ",[14,689,686],{}," that defines its own. If the tests show no backend suffix at all, ",[14,692,71],{}," claimed them first, which is the failure covered next.",[23,695,697],{"id":696},"troubleshooting","Troubleshooting",[699,700,701,717],"table",{},[702,703,704],"thead",{},[705,706,707,711,714],"tr",{},[708,709,710],"th",{},"Symptom",[708,712,713],{},"Root cause",[708,715,716],{},"Fix",[718,719,720,742,761,779,797,817],"tbody",{},[705,721,722,728,731],{},[723,724,725],"td",{},[14,726,727],{},"fixture 'anyio_backend' not found",[723,729,730],{},"AnyIO plugin inactive, or another plugin claimed the test",[723,732,733,734,737,738,741],{},"Confirm with ",[14,735,736],{},"pytest --fixtures","; set ",[14,739,740],{},"asyncio_mode = \"strict\""," nearby",[705,743,744,751,756],{},[723,745,746,747,750],{},"Tests run once, no ",[14,748,749],{},"[backend]"," suffix",[723,752,753,755],{},[14,754,71],{}," in auto mode collected them",[723,757,758,759],{},"Move AnyIO tests to their own directory with a local ",[14,760,686],{},[705,762,763,769,772],{},[723,764,765,768],{},[14,766,767],{},"RuntimeError: no running event loop"," on the Trio item",[723,770,771],{},"asyncio API called in a portable test",[723,773,774,775,778],{},"Replace with the ",[14,776,777],{},"anyio"," equivalent, or pin the test to asyncio",[705,780,781,787,792],{},[723,782,783,786],{},[14,784,785],{},"ImportError: trio"," at test time",[723,788,789,791],{},[14,790,777],{}," installed without the extra",[723,793,794,795],{},"Depend on ",[14,796,327],{},[705,798,799,806,811],{},[723,800,801,802,805],{},"Timeout raises ",[14,803,804],{},"TimeoutError"," on one backend only",[723,807,808,810],{},[14,809,280],{}," used instead of a cancel scope",[723,812,813,814,816],{},"Use ",[14,815,284],{},", which normalises the exception",[705,818,819,825,828],{},[723,820,821,822,824],{},"Task group failure reports ",[14,823,56],{}," unexpectedly",[723,826,827],{},"Structured concurrency semantics, not a bug",[723,829,830,831,833,834],{},"Match with ",[14,832,60],{}," or assert on ",[14,835,836],{},"excinfo.value.exceptions",[23,838,840],{"id":839},"cancel-scopes-are-the-real-portability-story","Cancel scopes are the real portability story",[10,842,843,846,847,850],{},[14,844,845],{},"asyncio.wait_for(coro, timeout)"," applies a deadline to one awaitable. AnyIO applies it to a ",[91,848,849],{},"region"," of code:",[296,852,854],{"className":342,"code":853,"language":344,"meta":301,"style":301},"import anyio\n\n\nasync def test_partial_work_is_kept_when_the_deadline_passes(feed):\n    received: list[bytes] = []\n\n    # move_on_after does not raise; it cancels the block and records the fact.\n    with anyio.move_on_after(0.25) as scope:\n        async for chunk in feed.stream():\n            received.append(chunk)\n\n    assert scope.cancelled_caught is True     # the deadline actually fired\n    assert received, \"the stream should have yielded something before the cutoff\"\n",[14,855,856,860,864,868,873,878,882,887,892,897,902,906,911],{"__ignoreMap":301},[305,857,858],{"class":176,"line":307},[305,859,446],{},[305,861,862],{"class":176,"line":313},[305,863,357],{"emptyLinePlaceholder":356},[305,865,866],{"class":176,"line":319},[305,867,357],{"emptyLinePlaceholder":356},[305,869,870],{"class":176,"line":365},[305,871,872],{},"async def test_partial_work_is_kept_when_the_deadline_passes(feed):\n",[305,874,875],{"class":176,"line":397},[305,876,877],{},"    received: list[bytes] = []\n",[305,879,880],{"class":176,"line":403},[305,881,357],{"emptyLinePlaceholder":356},[305,883,884],{"class":176,"line":409},[305,885,886],{},"    # move_on_after does not raise; it cancels the block and records the fact.\n",[305,888,889],{"class":176,"line":415},[305,890,891],{},"    with anyio.move_on_after(0.25) as scope:\n",[305,893,894],{"class":176,"line":481},[305,895,896],{},"        async for chunk in feed.stream():\n",[305,898,899],{"class":176,"line":487},[305,900,901],{},"            received.append(chunk)\n",[305,903,904],{"class":176,"line":493},[305,905,357],{"emptyLinePlaceholder":356},[305,907,908],{"class":176,"line":498},[305,909,910],{},"    assert scope.cancelled_caught is True     # the deadline actually fired\n",[305,912,913],{"class":176,"line":504},[305,914,915],{},"    assert received, \"the stream should have yielded something before the cutoff\"\n",[10,917,918,919,922,923,925,926,929,930,933,934,936],{},"The distinction matters for tests specifically. ",[14,920,921],{},"fail_after"," raises ",[14,924,804],{},", which is right when the deadline is a failure; ",[14,927,928],{},"move_on_after"," returns and sets ",[14,931,932],{},"cancelled_caught",", which is right when the deadline is part of the behaviour being tested — a poller that gives up, a batch that flushes on a timer, a stream that returns what it has. Expressing \"give up after 250 ms, then assert on what arrived\" with ",[14,935,280],{}," requires wrapping the call in a try\u002Fexcept and losing the partial results; the cancel scope keeps them because the cancellation unwinds the loop body rather than the whole coroutine.",[10,938,939,940,942,943,78],{},"Cancel scopes also nest correctly, which ",[14,941,20],{}," does not do in any obvious way. An outer scope with a 5-second budget containing an inner scope with a 200 ms budget behaves the way the code reads: the inner deadline fires first and only cancels its own block. Testing that behaviour — that an inner timeout does not consume the outer budget, and that an outer timeout does interrupt the inner block — is the kind of assertion that catches real retry-loop bugs, and it is nearly unwriteable against ",[14,944,20],{},[100,946,948,1023],{"className":947},[103],[105,949,113,954,113,957,113,960,113,964,113,968,113,975,113,979,113,984,113,990,113,994,113,997,113,1000,113,1003,113,1008,113,1011,113,1015,113,1019],{"viewBox":950,"role":108,"ariaLabelledBy":951,"xmlns":112},"0 0 800 246",[952,953],"scope-t","scope-d",[115,955,956],{"id":952},"Nested cancel scopes and which deadline fires",[119,958,959],{"id":953},"An outer cancel scope with a five second budget contains three inner scopes of two hundred milliseconds each. The first two inner scopes complete normally, the third exceeds its own deadline and is cancelled alone, and the outer scope continues with its remaining budget intact.",[141,961],{"x":143,"y":143,"width":962,"height":963,"rx":146,"fill":147},"800","246",[149,965,967],{"x":966,"y":152,"textAnchor":153,"fontSize":154,"fontWeight":155,"fill":139},"400","An inner deadline cancels only its own block",[141,969],{"x":970,"y":971,"width":972,"height":973,"rx":216,"fill":164,"stroke":139,"strokeWidth":974},"34","50","732","120","1.8",[149,976,978],{"x":243,"y":977,"fontSize":216,"fontWeight":155,"fill":139},"74","fail_after(5.0) — outer budget",[141,980],{"x":981,"y":982,"width":161,"height":983,"rx":244,"fill":210,"stroke":211,"strokeWidth":974},"64","90","62",[149,985,989],{"x":986,"y":987,"textAnchor":153,"fontSize":988,"fill":139},"162","114","11.5","move_on_after(0.2)",[149,991,993],{"x":986,"y":992,"textAnchor":153,"fontSize":163,"fill":225},"134","completed in 40 ms",[141,995],{"x":996,"y":982,"width":161,"height":983,"rx":244,"fill":210,"stroke":211,"strokeWidth":974},"278",[149,998,989],{"x":999,"y":987,"textAnchor":153,"fontSize":988,"fill":139},"376",[149,1001,1002],{"x":999,"y":992,"textAnchor":153,"fontSize":163,"fill":225},"completed in 90 ms",[141,1004],{"x":1005,"y":982,"width":161,"height":983,"rx":244,"fill":1006,"stroke":1007,"strokeWidth":188},"492","#fbe9e3","#e07a5f",[149,1009,989],{"x":1010,"y":987,"textAnchor":153,"fontSize":988,"fill":139},"590",[149,1012,1014],{"x":1010,"y":992,"textAnchor":153,"fontSize":163,"fill":1013},"#8f3d22","cancelled_caught = True",[141,1016],{"x":970,"y":1017,"width":972,"height":1018,"rx":244,"fill":147,"stroke":245,"strokeWidth":246},"186","44",[149,1020,1022],{"x":966,"y":1021,"textAnchor":153,"fontSize":216,"fill":139},"213","Outer budget untouched: 4.67 s remain, and the loop proceeds to the next item.",[256,1024,1025,1026,1028],{},"Each scope owns its own cancellation. Asserting on ",[14,1027,932],{}," per scope is how a test distinguishes \"this one item was slow\" from \"the whole operation ran out of time\".",[23,1030,1032],{"id":1031},"what-portability-costs","What portability costs",[10,1034,1035],{},"Two things, and both are worth stating plainly before a team commits.",[10,1037,1038],{},"The first is runtime: every parametrised test runs twice. On a suite of 800 async tests that is not a rounding error, and the mitigation is selectivity — parametrise the tests that exercise concurrency primitives, scheduling, cancellation or stream semantics, and pin everything else to one backend. A test that posts JSON and asserts on a status code learns nothing from running on Trio.",[10,1040,1041,1042,1045,1046,1045,1049,1052,1053,1056,1057,1060,1061,1063,1064,1067],{},"The second is expressiveness. Inside a portable test you cannot reach for ",[14,1043,1044],{},"loop.call_later",", ",[14,1047,1048],{},"loop.run_in_executor",[14,1050,1051],{},"asyncio.all_tasks()"," or any of the introspection that makes certain assertions easy. ",[14,1054,1055],{},"anyio.to_thread.run_sync"," covers the executor case, and ",[14,1058,1059],{},"anyio.get_current_task()"," covers part of the introspection case, but the leak-detection fixture that asserts ",[14,1062,1051],{}," is empty has no portable equivalent — task groups are supposed to make it unnecessary, which is true right up until the code under test calls ",[14,1065,1066],{},"asyncio.create_task"," directly.",[10,1069,1070,1071,78],{},"The honest summary: if the library has to support Trio, AnyIO is not a choice but a requirement, and the constraints are the ones the library already has. If it does not, adopt AnyIO for the cancel-scope semantics and pin the backend, which gets the better API without doubling the suite. Migrating an existing asyncio suite is a mechanical but non-trivial exercise, covered step by step in ",[74,1072,1074],{"href":1073},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Fporting-a-pytest-asyncio-suite-to-anyio\u002F","porting a pytest-asyncio suite to AnyIO",[23,1076,1078],{"id":1077},"memory-object-streams-replace-queues","Memory object streams replace queues",[10,1080,1081,1084],{},[14,1082,1083],{},"asyncio.Queue"," has no Trio counterpart, and the substitute AnyIO offers is better suited to testing anyway. A memory object stream is a typed, bounded channel split into a send half and a receive half, and closing either half is observable on the other — which is exactly the signal a test needs to assert that a producer finished rather than stalled.",[296,1086,1088],{"className":342,"code":1087,"language":344,"meta":301,"style":301},"import anyio\nimport pytest\n\n\nasync def test_producer_closes_the_stream_when_done():\n    # Buffer of 0 means every send waits for a receive: the tightest coupling,\n    # and the one that surfaces ordering bugs immediately.\n    send, receive = anyio.create_memory_object_stream[int](max_buffer_size=0)\n\n    async def produce():\n        async with send:                     # closing the send half is the \"done\" signal\n            for value in range(3):\n                await send.send(value)\n\n    received: list[int] = []\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(produce)\n        async with receive:\n            async for value in receive:      # loop ends when the sender closes\n                received.append(value)\n\n    assert received == [0, 1, 2]\n\n\nasync def test_consumer_sees_closure_not_a_hang():\n    send, receive = anyio.create_memory_object_stream[int](max_buffer_size=1)\n    await send.aclose()\n\n    # A closed stream raises rather than blocking forever — assertable, unlike a hang.\n    with pytest.raises(anyio.EndOfStream):\n        await receive.receive()\n",[14,1089,1090,1094,1098,1102,1106,1111,1116,1121,1126,1130,1135,1140,1145,1150,1154,1159,1163,1168,1173,1179,1185,1190,1196,1201,1206,1212,1218,1224,1229,1235,1241],{"__ignoreMap":301},[305,1091,1092],{"class":176,"line":307},[305,1093,446],{},[305,1095,1096],{"class":176,"line":313},[305,1097,351],{},[305,1099,1100],{"class":176,"line":319},[305,1101,357],{"emptyLinePlaceholder":356},[305,1103,1104],{"class":176,"line":365},[305,1105,357],{"emptyLinePlaceholder":356},[305,1107,1108],{"class":176,"line":397},[305,1109,1110],{},"async def test_producer_closes_the_stream_when_done():\n",[305,1112,1113],{"class":176,"line":403},[305,1114,1115],{},"    # Buffer of 0 means every send waits for a receive: the tightest coupling,\n",[305,1117,1118],{"class":176,"line":409},[305,1119,1120],{},"    # and the one that surfaces ordering bugs immediately.\n",[305,1122,1123],{"class":176,"line":415},[305,1124,1125],{},"    send, receive = anyio.create_memory_object_stream[int](max_buffer_size=0)\n",[305,1127,1128],{"class":176,"line":481},[305,1129,357],{"emptyLinePlaceholder":356},[305,1131,1132],{"class":176,"line":487},[305,1133,1134],{},"    async def produce():\n",[305,1136,1137],{"class":176,"line":493},[305,1138,1139],{},"        async with send:                     # closing the send half is the \"done\" signal\n",[305,1141,1142],{"class":176,"line":498},[305,1143,1144],{},"            for value in range(3):\n",[305,1146,1147],{"class":176,"line":504},[305,1148,1149],{},"                await send.send(value)\n",[305,1151,1152],{"class":176,"line":510},[305,1153,357],{"emptyLinePlaceholder":356},[305,1155,1156],{"class":176,"line":516},[305,1157,1158],{},"    received: list[int] = []\n",[305,1160,1161],{"class":176,"line":522},[305,1162,501],{},[305,1164,1165],{"class":176,"line":528},[305,1166,1167],{},"        tg.start_soon(produce)\n",[305,1169,1170],{"class":176,"line":533},[305,1171,1172],{},"        async with receive:\n",[305,1174,1176],{"class":176,"line":1175},19,[305,1177,1178],{},"            async for value in receive:      # loop ends when the sender closes\n",[305,1180,1182],{"class":176,"line":1181},20,[305,1183,1184],{},"                received.append(value)\n",[305,1186,1188],{"class":176,"line":1187},21,[305,1189,357],{"emptyLinePlaceholder":356},[305,1191,1193],{"class":176,"line":1192},22,[305,1194,1195],{},"    assert received == [0, 1, 2]\n",[305,1197,1199],{"class":176,"line":1198},23,[305,1200,357],{"emptyLinePlaceholder":356},[305,1202,1204],{"class":176,"line":1203},24,[305,1205,357],{"emptyLinePlaceholder":356},[305,1207,1209],{"class":176,"line":1208},25,[305,1210,1211],{},"async def test_consumer_sees_closure_not_a_hang():\n",[305,1213,1215],{"class":176,"line":1214},26,[305,1216,1217],{},"    send, receive = anyio.create_memory_object_stream[int](max_buffer_size=1)\n",[305,1219,1221],{"class":176,"line":1220},27,[305,1222,1223],{},"    await send.aclose()\n",[305,1225,1227],{"class":176,"line":1226},28,[305,1228,357],{"emptyLinePlaceholder":356},[305,1230,1232],{"class":176,"line":1231},29,[305,1233,1234],{},"    # A closed stream raises rather than blocking forever — assertable, unlike a hang.\n",[305,1236,1238],{"class":176,"line":1237},30,[305,1239,1240],{},"    with pytest.raises(anyio.EndOfStream):\n",[305,1242,1244],{"class":176,"line":1243},31,[305,1245,1246],{},"        await receive.receive()\n",[10,1248,1249,1250,1252,1253,1256],{},"The second test is the one worth copying into any codebase using queues. With ",[14,1251,1083],{}," there is no closure concept at all: a consumer waiting on an empty queue whose producer has died waits forever, and the test that should catch it instead hangs until the suite's timeout kills it. Memory object streams turn that into ",[14,1254,1255],{},"EndOfStream",", which a test can assert on in a millisecond.",[10,1258,1259,1262,1263,1266],{},[14,1260,1261],{},"max_buffer_size"," deserves a deliberate choice rather than a default. Zero forces a rendezvous, which makes interleaving deterministic and is the right setting for tests about ordering. A finite buffer lets the producer run ahead by a known amount, which is the right setting for tests about backpressure — fill the buffer, assert the next ",[14,1264,1265],{},"send"," blocks, drain one item, assert it unblocks. An unbounded buffer, which AnyIO deliberately makes awkward to request, removes backpressure entirely and with it the ability to test for it.",[23,1268,1270],{"id":1269},"running-the-backend-matrix-without-doubling-the-bill","Running the backend matrix without doubling the bill",[10,1272,1273],{},"Two backends means up to twice the runtime, and the way to avoid paying it on every push is to treat the second backend as a distinct job rather than as part of the default run.",[296,1275,1277],{"className":342,"code":1276,"language":344,"meta":301,"style":301},"# conftest.py — one backend by default, both when explicitly asked\nimport os\n\nimport pytest\n\n_BACKENDS = [\"asyncio\", \"trio\"] if os.environ.get(\"TEST_ALL_BACKENDS\") else [\"asyncio\"]\n\n\n@pytest.fixture(params=_BACKENDS)\ndef anyio_backend(request):\n    return request.param\n",[14,1278,1279,1284,1289,1293,1297,1301,1306,1310,1314,1319,1323],{"__ignoreMap":301},[305,1280,1281],{"class":176,"line":307},[305,1282,1283],{},"# conftest.py — one backend by default, both when explicitly asked\n",[305,1285,1286],{"class":176,"line":313},[305,1287,1288],{},"import os\n",[305,1290,1291],{"class":176,"line":319},[305,1292,357],{"emptyLinePlaceholder":356},[305,1294,1295],{"class":176,"line":365},[305,1296,351],{},[305,1298,1299],{"class":176,"line":397},[305,1300,357],{"emptyLinePlaceholder":356},[305,1302,1303],{"class":176,"line":403},[305,1304,1305],{},"_BACKENDS = [\"asyncio\", \"trio\"] if os.environ.get(\"TEST_ALL_BACKENDS\") else [\"asyncio\"]\n",[305,1307,1308],{"class":176,"line":409},[305,1309,357],{"emptyLinePlaceholder":356},[305,1311,1312],{"class":176,"line":415},[305,1313,357],{"emptyLinePlaceholder":356},[305,1315,1316],{"class":176,"line":481},[305,1317,1318],{},"@pytest.fixture(params=_BACKENDS)\n",[305,1320,1321],{"class":176,"line":487},[305,1322,406],{},[305,1324,1325],{"class":176,"line":493},[305,1326,418],{},[296,1328,1330],{"className":648,"code":1329,"language":650,"meta":301,"style":301},"pytest -q                          # pull requests: asyncio only, full speed\nTEST_ALL_BACKENDS=1 pytest -q      # merge queue and nightly: both runtimes\n",[14,1331,1332,1343],{"__ignoreMap":301},[305,1333,1334,1336,1339],{"class":176,"line":307},[305,1335,658],{"class":657},[305,1337,1338],{"class":665}," -q",[305,1340,1342],{"class":1341},"sJ8bj","                          # pull requests: asyncio only, full speed\n",[305,1344,1345,1349,1353,1356,1359,1361],{"class":176,"line":313},[305,1346,1348],{"class":1347},"sVt8B","TEST_ALL_BACKENDS",[305,1350,1352],{"class":1351},"szBVR","=",[305,1354,1355],{"class":661},"1",[305,1357,1358],{"class":657}," pytest",[305,1360,1338],{"class":665},[305,1362,1363],{"class":1341},"      # merge queue and nightly: both runtimes\n",[100,1365,1367,1424],{"className":1366},[103],[105,1368,113,1373,113,1376,113,1379,113,1382,113,1386,113,1390,113,1394,113,1398,113,1402,113,1406,113,1409,113,1413,113,1417,113,1420],{"viewBox":1369,"role":108,"ariaLabelledBy":1370,"xmlns":112},"0 0 800 226",[1371,1372],"bm-t","bm-d",[115,1374,1375],{"id":1371},"Backend matrix by pipeline stage",[119,1377,1378],{"id":1372},"Pull-request runs use only the asyncio backend at full speed. The merge queue and nightly jobs set TEST_ALL_BACKENDS, which parametrizes every test over asyncio and trio. A failure then carries its backend in the test id, such as test_stream with the trio parameter.",[141,1380],{"x":143,"y":143,"width":962,"height":1381,"rx":146,"fill":147},"226",[149,1383,1385],{"x":966,"y":152,"textAnchor":153,"fontSize":1384,"fontWeight":155,"fill":139},"15.5","One conftest, two levels of coverage",[141,1387],{"x":1388,"y":971,"width":1389,"height":174,"rx":216,"fill":210,"stroke":211,"strokeWidth":188},"26","360",[149,1391,1393],{"x":1392,"y":215,"textAnchor":153,"fontSize":170,"fontWeight":155,"fill":139},"206","pull requests",[149,1395,1397],{"x":1018,"y":1396,"fontSize":163,"fill":139},"108","anyio_backend params: [asyncio]",[149,1399,1401],{"x":1018,"y":1400,"fontSize":163,"fill":139},"130","one run per test",[149,1403,1405],{"x":1018,"y":1404,"fontSize":163,"fontWeight":155,"fill":225},"176","full speed on every push",[141,1407],{"x":1408,"y":971,"width":1389,"height":174,"rx":216,"fill":186,"stroke":187,"strokeWidth":188},"414",[149,1410,1412],{"x":1411,"y":215,"textAnchor":153,"fontSize":170,"fontWeight":155,"fill":139},"594","merge queue · nightly",[149,1414,1416],{"x":1415,"y":1396,"fontSize":163,"fill":139},"432","TEST_ALL_BACKENDS=1",[149,1418,1419],{"x":1415,"y":1400,"fontSize":163,"fill":139},"params: [asyncio, trio]",[149,1421,1423],{"x":1415,"y":1404,"fontSize":163,"fontWeight":155,"fill":1422},"#8a5a00","failure id: test_stream[trio]",[256,1425,1426],{},"The matrix is decided at collection time from the environment, so the same tests serve both stages.",[10,1428,1429,1430,1432],{},"Deriving the parameter list from the environment keeps one ",[14,1431,686],{}," and one set of tests, with the matrix as a runtime decision. Reading an environment variable at import time is normally a smell, but parametrization is decided at collection, so there is no later point at which the choice could be made.",[10,1434,1435],{},"The alternative — marking individual tests for the second backend — is worth resisting. It requires a judgement per test about whether the runtime could matter, that judgement is made when the test is written and never revisited, and the tests that turn out to be backend-sensitive are precisely the ones nobody predicted. Running everything on both backends nightly costs one job and needs no judgement at all.",[10,1437,1438,1439,1441,1442,1444,1445,1448],{},"When a failure does appear on one backend only, the test identifier already names it: ",[14,1440,233],{}," failing while ",[14,1443,217],{}," passes is a complete bug report for anyone who knows the two runtimes. Reproducing it locally is ",[14,1446,1447],{},"pytest -k \"trio and test_stream\"",", and the diagnosis is almost always one of three things — a scheduling assumption (Trio's scheduler is deliberately randomised, asyncio's is FIFO), a cancellation assumption, or an asyncio API that leaked into portable code. Trio's randomised scheduling is a feature here: it finds ordering assumptions that asyncio's deterministic queue would hide for years.",[23,1450,1452],{"id":1451},"async-fixtures-under-anyio","Async fixtures under AnyIO",[10,1454,1455,1456,1458],{},"AnyIO runs async fixtures the same way it runs tests: through the backend the requesting test resolved to. That gives one guarantee ",[14,1457,71],{}," needs configuration to achieve — a fixture is always on the same runtime as the test using it, because there is no separate loop lifetime to get out of step.",[296,1460,1462],{"className":342,"code":1461,"language":344,"meta":301,"style":301},"import anyio\nimport pytest\n\n\n@pytest.fixture\nasync def broker():\n    # Runs on whichever backend the requesting test resolved to.\n    send, receive = anyio.create_memory_object_stream[dict](max_buffer_size=8)\n    async with send, receive:\n        yield send, receive\n    # Both halves closed here, on the same runtime, before the test item ends.\n",[14,1463,1464,1468,1472,1476,1480,1484,1489,1494,1499,1504,1509],{"__ignoreMap":301},[305,1465,1466],{"class":176,"line":307},[305,1467,446],{},[305,1469,1470],{"class":176,"line":313},[305,1471,351],{},[305,1473,1474],{"class":176,"line":319},[305,1475,357],{"emptyLinePlaceholder":356},[305,1477,1478],{"class":176,"line":365},[305,1479,357],{"emptyLinePlaceholder":356},[305,1481,1482],{"class":176,"line":397},[305,1483,581],{},[305,1485,1486],{"class":176,"line":403},[305,1487,1488],{},"async def broker():\n",[305,1490,1491],{"class":176,"line":409},[305,1492,1493],{},"    # Runs on whichever backend the requesting test resolved to.\n",[305,1495,1496],{"class":176,"line":415},[305,1497,1498],{},"    send, receive = anyio.create_memory_object_stream[dict](max_buffer_size=8)\n",[305,1500,1501],{"class":176,"line":481},[305,1502,1503],{},"    async with send, receive:\n",[305,1505,1506],{"class":176,"line":487},[305,1507,1508],{},"        yield send, receive\n",[305,1510,1511],{"class":176,"line":493},[305,1512,1513],{},"    # Both halves closed here, on the same runtime, before the test item ends.\n",[10,1515,1516,1517,1519],{},"The constraint that replaces loop scoping is sharper: a session-scoped async fixture is not possible in the general case, because each test item gets a fresh runtime. AnyIO documents this directly — async fixtures are effectively function-scoped unless the backend fixture is widened to match, and widening ",[14,1518,97],{}," to session scope means every test in the session shares one runtime and one set of parameters.",[10,1521,1522,1523,1527],{},"In practice that pushes expensive setup into synchronous fixtures wherever it can live there. A Testcontainers instance, a temporary directory, a loaded configuration file: none of these need to await anything, so making them synchronous and session-scoped sidesteps the whole question. Only the thin async wrapper around them — the connection, the client, the stream — needs to be per-test, and per-test is cheap once the expensive part is already running. That split is the same one recommended for ",[74,1524,1526],{"href":1525},"\u002Fintegration-database-and-service-testing\u002Fdatabase-fixtures-and-transactional-tests\u002F","database fixtures",": a durable resource created synchronously, a disposable handle acquired per test.",[23,1529,1531],{"id":1530},"frequently-asked-questions","Frequently Asked Questions",[10,1533,1534,1538,1539,1541,1542,1544,1545,1547],{},[1535,1536,1537],"strong",{},"Do I need Trio installed to use AnyIO in tests?","\nNo. AnyIO runs on asyncio by default and only imports Trio when a test is parametrised onto the ",[14,1540,39],{}," backend. Install ",[14,1543,39],{}," as a test-only dependency when you want the second backend in the matrix; without it, the ",[14,1546,97],{}," fixture simply yields asyncio and the suite runs as before.",[10,1549,1550,1553,1554,1556,1557,1560,1561,1563,1564,78],{},[1535,1551,1552],{},"Why does my AnyIO test say 'fixture anyio_backend not found'?","\nThe AnyIO plugin supplies that fixture only when it is active. Either the plugin is not installed, or ",[14,1555,71],{}," claimed the test first in auto mode. Check with ",[14,1558,1559],{},"pytest --fixtures | grep anyio",", and isolate backend-parametrised tests in a directory whose ",[14,1562,686],{}," sets ",[14,1565,740],{},[10,1567,1568,1571,1572,1575,1576,1579],{},[1535,1569,1570],{},"Can I call asyncio APIs inside an AnyIO test?","\nOnly in tests pinned to the asyncio backend. Calling ",[14,1573,1574],{},"asyncio.get_running_loop()"," or ",[14,1577,1578],{},"loop.call_soon"," in a test that also runs on Trio raises at runtime, because no asyncio loop exists there. Move backend-specific assertions into their own test marked for that backend alone.",[10,1581,1582,1585,1588,1589,1591,1592,1595,1596,1598],{},[1535,1583,1584],{},"What replaces asyncio.wait_for in AnyIO?",[14,1586,1587],{},"anyio.fail_after(seconds)"," for a deadline that raises ",[14,1590,804],{},", and ",[14,1593,1594],{},"anyio.move_on_after(seconds)"," for one that returns quietly with ",[14,1597,932],{}," set. Both are cancel scopes, so they apply to everything inside the block rather than to a single awaitable.",[10,1600,1601,1604],{},[1535,1602,1603],{},"Is AnyIO slower than running on asyncio directly?","\nThe abstraction costs a thin layer of indirection per call, which is immaterial next to any real I\u002FO. What costs measurable time is running the whole suite twice, once per backend — parametrise only the tests whose behaviour could differ between runtimes, and pin the rest to one backend.",[23,1606,1608],{"id":1607},"related-guides","Related guides",[28,1610,1611,1618,1625,1630,1637],{},[31,1612,1613,1614,78],{},"Put the backend matrix in place with ",[74,1615,1617],{"href":1616},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Frunning-one-test-on-asyncio-and-trio\u002F","running one test on asyncio and Trio",[31,1619,1620,1621,78],{},"Assert on structured-concurrency semantics in ",[74,1622,1624],{"href":1623},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Ftesting-code-that-uses-task-groups\u002F","testing code that uses task groups",[31,1626,1627,1628,78],{},"Move an existing suite across with ",[74,1629,1074],{"href":1073},[31,1631,1632,1633,78],{},"Compare fixture lifetimes between the two plugins in ",[74,1634,1636],{"href":1635},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fpytest-asyncio-vs-anyio-scoping-trade-offs\u002F","pytest-asyncio vs anyio scoping trade-offs",[31,1638,1639,1640,78],{},"Bound the whole suite regardless of backend with ",[74,1641,1643],{"href":1642},"\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002F","timeouts, cancellation and deadlines",[10,1645,1646,1647],{},"← Back to ",[74,1648,1650],{"href":1649},"\u002Ftesting-async-and-concurrent-python\u002F","Testing Async & Concurrent Python",[1652,1653,1654],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":301,"searchDepth":313,"depth":313,"links":1656},[1657,1658,1659,1666,1667,1668,1669,1670,1671,1672,1673,1674],{"id":25,"depth":313,"text":26},{"id":81,"depth":313,"text":82},{"id":288,"depth":313,"text":289,"children":1660},[1661,1662,1663,1664,1665],{"id":293,"depth":319,"text":294},{"id":338,"depth":319,"text":339},{"id":371,"depth":319,"text":372},{"id":435,"depth":319,"text":436},{"id":558,"depth":319,"text":559},{"id":637,"depth":313,"text":638},{"id":696,"depth":313,"text":697},{"id":839,"depth":313,"text":840},{"id":1031,"depth":313,"text":1032},{"id":1077,"depth":313,"text":1078},{"id":1269,"depth":313,"text":1270},{"id":1451,"depth":313,"text":1452},{"id":1530,"depth":313,"text":1531},{"id":1607,"depth":313,"text":1608},"Run one async test body on asyncio and Trio with AnyIO: backend parametrization, anyio_backend fixtures, task groups and cancel scopes, and what portability costs.","md",{"slug":1678,"type":1679,"breadcrumb":1680,"datePublished":1681,"dateModified":1681,"faq":1682,"howto":1693},"testing-with-anyio-and-trio","topic","AnyIO & Trio","2026-09-18",[1683,1685,1687,1689,1691],{"q":1537,"a":1684},"No. AnyIO runs on asyncio by default and only imports Trio when a test is parametrised onto the trio backend. Install trio as a test-only dependency when you want the second backend in the matrix; without it, the anyio_backend fixture simply yields asyncio and the suite runs as before.",{"q":1552,"a":1686},"The anyio plugin supplies that fixture only when it is active. Either the plugin is not installed, or pytest-asyncio claimed the test first in auto mode. Check with pytest --fixtures | grep anyio, and isolate backend-parametrised tests in a directory whose conftest.py sets asyncio_mode = strict.",{"q":1570,"a":1688},"Only in tests pinned to the asyncio backend. Calling asyncio.get_running_loop() or loop.call_soon in a test that also runs on Trio raises at runtime, because no asyncio loop exists there. Move backend-specific assertions into their own test marked for that backend alone.",{"q":1584,"a":1690},"anyio.fail_after(seconds) for a deadline that raises TimeoutError, and anyio.move_on_after(seconds) for one that returns quietly with cancelled_caught set. Both are cancel scopes, so they apply to everything inside the block rather than to a single awaitable.",{"q":1603,"a":1692},"The abstraction costs a thin layer of indirection per call, which is immaterial next to any real I\u002FO. What costs measurable time is running the whole suite twice, once per backend — parametrise only the tests whose behaviour could differ between runtimes, and pin the rest to one backend.",{"name":1694,"description":1695,"steps":1696},"How to run one async test suite on both asyncio and Trio","Install the AnyIO pytest plugin, parametrise the backend fixture, and keep backend-specific code out of portable tests.",[1697,1700,1703,1706,1709],{"name":1698,"text":1699},"Install the plugin and Trio","Add anyio[trio] to the test dependencies so both backends are importable, and confirm the anyio_backend fixture appears in pytest --fixtures.",{"name":1701,"text":1702},"Mark the tests that should be portable","Apply pytestmark = pytest.mark.anyio at module level so every coroutine test in the module runs through AnyIO.",{"name":1704,"text":1705},"Parametrise the backend fixture","Override anyio_backend in conftest.py with params=['asyncio', 'trio'] so each marked test runs once per backend.",{"name":1707,"text":1708},"Replace backend-specific primitives","Swap asyncio.Event, asyncio.Queue and asyncio.wait_for for anyio.Event, memory object streams and anyio.fail_after.",{"name":1710,"text":1711},"Pin the tests that cannot be portable","Give backend-specific tests their own anyio_backend override returning a single backend, so they do not fail on the runtime they were never written for.","\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio",{"title":5,"description":1675},"testing-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Findex","Qt5_-D-h5Jn1bt37kncSYC05EdeEJpQHrskUPIFvz3U",1789718765722]