[{"data":1,"prerenderedAt":765},["ShallowReactive",2],{"page-\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fpytest-asyncio-vs-anyio-scoping-trade-offs\u002F":3},{"id":4,"title":5,"body":6,"description":726,"extension":727,"meta":728,"navigation":293,"path":761,"seo":762,"stem":763,"__hash__":764},"content\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fpytest-asyncio-vs-anyio-scoping-trade-offs\u002Findex.md","pytest-asyncio vs anyio: Scoping Trade-offs",{"type":7,"value":8,"toc":716},"minimark",[9,39,44,95,99,102,235,240,253,376,382,386,395,486,489,569,573,590,594,666,670,685,697,705,712],[10,11,12,13,17,18,21,22,26,27,30,31,34,35,38],"p",{},"You promote an async fixture from function scope to session scope to avoid reconnecting a client for every test, and the suite explodes with ",[14,15,16],"code",{},"RuntimeError: ... attached to a different loop"," or ",[14,19,20],{},"Event loop is closed",". The root cause is a scope mismatch between the ",[23,24,25],"em",{},"fixture's"," lifetime and the ",[23,28,29],{},"event loop's"," lifetime — and the two leading frameworks, ",[14,32,33],{},"pytest-asyncio"," and ",[14,36,37],{},"anyio",", resolve it with fundamentally different models. This is a decision page: it lays out how each scopes the loop, where each breaks, and which to pick.",[40,41,43],"h2",{"id":42},"prerequisites","Prerequisites",[45,46,47,51,56,86],"ul",{},[48,49,50],"li",{},"Python 3.9+",[48,52,53],{},[14,54,55],{},"pytest >= 7.0",[48,57,58,61,62,65,66,69,70,73,74,78,79,82,83],{},[14,59,60],{},"pytest-asyncio >= 0.23"," (the ",[14,63,64],{},"loop_scope"," parameter and the ",[14,67,68],{},"asyncio_mode","\u002Floop-scope split were introduced in 0.23; earlier versions use the removed ",[14,71,72],{},"event_loop"," fixture override pattern) ",[75,76,77],"strong",{},"or"," ",[14,80,81],{},"anyio >= 4.0"," with ",[14,84,85],{},"pytest >= 7",[48,87,88,89,94],{},"Background on async fixture lifecycles from ",[90,91,93],"a",{"href":92},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fhow-to-scope-pytest-fixtures-for-async-tests\u002F","How to Scope Pytest Fixtures for Async Tests",".",[40,96,98],{"id":97},"solution","Solution",[10,100,101],{},"The decision hinges on two axes: how many concurrency backends you must support, and how loop lifetime maps onto fixture scope.",[103,104,107,231],"figure",{"className":105},[106],"diagram",[108,109,116,117,116,121,116,125,116,135,116,142,116,147,116,157,116,162,116,167,116,171,116,175,116,179,116,182,116,186,116,190,116,193,116,196,116,199,116,201,116,204,116,207,116,209,116,212,116,215,116,223,116,227],"svg",{"viewBox":110,"role":111,"ariaLabelledBy":112,"xmlns":115},"0 0 800 400","img",[113,114],"asyncscope-t","asyncscope-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[118,119,120],"title",{"id":113},"Loop-scope ladder for async fixtures",[122,123,124],"desc",{"id":114},"Comparison of how pytest-asyncio loop_scope and anyio map event-loop lifetime onto session, module, and function fixture scopes.",[126,127,134],"text",{"x":128,"y":129,"textAnchor":130,"fontSize":131,"fontWeight":132,"fill":133},"400","32","middle","18","700","#3d405b","Event-loop lifetime vs fixture scope",[126,136,141],{"x":137,"y":138,"textAnchor":130,"fontSize":139,"fontWeight":132,"fill":140},"210","66","14","#e07a5f","pytest-asyncio (>= 0.23)",[126,143,146],{"x":144,"y":138,"textAnchor":130,"fontSize":139,"fontWeight":132,"fill":145},"590","#81b29a","anyio (>= 4.0)",[148,149],"rect",{"x":150,"y":151,"width":152,"height":153,"rx":154,"fill":155,"stroke":140,"strokeWidth":156},"60","86","300","56","10","#fffdf8","2",[126,158,161],{"x":137,"y":159,"textAnchor":130,"fontSize":160,"fontWeight":132,"fill":133},"110","13","loop_scope='session'",[126,163,166],{"x":137,"y":164,"textAnchor":130,"fontSize":165,"fill":133},"130","11.5","one loop for whole session",[148,168],{"x":150,"y":169,"width":152,"height":153,"rx":154,"fill":155,"stroke":140,"strokeWidth":170},"156","1.6",[126,172,174],{"x":137,"y":173,"textAnchor":130,"fontSize":160,"fontWeight":132,"fill":133},"180","loop_scope='module'",[126,176,178],{"x":137,"y":177,"textAnchor":130,"fontSize":165,"fill":133},"200","loop per module",[148,180],{"x":150,"y":181,"width":152,"height":153,"rx":154,"fill":155,"stroke":140,"strokeWidth":170},"226",[126,183,185],{"x":137,"y":184,"textAnchor":130,"fontSize":160,"fontWeight":132,"fill":133},"250","loop_scope='function'",[126,187,189],{"x":137,"y":188,"textAnchor":130,"fontSize":165,"fill":133},"270","fresh loop per test (default)",[148,191],{"x":192,"y":151,"width":152,"height":153,"rx":154,"fill":155,"stroke":145,"strokeWidth":156},"440",[126,194,195],{"x":144,"y":159,"textAnchor":130,"fontSize":160,"fontWeight":132,"fill":133},"anyio_backend fixture",[126,197,198],{"x":144,"y":164,"textAnchor":130,"fontSize":165,"fill":133},"governs loop uniformly",[148,200],{"x":192,"y":169,"width":152,"height":153,"rx":154,"fill":155,"stroke":145,"strokeWidth":170},[126,202,203],{"x":144,"y":173,"textAnchor":130,"fontSize":160,"fontWeight":132,"fill":133},"backend-agnostic",[126,205,206],{"x":144,"y":177,"textAnchor":130,"fontSize":165,"fill":133},"asyncio or trio",[148,208],{"x":192,"y":181,"width":152,"height":153,"rx":154,"fill":155,"stroke":145,"strokeWidth":170},[126,210,211],{"x":144,"y":184,"textAnchor":130,"fontSize":160,"fontWeight":132,"fill":133},"structured concurrency",[126,213,214],{"x":144,"y":188,"textAnchor":130,"fontSize":165,"fill":133},"task groups, cancel scopes",[148,216],{"x":217,"y":218,"width":219,"height":150,"rx":220,"fill":221,"stroke":133,"strokeWidth":222},"120","312","560","12","#f4f1de","1.5",[126,224,226],{"x":128,"y":225,"textAnchor":130,"fontSize":160,"fontWeight":132,"fill":133},"338","Rule: scope must not outlive its loop",[126,228,230],{"x":128,"y":229,"textAnchor":130,"fontSize":165,"fill":133},"358","match loop_scope, or let anyio manage it",[232,233,234],"figcaption",{},"pytest-asyncio exposes loop lifetime directly via loop_scope; anyio hides it behind the anyio_backend fixture and adds backend portability and structured concurrency.",[236,237,239],"h3",{"id":238},"pytest-asyncio-with-loop_scope","pytest-asyncio with loop_scope",[10,241,242,243,245,246,248,249,252],{},"In ",[14,244,60],{},", the event loop's lifetime is set by ",[14,247,64],{},", separately from a fixture's ",[14,250,251],{},"scope",". To share a session-scoped async resource, both the fixture and the tests must declare the same loop scope so they run on one loop.",[254,255,260],"pre",{"className":256,"code":257,"language":258,"meta":259,"style":259},"language-python shiki shiki-themes github-light github-dark","# conftest.py  (requires pytest-asyncio >= 0.23)\nimport pytest\nimport pytest_asyncio\nimport asyncio\n\n@pytest_asyncio.fixture(loop_scope=\"session\", scope=\"session\")\nasync def shared_client():\n    # Created and awaited on the SESSION loop, so it stays valid all session.\n    await asyncio.sleep(0)          # stand-in for connect()\n    client = {\"connected\": True}\n    yield client\n    client[\"connected\"] = False     # torn down on the same loop\n\n# test_asyncio_scope.py\nimport pytest\n\n@pytest.mark.asyncio(loop_scope=\"session\")\nasync def test_uses_shared_client(shared_client):\n    assert shared_client[\"connected\"] is True\n","python","",[14,261,262,270,276,282,288,295,301,307,313,319,325,331,337,342,348,353,358,364,370],{"__ignoreMap":259},[263,264,267],"span",{"class":265,"line":266},"line",1,[263,268,269],{},"# conftest.py  (requires pytest-asyncio >= 0.23)\n",[263,271,273],{"class":265,"line":272},2,[263,274,275],{},"import pytest\n",[263,277,279],{"class":265,"line":278},3,[263,280,281],{},"import pytest_asyncio\n",[263,283,285],{"class":265,"line":284},4,[263,286,287],{},"import asyncio\n",[263,289,291],{"class":265,"line":290},5,[263,292,294],{"emptyLinePlaceholder":293},true,"\n",[263,296,298],{"class":265,"line":297},6,[263,299,300],{},"@pytest_asyncio.fixture(loop_scope=\"session\", scope=\"session\")\n",[263,302,304],{"class":265,"line":303},7,[263,305,306],{},"async def shared_client():\n",[263,308,310],{"class":265,"line":309},8,[263,311,312],{},"    # Created and awaited on the SESSION loop, so it stays valid all session.\n",[263,314,316],{"class":265,"line":315},9,[263,317,318],{},"    await asyncio.sleep(0)          # stand-in for connect()\n",[263,320,322],{"class":265,"line":321},10,[263,323,324],{},"    client = {\"connected\": True}\n",[263,326,328],{"class":265,"line":327},11,[263,329,330],{},"    yield client\n",[263,332,334],{"class":265,"line":333},12,[263,335,336],{},"    client[\"connected\"] = False     # torn down on the same loop\n",[263,338,340],{"class":265,"line":339},13,[263,341,294],{"emptyLinePlaceholder":293},[263,343,345],{"class":265,"line":344},14,[263,346,347],{},"# test_asyncio_scope.py\n",[263,349,351],{"class":265,"line":350},15,[263,352,275],{},[263,354,356],{"class":265,"line":355},16,[263,357,294],{"emptyLinePlaceholder":293},[263,359,361],{"class":265,"line":360},17,[263,362,363],{},"@pytest.mark.asyncio(loop_scope=\"session\")\n",[263,365,367],{"class":265,"line":366},18,[263,368,369],{},"async def test_uses_shared_client(shared_client):\n",[263,371,373],{"class":265,"line":372},19,[263,374,375],{},"    assert shared_client[\"connected\"] is True\n",[10,377,378,379,381],{},"The crucial point: a session-scoped fixture with a function-scoped loop will fail, because the resource is created on a loop that closes after the first test. The ",[14,380,64],{}," on both sides keeps the loop alive.",[236,383,385],{"id":384},"anyio-with-the-backend-fixture","anyio with the backend fixture",[10,387,388,390,391,394],{},[14,389,37],{}," runs the same test on multiple backends and manages the loop through the ",[14,392,393],{},"anyio_backend"," fixture; you write backend-agnostic code and never touch the loop directly.",[254,396,398],{"className":256,"code":397,"language":258,"meta":259,"style":259},"# test_anyio_scope.py  (requires anyio >= 4.0)\nimport pytest\nimport anyio\n\n@pytest.fixture\ndef anyio_backend():\n    return \"asyncio\"          # or parametrize: [\"asyncio\", \"trio\"]\n\n@pytest.fixture\nasync def shared_client(anyio_backend):\n    # anyio governs the loop; the fixture lives on the backend it provides.\n    await anyio.sleep(0)\n    client = {\"connected\": True}\n    yield client\n    client[\"connected\"] = False\n\n@pytest.mark.anyio\nasync def test_uses_shared_client(shared_client):\n    assert shared_client[\"connected\"] is True\n",[14,399,400,405,409,414,418,423,428,433,437,441,446,451,456,460,464,469,473,478,482],{"__ignoreMap":259},[263,401,402],{"class":265,"line":266},[263,403,404],{},"# test_anyio_scope.py  (requires anyio >= 4.0)\n",[263,406,407],{"class":265,"line":272},[263,408,275],{},[263,410,411],{"class":265,"line":278},[263,412,413],{},"import anyio\n",[263,415,416],{"class":265,"line":284},[263,417,294],{"emptyLinePlaceholder":293},[263,419,420],{"class":265,"line":290},[263,421,422],{},"@pytest.fixture\n",[263,424,425],{"class":265,"line":297},[263,426,427],{},"def anyio_backend():\n",[263,429,430],{"class":265,"line":303},[263,431,432],{},"    return \"asyncio\"          # or parametrize: [\"asyncio\", \"trio\"]\n",[263,434,435],{"class":265,"line":309},[263,436,294],{"emptyLinePlaceholder":293},[263,438,439],{"class":265,"line":315},[263,440,422],{},[263,442,443],{"class":265,"line":321},[263,444,445],{},"async def shared_client(anyio_backend):\n",[263,447,448],{"class":265,"line":327},[263,449,450],{},"    # anyio governs the loop; the fixture lives on the backend it provides.\n",[263,452,453],{"class":265,"line":333},[263,454,455],{},"    await anyio.sleep(0)\n",[263,457,458],{"class":265,"line":339},[263,459,324],{},[263,461,462],{"class":265,"line":344},[263,463,330],{},[263,465,466],{"class":265,"line":350},[263,467,468],{},"    client[\"connected\"] = False\n",[263,470,471],{"class":265,"line":355},[263,472,294],{"emptyLinePlaceholder":293},[263,474,475],{"class":265,"line":360},[263,476,477],{},"@pytest.mark.anyio\n",[263,479,480],{"class":265,"line":366},[263,481,369],{},[263,483,484],{"class":265,"line":372},[263,485,375],{},[10,487,488],{},"Decision matrix:",[490,491,492,506],"table",{},[493,494,495],"thead",{},[496,497,498,502,504],"tr",{},[499,500,501],"th",{},"Concern",[499,503,141],{},[499,505,146],{},[507,508,509,521,536,547,558],"tbody",{},[496,510,511,515,518],{},[512,513,514],"td",{},"Backends",[512,516,517],{},"asyncio only",[512,519,520],{},"asyncio and trio",[496,522,523,526,531],{},[512,524,525],{},"Loop control",[512,527,528,529],{},"Explicit via ",[14,530,64],{},[512,532,533,534],{},"Implicit via ",[14,535,393],{},[496,537,538,541,544],{},[512,539,540],{},"Fixture\u002Floop scope coupling",[512,542,543],{},"You match them manually",[512,545,546],{},"Framework manages it",[496,548,549,552,555],{},[512,550,551],{},"Structured concurrency",[512,553,554],{},"Use asyncio primitives directly",[512,556,557],{},"First-class task groups, cancel scopes",[496,559,560,563,566],{},[512,561,562],{},"Best when",[512,564,565],{},"asyncio-only app, need per-test loop tuning",[512,567,568],{},"Library shipping to both backends, want portability",[40,570,572],{"id":571},"why-this-works","Why this works",[10,574,575,577,578,580,581,583,584,586,587,589],{},[14,576,33],{}," separates loop lifetime (",[14,579,64],{},") from fixture lifetime (",[14,582,251],{},") so you can keep one loop alive exactly as long as the resources awaited on it, which is what eliminates cross-loop errors for session-scoped clients. ",[14,585,37],{}," instead makes the loop an implementation detail of the ",[14,588,393],{}," fixture, trading that fine-grained control for backend portability and structured concurrency. Pick the model whose default matches your dominant constraint: explicit loop scoping for asyncio-only suites, backend abstraction for dual-backend libraries.",[40,591,593],{"id":592},"edge-cases-and-failure-modes","Edge cases and failure modes",[45,595,596,611,625,635,652],{},[48,597,598,604,605,607,608,610],{},[75,599,600,601,603],{},"Pre-0.23 ",[14,602,72],{}," override."," Old guides redefine the ",[14,606,72],{}," fixture to widen scope; this is deprecated and removed in modern pytest-asyncio. Use ",[14,609,64],{}," instead.",[48,612,613,616,617,620,621,624],{},[75,614,615],{},"Mismatched scopes."," A ",[14,618,619],{},"scope=\"session\""," fixture marked ",[14,622,623],{},"loop_scope=\"function\""," recreates the resource per loop and fails on reuse — the two must agree.",[48,626,627,630,631,94],{},[75,628,629],{},"\"Event loop is closed\" on teardown."," A fixture awaiting cleanup after its loop closed; covered in depth under ",[90,632,634],{"href":633},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002F","debugging async code and event loops",[48,636,637,640,641,644,645,647,648,651],{},[75,638,639],{},"anyio trio incompatibility."," Code using asyncio-only APIs (e.g. ",[14,642,643],{},"asyncio.get_event_loop",") breaks when the ",[14,646,393],{}," parametrizes ",[14,649,650],{},"trio","; keep fixtures backend-neutral.",[48,653,654,657,658,661,662,94],{},[75,655,656],{},"Hypothesis async tests."," Combining ",[14,659,660],{},"@given"," with async fixtures adds health-check concerns on top of loop scoping; see ",[90,663,665],{"href":664},"\u002Fproperty-based-fuzz-testing-strategies\u002Fhypothesis-framework-fundamentals\u002Ffixing-hypothesis-flaky-health-check-failures\u002F","fixing Hypothesis FlakyHealthCheck failures",[40,667,669],{"id":668},"frequently-asked-questions","Frequently Asked Questions",[10,671,672,678,680,681,684],{},[75,673,674,675,677],{},"What does ",[14,676,64],{}," do in pytest-asyncio?",[14,679,64],{},", added in pytest-asyncio 0.23, controls the lifespan of the event loop independently of fixture scope. Setting ",[14,682,683],{},"loop_scope=\"session\""," on the asyncio mark and matching async fixtures keeps one loop alive across the session, so a session-scoped async resource is created and awaited on the same loop.",[10,686,687,690,691,693,694,696],{},[75,688,689],{},"Why do I get \"attached to a different loop\" errors with session-scoped async fixtures?","\nBefore pytest-asyncio 0.23 each test got a fresh event loop, so a session-scoped async fixture created on one loop was awaited on another. Set a matching ",[14,692,64],{}," on both the fixture and the tests, or use anyio, whose ",[14,695,393],{}," fixture governs the loop uniformly.",[10,698,699,702,703,94],{},[75,700,701],{},"When should I choose anyio over pytest-asyncio?","\nChoose anyio when your library must support both asyncio and trio, or when you want structured concurrency and a single backend-agnostic fixture model. Choose pytest-asyncio when you are asyncio-only and want fine-grained per-test loop scoping via ",[14,704,64],{},[10,706,707,708],{},"← Back to ",[90,709,711],{"href":710},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002F","Mastering Pytest Fixtures",[713,714,715],"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);}",{"title":259,"searchDepth":272,"depth":272,"links":717},[718,719,723,724,725],{"id":42,"depth":272,"text":43},{"id":97,"depth":272,"text":98,"children":720},[721,722],{"id":238,"depth":278,"text":239},{"id":384,"depth":278,"text":385},{"id":571,"depth":272,"text":572},{"id":592,"depth":272,"text":593},{"id":668,"depth":272,"text":669},"Decide between pytest-asyncio loop_scope and anyio's backend-agnostic model for async fixture and event-loop scoping, with a comparison matrix and decision rules.","md",{"slug":729,"type":730,"breadcrumb":731,"datePublished":732,"dateModified":732,"faq":733,"howto":742},"pytest-asyncio-vs-anyio-scoping-trade-offs","long_tail","asyncio vs anyio","2026-06-18",[734,737,740],{"q":735,"a":736},"What does loop_scope do in pytest-asyncio?","loop_scope, added in pytest-asyncio 0.23, controls the lifespan of the event loop independently of fixture scope. Setting loop_scope='session' on the asyncio mark and matching async fixtures keeps one loop alive across the session so a session-scoped async resource is created and awaited on the same loop.",{"q":738,"a":739},"Why do I get 'attached to a different loop' errors with session-scoped async fixtures?","Before pytest-asyncio 0.23 each test got a fresh event loop, so a session-scoped async fixture created on one loop was awaited on another. Set a matching loop_scope on both the fixture and the tests, or use anyio whose anyio_backend fixture governs the loop uniformly.",{"q":701,"a":741},"Choose anyio when your library must support both asyncio and trio, or when you want structured concurrency and a single backend-agnostic fixture model. Choose pytest-asyncio when you are asyncio-only and want fine-grained per-test loop scoping via loop_scope.",{"name":743,"description":744,"steps":745},"How to choose async fixture scoping between pytest-asyncio and anyio","Pick the async test framework and loop-scoping model that matches your concurrency backend and fixture lifetimes.",[746,749,752,755,758],{"name":747,"text":748},"Identify the backend requirement","Determine whether the code must run on asyncio only or also on trio; trio support points to anyio.",{"name":750,"text":751},"Map fixture lifetimes to loop lifetime","List which async fixtures are session, module, or function scoped and confirm the event loop must outlive each.",{"name":753,"text":754},"Pin pytest-asyncio and set loop_scope","If choosing pytest-asyncio, require >=0.23 and set matching loop_scope on the asyncio mark and async fixtures.",{"name":756,"text":757},"Or configure the anyio backend fixture","If choosing anyio, parametrize the anyio_backend fixture and rely on its uniform loop management.",{"name":759,"text":760},"Verify with --setup-show","Run pytest --setup-show to confirm fixtures set up and tear down on the expected loop without cross-loop errors.","\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fpytest-asyncio-vs-anyio-scoping-trade-offs",{"title":5,"description":726},"advanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fpytest-asyncio-vs-anyio-scoping-trade-offs\u002Findex","70fgCkRGTQY34IQP8IQGH2177mvXJ1blyK6uG1k2grI",1782236147266]