[{"data":1,"prerenderedAt":767},["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":728,"extension":729,"meta":730,"navigation":293,"path":763,"seo":764,"stem":765,"__hash__":766},"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":718},"minimark",[9,39,44,95,99,102,235,240,253,376,382,386,395,486,489,571,575,592,596,668,672,687,699,707,714],[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,135,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","\npytest-asyncio (>= 0.23)\n",[126,143,146],{"x":144,"y":138,"textAnchor":130,"fontSize":139,"fontWeight":132,"fill":145},"590","#81b29a","\nanyio (>= 4.0)\n",[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,508],"table",{},[493,494,495],"thead",{},[496,497,498,502,505],"tr",{},[499,500,501],"th",{},"Concern",[499,503,504],{},"pytest-asyncio (>= 0.23)",[499,506,507],{},"anyio (>= 4.0)",[509,510,511,523,538,549,560],"tbody",{},[496,512,513,517,520],{},[514,515,516],"td",{},"Backends",[514,518,519],{},"asyncio only",[514,521,522],{},"asyncio and trio",[496,524,525,528,533],{},[514,526,527],{},"Loop control",[514,529,530,531],{},"Explicit via ",[14,532,64],{},[514,534,535,536],{},"Implicit via ",[14,537,393],{},[496,539,540,543,546],{},[514,541,542],{},"Fixture\u002Floop scope coupling",[514,544,545],{},"You match them manually",[514,547,548],{},"Framework manages it",[496,550,551,554,557],{},[514,552,553],{},"Structured concurrency",[514,555,556],{},"Use asyncio primitives directly",[514,558,559],{},"First-class task groups, cancel scopes",[496,561,562,565,568],{},[514,563,564],{},"Best when",[514,566,567],{},"asyncio-only app, need per-test loop tuning",[514,569,570],{},"Library shipping to both backends, want portability",[40,572,574],{"id":573},"why-this-works","Why this works",[10,576,577,579,580,582,583,585,586,588,589,591],{},[14,578,33],{}," separates loop lifetime (",[14,581,64],{},") from fixture lifetime (",[14,584,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,587,37],{}," instead makes the loop an implementation detail of the ",[14,590,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,593,595],{"id":594},"edge-cases-and-failure-modes","Edge cases and failure modes",[45,597,598,613,627,637,654],{},[48,599,600,606,607,609,610,612],{},[75,601,602,603,605],{},"Pre-0.23 ",[14,604,72],{}," override."," Old guides redefine the ",[14,608,72],{}," fixture to widen scope; this is deprecated and removed in modern pytest-asyncio. Use ",[14,611,64],{}," instead.",[48,614,615,618,619,622,623,626],{},[75,616,617],{},"Mismatched scopes."," A ",[14,620,621],{},"scope=\"session\""," fixture marked ",[14,624,625],{},"loop_scope=\"function\""," recreates the resource per loop and fails on reuse — the two must agree.",[48,628,629,632,633,94],{},[75,630,631],{},"\"Event loop is closed\" on teardown."," A fixture awaiting cleanup after its loop closed; covered in depth under ",[90,634,636],{"href":635},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002F","debugging async code and event loops",[48,638,639,642,643,646,647,649,650,653],{},[75,640,641],{},"anyio trio incompatibility."," Code using asyncio-only APIs (e.g. ",[14,644,645],{},"asyncio.get_event_loop",") breaks when the ",[14,648,393],{}," parametrizes ",[14,651,652],{},"trio","; keep fixtures backend-neutral.",[48,655,656,659,660,663,664,94],{},[75,657,658],{},"Hypothesis async tests."," Combining ",[14,661,662],{},"@given"," with async fixtures adds health-check concerns on top of loop scoping; see ",[90,665,667],{"href":666},"\u002Fproperty-based-fuzz-testing-strategies\u002Fhypothesis-framework-fundamentals\u002Ffixing-hypothesis-flaky-health-check-failures\u002F","fixing Hypothesis FlakyHealthCheck failures",[40,669,671],{"id":670},"frequently-asked-questions","Frequently Asked Questions",[10,673,674,680,682,683,686],{},[75,675,676,677,679],{},"What does ",[14,678,64],{}," do in pytest-asyncio?",[14,681,64],{},", added in pytest-asyncio 0.23, controls the lifespan of the event loop independently of fixture scope. Setting ",[14,684,685],{},"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,688,689,692,693,695,696,698],{},[75,690,691],{},"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,694,64],{}," on both the fixture and the tests, or use anyio, whose ",[14,697,393],{}," fixture governs the loop uniformly.",[10,700,701,704,705,94],{},[75,702,703],{},"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,706,64],{},[10,708,709,710],{},"← Back to ",[90,711,713],{"href":712},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002F","Mastering Pytest Fixtures",[715,716,717],"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":719},[720,721,725,726,727],{"id":42,"depth":272,"text":43},{"id":97,"depth":272,"text":98,"children":722},[723,724],{"id":238,"depth":278,"text":239},{"id":384,"depth":278,"text":385},{"id":573,"depth":272,"text":574},{"id":594,"depth":272,"text":595},{"id":670,"depth":272,"text":671},"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":731,"type":732,"breadcrumb":733,"datePublished":734,"dateModified":734,"faq":735,"howto":744},"pytest-asyncio-vs-anyio-scoping-trade-offs","long_tail","asyncio vs anyio","2026-06-18",[736,739,742],{"q":737,"a":738},"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":740,"a":741},"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":703,"a":743},"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":745,"description":746,"steps":747},"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.",[748,751,754,757,760],{"name":749,"text":750},"Identify the backend requirement","Determine whether the code must run on asyncio only or also on trio; trio support points to anyio.",{"name":752,"text":753},"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":755,"text":756},"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":758,"text":759},"Or configure the anyio backend fixture","If choosing anyio, parametrize the anyio_backend fixture and rely on its uniform loop management.",{"name":761,"text":762},"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":728},"advanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fpytest-asyncio-vs-anyio-scoping-trade-offs\u002Findex","BjR4WBqOFfEr7ob3aMnx8wUCRXR-9x6wgnp7HuXD-EQ",1781793487737]