[{"data":1,"prerenderedAt":1122},["ShallowReactive",2],{"page-\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fsharing-an-event-loop-across-a-test-module\u002F":3},{"id":4,"title":5,"body":6,"description":1085,"extension":1086,"meta":1087,"navigation":85,"path":1118,"seo":1119,"stem":1120,"__hash__":1121},"content\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fsharing-an-event-loop-across-a-test-module\u002Findex.md","Sharing an Event Loop Across a Test Module",{"type":7,"value":8,"toc":1074},"minimark",[9,13,18,56,60,63,330,487,491,504,517,521,570,574,577,647,655,658,717,721,724,727,730,785,807,819,823,826,910,982,985,994,998,1004,1016,1032,1036,1065,1070],[10,11,12],"p",{},"Creating a Postgres pool, a Redis client and an HTTP server for every test in a module costs a second each and, at function loop scope, is unavoidable — the loop dies with the test, so anything holding sockets on it must die too. Widening the loop to the module changes the arithmetic: the pool is built once, each test takes a connection from it, and the module's runtime drops from a minute to a few seconds.",[14,15,17],"h2",{"id":16},"prerequisites","Prerequisites",[19,20,21,41,47],"ul",{},[22,23,24,28,29,32,33,36,37,40],"li",{},[25,26,27],"code",{},"pytest-asyncio >= 0.24",", which carries ",[25,30,31],{},"loop_scope"," on both ",[25,34,35],{},"pytest.mark.asyncio"," and ",[25,38,39],{},"pytest_asyncio.fixture",".",[22,42,43,46],{},[25,44,45],{},"pytest >= 8.0",", Python 3.10+.",[22,48,49,50,55],{},"The scope rules from ",[51,52,54],"a",{"href":53},"\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002F","pytest-asyncio in depth",", particularly that a test may use fixtures on its own loop or a wider one, never a narrower one.",[14,57,59],{"id":58},"solution","Solution",[10,61,62],{},"Declare the loop scope once at module level, and give the expensive fixture the same scope on both axes.",[64,65,70],"pre",{"className":66,"code":67,"language":68,"meta":69,"style":69},"language-python shiki shiki-themes github-light github-dark","import asyncio\n\nimport asyncpg\nimport pytest\nimport pytest_asyncio\n\n# Every test in this module runs on one loop that lives for the module.\npytestmark = pytest.mark.asyncio(loop_scope=\"module\")\n\n\n@pytest_asyncio.fixture(scope=\"module\", loop_scope=\"module\")\nasync def pool():\n    # Built once. Because loop_scope matches scope, the pool's sockets are\n    # registered with a loop that is still alive when `close()` runs.\n    pool = await asyncpg.create_pool(\n        dsn=\"postgresql:\u002F\u002Ftest@localhost\u002Ftest\", min_size=2, max_size=10\n    )\n    try:\n        yield pool\n    finally:\n        await pool.close()\n\n\n@pytest_asyncio.fixture(loop_scope=\"module\")\nasync def conn(pool):\n    # Function-scoped by default: a fresh connection and transaction per test,\n    # on the module's loop, so nothing crosses a loop boundary.\n    async with pool.acquire() as connection:\n        transaction = connection.transaction()\n        await transaction.start()\n        try:\n            yield connection\n        finally:\n            await transaction.rollback()\n\n\nasync def test_insert_is_visible_within_the_transaction(conn):\n    await conn.execute(\"INSERT INTO widget (name) VALUES ($1)\", \"a\")\n    assert await conn.fetchval(\"SELECT count(*) FROM widget\") == 1\n\n\nasync def test_previous_insert_was_rolled_back(conn):\n    # Isolation comes from the transaction, not from a new loop.\n    assert await conn.fetchval(\"SELECT count(*) FROM widget\") == 0\n","python","",[25,71,72,80,87,93,99,105,110,116,122,127,132,138,144,150,156,162,168,174,180,186,192,198,203,208,214,220,226,232,238,244,250,256,262,268,274,279,284,290,296,302,307,312,318,324],{"__ignoreMap":69},[73,74,77],"span",{"class":75,"line":76},"line",1,[73,78,79],{},"import asyncio\n",[73,81,83],{"class":75,"line":82},2,[73,84,86],{"emptyLinePlaceholder":85},true,"\n",[73,88,90],{"class":75,"line":89},3,[73,91,92],{},"import asyncpg\n",[73,94,96],{"class":75,"line":95},4,[73,97,98],{},"import pytest\n",[73,100,102],{"class":75,"line":101},5,[73,103,104],{},"import pytest_asyncio\n",[73,106,108],{"class":75,"line":107},6,[73,109,86],{"emptyLinePlaceholder":85},[73,111,113],{"class":75,"line":112},7,[73,114,115],{},"# Every test in this module runs on one loop that lives for the module.\n",[73,117,119],{"class":75,"line":118},8,[73,120,121],{},"pytestmark = pytest.mark.asyncio(loop_scope=\"module\")\n",[73,123,125],{"class":75,"line":124},9,[73,126,86],{"emptyLinePlaceholder":85},[73,128,130],{"class":75,"line":129},10,[73,131,86],{"emptyLinePlaceholder":85},[73,133,135],{"class":75,"line":134},11,[73,136,137],{},"@pytest_asyncio.fixture(scope=\"module\", loop_scope=\"module\")\n",[73,139,141],{"class":75,"line":140},12,[73,142,143],{},"async def pool():\n",[73,145,147],{"class":75,"line":146},13,[73,148,149],{},"    # Built once. Because loop_scope matches scope, the pool's sockets are\n",[73,151,153],{"class":75,"line":152},14,[73,154,155],{},"    # registered with a loop that is still alive when `close()` runs.\n",[73,157,159],{"class":75,"line":158},15,[73,160,161],{},"    pool = await asyncpg.create_pool(\n",[73,163,165],{"class":75,"line":164},16,[73,166,167],{},"        dsn=\"postgresql:\u002F\u002Ftest@localhost\u002Ftest\", min_size=2, max_size=10\n",[73,169,171],{"class":75,"line":170},17,[73,172,173],{},"    )\n",[73,175,177],{"class":75,"line":176},18,[73,178,179],{},"    try:\n",[73,181,183],{"class":75,"line":182},19,[73,184,185],{},"        yield pool\n",[73,187,189],{"class":75,"line":188},20,[73,190,191],{},"    finally:\n",[73,193,195],{"class":75,"line":194},21,[73,196,197],{},"        await pool.close()\n",[73,199,201],{"class":75,"line":200},22,[73,202,86],{"emptyLinePlaceholder":85},[73,204,206],{"class":75,"line":205},23,[73,207,86],{"emptyLinePlaceholder":85},[73,209,211],{"class":75,"line":210},24,[73,212,213],{},"@pytest_asyncio.fixture(loop_scope=\"module\")\n",[73,215,217],{"class":75,"line":216},25,[73,218,219],{},"async def conn(pool):\n",[73,221,223],{"class":75,"line":222},26,[73,224,225],{},"    # Function-scoped by default: a fresh connection and transaction per test,\n",[73,227,229],{"class":75,"line":228},27,[73,230,231],{},"    # on the module's loop, so nothing crosses a loop boundary.\n",[73,233,235],{"class":75,"line":234},28,[73,236,237],{},"    async with pool.acquire() as connection:\n",[73,239,241],{"class":75,"line":240},29,[73,242,243],{},"        transaction = connection.transaction()\n",[73,245,247],{"class":75,"line":246},30,[73,248,249],{},"        await transaction.start()\n",[73,251,253],{"class":75,"line":252},31,[73,254,255],{},"        try:\n",[73,257,259],{"class":75,"line":258},32,[73,260,261],{},"            yield connection\n",[73,263,265],{"class":75,"line":264},33,[73,266,267],{},"        finally:\n",[73,269,271],{"class":75,"line":270},34,[73,272,273],{},"            await transaction.rollback()\n",[73,275,277],{"class":75,"line":276},35,[73,278,86],{"emptyLinePlaceholder":85},[73,280,282],{"class":75,"line":281},36,[73,283,86],{"emptyLinePlaceholder":85},[73,285,287],{"class":75,"line":286},37,[73,288,289],{},"async def test_insert_is_visible_within_the_transaction(conn):\n",[73,291,293],{"class":75,"line":292},38,[73,294,295],{},"    await conn.execute(\"INSERT INTO widget (name) VALUES ($1)\", \"a\")\n",[73,297,299],{"class":75,"line":298},39,[73,300,301],{},"    assert await conn.fetchval(\"SELECT count(*) FROM widget\") == 1\n",[73,303,305],{"class":75,"line":304},40,[73,306,86],{"emptyLinePlaceholder":85},[73,308,310],{"class":75,"line":309},41,[73,311,86],{"emptyLinePlaceholder":85},[73,313,315],{"class":75,"line":314},42,[73,316,317],{},"async def test_previous_insert_was_rolled_back(conn):\n",[73,319,321],{"class":75,"line":320},43,[73,322,323],{},"    # Isolation comes from the transaction, not from a new loop.\n",[73,325,327],{"class":75,"line":326},44,[73,328,329],{},"    assert await conn.fetchval(\"SELECT count(*) FROM widget\") == 0\n",[331,332,335,483],"figure",{"className":333},[334],"diagram",[336,337,344,345,344,349,344,353,344,371,344,379,344,388,344,397,344,402,344,410,344,416,344,422,344,428,344,434,344,438,344,442,344,446,344,449,344,453,344,455,344,459,344,463,344,467,344,470,344,475,344,479],"svg",{"viewBox":338,"role":339,"ariaLabelledBy":340,"xmlns":343},"0 0 820 262","img",[341,342],"share-t","share-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[346,347,348],"title",{"id":341},"One module loop carrying a pool across three tests",[350,351,352],"desc",{"id":342},"A module-scoped loop is created once and a connection pool is built on it. Three tests then run in sequence, each acquiring a connection and opening a transaction that is rolled back at the end of the test. The loop and the pool are closed once after the last test.",[354,355,356,357,344],"defs",{},"\n    ",[358,359,366],"marker",{"id":360,"viewBox":361,"refX":362,"refY":363,"markerWidth":364,"markerHeight":364,"orient":365},"share-a","0 0 10 10","9","5","7","auto-start-reverse",[367,368],"path",{"d":369,"fill":370},"M0 0 L10 5 L0 10 z","#3d405b",[372,373],"rect",{"x":374,"y":374,"width":375,"height":376,"rx":377,"fill":378},"0","820","262","14","#fffdf8",[380,381,387],"text",{"x":382,"y":383,"textAnchor":384,"fontSize":385,"fontWeight":386,"fill":370},"410","28","middle","16","700","Built once, isolated per test",[372,389],{"x":390,"y":391,"width":392,"height":393,"rx":394,"fill":395,"stroke":370,"strokeWidth":396},"26","50","768","122","12","#f4f1de","1.8",[380,398,401],{"x":399,"y":400,"fontSize":394,"fontWeight":386,"fill":370},"46","74","module loop — created once, closed after the last test",[372,403],{"x":399,"y":404,"width":405,"height":406,"rx":407,"fill":408,"stroke":409,"strokeWidth":396},"86","150","70","10","#f7f0da","#f2cc8f",[380,411,415],{"x":412,"y":413,"textAnchor":384,"fontSize":414,"fontWeight":386,"fill":370},"121","112","11.5","pool built",[380,417,421],{"x":412,"y":418,"textAnchor":384,"fontSize":419,"fill":420},"132","11","#8a5a00","one handshake",[75,423],{"x1":424,"y1":412,"x2":425,"y2":412,"stroke":370,"strokeWidth":426,"markerEnd":427},"200","222","1.5","url(#share-a)",[372,429],{"x":430,"y":404,"width":431,"height":406,"rx":407,"fill":432,"stroke":433,"strokeWidth":396},"228","168","#e6f0ea","#81b29a",[380,435,437],{"x":436,"y":413,"textAnchor":384,"fontSize":414,"fontWeight":386,"fill":370},"312","test 1",[380,439,441],{"x":436,"y":418,"textAnchor":384,"fontSize":419,"fill":440},"#2a5f49","acquire · begin · rollback",[75,443],{"x1":444,"y1":412,"x2":445,"y2":412,"stroke":370,"strokeWidth":426,"markerEnd":427},"400","422",[372,447],{"x":448,"y":404,"width":431,"height":406,"rx":407,"fill":432,"stroke":433,"strokeWidth":396},"428",[380,450,452],{"x":451,"y":413,"textAnchor":384,"fontSize":414,"fontWeight":386,"fill":370},"512","test 2",[380,454,441],{"x":451,"y":418,"textAnchor":384,"fontSize":419,"fill":440},[75,456],{"x1":457,"y1":412,"x2":458,"y2":412,"stroke":370,"strokeWidth":426,"markerEnd":427},"600","622",[372,460],{"x":461,"y":404,"width":462,"height":406,"rx":407,"fill":432,"stroke":433,"strokeWidth":396},"628","146",[380,464,466],{"x":465,"y":413,"textAnchor":384,"fontSize":414,"fontWeight":386,"fill":370},"701","test 3",[380,468,469],{"x":465,"y":418,"textAnchor":384,"fontSize":419,"fill":440},"same pool",[372,471],{"x":390,"y":472,"width":392,"height":473,"rx":407,"fill":378,"stroke":474,"strokeWidth":426},"188","56","rgba(61,64,91,0.35)",[380,476,478],{"x":382,"y":477,"textAnchor":384,"fontSize":394,"fill":370},"212","Function loops would rebuild the pool three times — three handshakes, three teardowns,",[380,480,482],{"x":382,"y":481,"textAnchor":384,"fontSize":394,"fill":370},"232","and the same isolation the transaction was already providing.",[484,485,486],"figcaption",{},"Isolation and lifetime are separate concerns. Widening the loop does not weaken isolation as long as the per-test transaction is doing that job.",[14,488,490],{"id":489},"why-this-works","Why this works",[10,492,493,495,496,499,500,503],{},[25,494,31],{}," tells ",[25,497,498],{},"pytest-asyncio"," which of its loop instances to run an item on. Loops are keyed by scope and by the node that owns them, so a ",[25,501,502],{},"module"," scope produces one loop per test module and hands it to every test and fixture declaring that scope. The pool is created inside that loop, registers its sockets with that loop's selector, and is closed while the loop is still running — which is exactly the invariant a function-scoped loop cannot provide for a module-scoped object.",[10,505,506,507,509,510,513,514,516],{},"The per-test connection fixture is function-scoped but declares the same ",[25,508,31],{},", so it executes on the module loop while still running once per test. That combination — narrow ",[25,511,512],{},"scope",", wide ",[25,515,31],{}," — is the one worth remembering, because it is what gives per-test state on shared infrastructure.",[14,518,520],{"id":519},"edge-cases-and-failure-modes","Edge cases and failure modes",[19,522,523,533,545,551,560],{},[22,524,525,529,530,532],{},[526,527,528],"strong",{},"A test on a narrower loop requesting a wider fixture."," Legal. The reverse is not: a module-loop fixture cannot be used by a session-loop test, and ",[25,531,498],{}," reports it as a fixture requested from a different loop.",[22,534,535,541,542,544],{},[526,536,537,540],{},[25,538,539],{},"pytestmark"," inside a class."," Assigning ",[25,543,539],{}," in a class body applies to that class only; tests outside it in the same file silently keep function loops. Put it at module level unless the split is deliberate.",[22,546,547,550],{},[526,548,549],{},"Leaked tasks carried forward."," A test that starts a task and does not await it leaves that task pending on a loop the next test will use. With function loops the leak dies with the loop; with a shared one it does not.",[22,552,553,556,557,559],{},[526,554,555],{},"Module-scoped fixtures that are not async."," A synchronous fixture has no loop affinity at all and needs no ",[25,558,31],{},"; adding one is harmless but signals a misunderstanding worth correcting.",[22,561,562,565,566,569],{},[526,563,564],{},"Ordering dependence."," Tests sharing a loop and a pool can accidentally depend on each other through anything not rolled back — a Redis key, an in-memory cache, a background consumer. Run the module with ",[25,567,568],{},"-p randomly"," occasionally to catch it.",[14,571,573],{"id":572},"guarding-a-shared-loop-against-leaks","Guarding a shared loop against leaks",[10,575,576],{},"The one real cost of a shared loop is that a leak survives the test that caused it. An autouse fixture makes the leak fail its own test instead of a later one.",[64,578,580],{"className":66,"code":579,"language":68,"meta":69,"style":69},"import asyncio\n\nimport pytest\n\n\n@pytest.fixture(autouse=True)\nasync def no_pending_tasks():\n    yield\n    pending = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]\n    if pending:\n        for task in pending:\n            task.cancel()                      # do not poison the next test\n        await asyncio.gather(*pending, return_exceptions=True)\n        raise AssertionError(f\"test left {len(pending)} pending task(s): {pending}\")\n",[25,581,582,586,590,594,598,602,607,612,617,622,627,632,637,642],{"__ignoreMap":69},[73,583,584],{"class":75,"line":76},[73,585,79],{},[73,587,588],{"class":75,"line":82},[73,589,86],{"emptyLinePlaceholder":85},[73,591,592],{"class":75,"line":89},[73,593,98],{},[73,595,596],{"class":75,"line":95},[73,597,86],{"emptyLinePlaceholder":85},[73,599,600],{"class":75,"line":101},[73,601,86],{"emptyLinePlaceholder":85},[73,603,604],{"class":75,"line":107},[73,605,606],{},"@pytest.fixture(autouse=True)\n",[73,608,609],{"class":75,"line":112},[73,610,611],{},"async def no_pending_tasks():\n",[73,613,614],{"class":75,"line":118},[73,615,616],{},"    yield\n",[73,618,619],{"class":75,"line":124},[73,620,621],{},"    pending = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]\n",[73,623,624],{"class":75,"line":129},[73,625,626],{},"    if pending:\n",[73,628,629],{"class":75,"line":134},[73,630,631],{},"        for task in pending:\n",[73,633,634],{"class":75,"line":140},[73,635,636],{},"            task.cancel()                      # do not poison the next test\n",[73,638,639],{"class":75,"line":146},[73,640,641],{},"        await asyncio.gather(*pending, return_exceptions=True)\n",[73,643,644],{"class":75,"line":152},[73,645,646],{},"        raise AssertionError(f\"test left {len(pending)} pending task(s): {pending}\")\n",[10,648,649,650,654],{},"Cancelling before raising matters: without it the assertion fails ",[651,652,653],"em",{},"and"," the stray tasks continue into the next test, producing a second, confusing failure. Cancelling first means exactly one test goes red and the module continues cleanly.",[10,656,657],{},"The same idea applies to other shared state on the loop. A module that starts a background consumer should assert the queue is empty at the end of each test; one that uses a lock should assert it is not held. Each check is one line and turns a future ordering mystery into an immediate, attributable failure.",[331,659,661,714],{"className":660},[334],[336,662,344,667,344,670,344,673,344,677,344,680,344,685,344,688,344,692,344,696,344,702,344,706,344,709],{"viewBox":663,"role":339,"ariaLabelledBy":664,"xmlns":343},"0 0 800 244",[665,666],"leak2-t","leak2-d",[346,668,669],{"id":665},"How a leaked task behaves under function versus shared loops",[350,671,672],{"id":666},"Under function-scoped loops, a task leaked by the first test dies when that test's loop is closed and the next test is unaffected. Under a shared module loop the leaked task survives into the following tests, where it can consume the pool or mutate state, producing a failure attributed to the wrong test.",[372,674],{"x":374,"y":374,"width":675,"height":676,"rx":377,"fill":378},"800","244",[380,678,679],{"x":444,"y":383,"textAnchor":384,"fontSize":385,"fontWeight":386,"fill":370},"A shared loop carries leaks forward",[372,681],{"x":390,"y":391,"width":682,"height":683,"rx":419,"fill":432,"stroke":433,"strokeWidth":684},"748","80","2",[380,686,687],{"x":399,"y":400,"fontSize":394,"fontWeight":386,"fill":370},"function loops",[380,689,691],{"x":399,"y":690,"fontSize":419,"fill":370},"96","test 1 leaks a task → its loop closes → the task is gone",[380,693,695],{"x":399,"y":694,"fontSize":419,"fill":440},"118","later tests unaffected; the leak is invisible and also untested",[372,697],{"x":390,"y":698,"width":682,"height":699,"rx":419,"fill":700,"stroke":701,"strokeWidth":684},"142","88","#fbe9e3","#e07a5f",[380,703,705],{"x":399,"y":704,"fontSize":394,"fontWeight":386,"fill":370},"166","shared module loop",[380,707,708],{"x":399,"y":472,"fontSize":419,"fill":370},"test 1 leaks a task → the loop lives on → the task keeps running",[380,710,713],{"x":399,"y":711,"fontSize":419,"fill":712},"210","#8f3d22","test 3 fails because test 1's task drained the pool — wrong test blamed",[484,715,716],{},"The autouse guard converts the lower row into the upper row's convenience without losing the information: the leak is reported, against the test that caused it.",[14,718,720],{"id":719},"choosing-between-module-and-session-scope","Choosing between module and session scope",[10,722,723],{},"Module scope is the safer default and session scope the faster one, and the choice follows from what the resource costs and how far the blast radius should reach.",[10,725,726],{},"A session loop amortises setup across the entire run, which is right for something genuinely global: a database pool, a containerised service, a message broker connection. Its risk is that every test in the suite shares one failure domain, so a leak anywhere can affect anything.",[10,728,729],{},"A module loop confines both the benefit and the risk to one file. For a module of thirty tests against one service that is usually the better trade — the setup is paid once per file rather than once per run, which is nearly the same saving, and a leak can only affect the twenty-nine tests the author is already looking at.",[64,731,733],{"className":66,"code":732,"language":68,"meta":69,"style":69},"# conftest.py — session scope for the truly global resource\nimport pytest_asyncio\n\n\n@pytest_asyncio.fixture(scope=\"session\", loop_scope=\"session\")\nasync def broker():\n    client = await connect_broker()\n    try:\n        yield client\n    finally:\n        await client.close()\n",[25,734,735,740,744,748,752,757,762,767,771,776,780],{"__ignoreMap":69},[73,736,737],{"class":75,"line":76},[73,738,739],{},"# conftest.py — session scope for the truly global resource\n",[73,741,742],{"class":75,"line":82},[73,743,104],{},[73,745,746],{"class":75,"line":89},[73,747,86],{"emptyLinePlaceholder":85},[73,749,750],{"class":75,"line":95},[73,751,86],{"emptyLinePlaceholder":85},[73,753,754],{"class":75,"line":101},[73,755,756],{},"@pytest_asyncio.fixture(scope=\"session\", loop_scope=\"session\")\n",[73,758,759],{"class":75,"line":107},[73,760,761],{},"async def broker():\n",[73,763,764],{"class":75,"line":112},[73,765,766],{},"    client = await connect_broker()\n",[73,768,769],{"class":75,"line":118},[73,770,179],{},[73,772,773],{"class":75,"line":124},[73,774,775],{},"        yield client\n",[73,777,778],{"class":75,"line":129},[73,779,191],{},[73,781,782],{"class":75,"line":134},[73,783,784],{},"        await client.close()\n",[64,786,788],{"className":66,"code":787,"language":68,"meta":69,"style":69},"# tests\u002Ftest_orders.py — module loop for everything else\nimport pytest\n\npytestmark = pytest.mark.asyncio(loop_scope=\"module\")\n",[25,789,790,795,799,803],{"__ignoreMap":69},[73,791,792],{"class":75,"line":76},[73,793,794],{},"# tests\u002Ftest_orders.py — module loop for everything else\n",[73,796,797],{"class":75,"line":82},[73,798,98],{},[73,800,801],{"class":75,"line":89},[73,802,86],{"emptyLinePlaceholder":85},[73,804,805],{"class":75,"line":95},[73,806,121],{},[10,808,809,810,813,814,818],{},"Mixing the two needs one rule respected: a test on the module loop cannot use the session-loop ",[25,811,812],{},"broker"," fixture. Either the test moves to session scope, or the fixture provides something loop-independent — a DSN, a factory, a synchronous handle — that the module-loop test uses to build its own client. The second option is usually cleaner, and it is the same separation recommended in ",[51,815,817],{"href":816},"\u002Fintegration-database-and-service-testing\u002Fdatabase-fixtures-and-transactional-tests\u002F","database fixtures and transactional tests",": expensive discovery at session scope, cheap per-scope construction on top.",[14,820,822],{"id":821},"confirming-the-arrangement","Confirming the arrangement",[10,824,825],{},"One temporary test proves the whole configuration, and it is worth running once whenever the scopes change.",[64,827,829],{"className":66,"code":828,"language":68,"meta":69,"style":69},"import asyncio\n\nimport pytest\n\npytestmark = pytest.mark.asyncio(loop_scope=\"module\")\n\n_seen: list[int] = []\n\n\nasync def test_records_the_loop_a():\n    _seen.append(id(asyncio.get_running_loop()))\n\n\nasync def test_records_the_loop_b(pool):\n    _seen.append(id(asyncio.get_running_loop()))\n    # The pool must be usable, which it is only if it lives on this same loop.\n    assert await pool.fetchval(\"SELECT 1\") == 1\n    assert len(set(_seen)) == 1, f\"tests ran on different loops: {_seen}\"\n",[25,830,831,835,839,843,847,851,855,860,864,868,873,878,882,886,891,895,900,905],{"__ignoreMap":69},[73,832,833],{"class":75,"line":76},[73,834,79],{},[73,836,837],{"class":75,"line":82},[73,838,86],{"emptyLinePlaceholder":85},[73,840,841],{"class":75,"line":89},[73,842,98],{},[73,844,845],{"class":75,"line":95},[73,846,86],{"emptyLinePlaceholder":85},[73,848,849],{"class":75,"line":101},[73,850,121],{},[73,852,853],{"class":75,"line":107},[73,854,86],{"emptyLinePlaceholder":85},[73,856,857],{"class":75,"line":112},[73,858,859],{},"_seen: list[int] = []\n",[73,861,862],{"class":75,"line":118},[73,863,86],{"emptyLinePlaceholder":85},[73,865,866],{"class":75,"line":124},[73,867,86],{"emptyLinePlaceholder":85},[73,869,870],{"class":75,"line":129},[73,871,872],{},"async def test_records_the_loop_a():\n",[73,874,875],{"class":75,"line":134},[73,876,877],{},"    _seen.append(id(asyncio.get_running_loop()))\n",[73,879,880],{"class":75,"line":140},[73,881,86],{"emptyLinePlaceholder":85},[73,883,884],{"class":75,"line":146},[73,885,86],{"emptyLinePlaceholder":85},[73,887,888],{"class":75,"line":152},[73,889,890],{},"async def test_records_the_loop_b(pool):\n",[73,892,893],{"class":75,"line":158},[73,894,877],{},[73,896,897],{"class":75,"line":164},[73,898,899],{},"    # The pool must be usable, which it is only if it lives on this same loop.\n",[73,901,902],{"class":75,"line":170},[73,903,904],{},"    assert await pool.fetchval(\"SELECT 1\") == 1\n",[73,906,907],{"class":75,"line":176},[73,908,909],{},"    assert len(set(_seen)) == 1, f\"tests ran on different loops: {_seen}\"\n",[331,911,913,979],{"className":912},[334],[336,914,344,919,344,922,344,925,344,929,344,934,344,938,344,943,344,948,344,952,344,955,344,959,344,963,344,967,344,970,344,973,344,976],{"viewBox":915,"role":339,"ariaLabelledBy":916,"xmlns":343},"0 0 780 214",[917,918],"chk-t","chk-d",[346,920,921],{"id":917},"Reading the loop-identity check",[350,923,924],{"id":918},"Two outcomes. Matching loop identifiers across the two tests confirm the module scope applied and the pool is reachable. Differing identifiers mean the marker did not apply, and the usual causes are a class-level pytestmark or a fixture left at the default loop scope.",[372,926],{"x":374,"y":374,"width":927,"height":928,"rx":377,"fill":378},"780","214",[380,930,933],{"x":931,"y":383,"textAnchor":384,"fontSize":932,"fontWeight":386,"fill":370},"390","15.5","What the identifiers tell you",[372,935],{"x":390,"y":391,"width":936,"height":937,"rx":394,"fill":432,"stroke":433,"strokeWidth":684},"346","140",[380,939,942],{"x":940,"y":941,"textAnchor":384,"fontSize":394,"fontWeight":386,"fill":370},"199","76","identical ids",[380,944,947],{"x":945,"y":946,"fontSize":419,"fill":370},"44","104","one loop for the module",[380,949,951],{"x":945,"y":950,"fontSize":419,"fill":370},"126","pool query succeeds",[380,953,954],{"x":945,"y":405,"fontSize":419,"fill":440},"configuration is correct",[380,956,958],{"x":945,"y":957,"fontSize":419,"fill":370},"174","delete the temporary test",[372,960],{"x":961,"y":391,"width":962,"height":937,"rx":394,"fill":700,"stroke":701,"strokeWidth":684},"404","350",[380,964,966],{"x":965,"y":941,"textAnchor":384,"fontSize":394,"fontWeight":386,"fill":370},"579","different ids",[380,968,969],{"x":445,"y":946,"fontSize":419,"fill":370},"pytestmark inside a class",[380,971,972],{"x":445,"y":950,"fontSize":419,"fill":370},"fixture loop_scope left default",[380,974,975],{"x":445,"y":405,"fontSize":419,"fill":712},"or the marker was overridden per test",[380,977,978],{"x":445,"y":957,"fontSize":419,"fill":370},"check the module level first",[484,980,981],{},"Two identifiers and one query. Anything that would break the arrangement in a hundred-test module shows up here in under a second.",[10,983,984],{},"Delete the temporary test once it passes; keeping it permanently asserts an implementation detail rather than behaviour, and the autouse leak guard above is the check worth keeping for the long term.",[10,986,987,988,990,991,993],{},"If the identifiers differ, work outward from the narrowest declaration. A ",[25,989,31],{}," argument on an individual test's marker overrides the module-level ",[25,992,539],{}," for that test alone, which is easy to leave behind after debugging. Next check that the fixture chain agrees: a fixture requesting another fixture that was left at the default scope pulls the whole chain back onto a function loop, and the error surfaces at the first test that uses the pool rather than at the fixture that caused it.",[14,995,997],{"id":996},"frequently-asked-questions","Frequently Asked Questions",[10,999,1000,1003],{},[526,1001,1002],{},"Does a shared loop mean tests share state?","\nIt means they share whatever was created on that loop. A connection pool, a running server and any lock or queue built inside it persist across the tests in that scope. Per-test isolation then has to come from something else — a transaction that rolls back, a fresh connection from the pool, an explicit reset in a fixture.",[10,1005,1006,1009,1010,1012,1013,1015],{},[526,1007,1008],{},"Can one module use a session loop while another uses function loops?","\nYes. ",[25,1011,31],{}," is declared per test or per module, so a module with ",[25,1014,539],{}," set to a session loop coexists with modules that say nothing and get function loops. The only rule is that a test may not request a fixture bound to a narrower loop than its own.",[10,1017,1018,1021,1022,1025,1026,1028,1029,1031],{},[526,1019,1020],{},"How do I prove two tests really shared a loop?","\nAssert on ",[25,1023,1024],{},"id(asyncio.get_running_loop())"," in both. Matching identifiers confirm the shared loop; differing ones mean the ",[25,1027,31],{}," did not apply, usually because the marker was placed on the class rather than the module or the fixture's ",[25,1030,31],{}," was left at its default.",[14,1033,1035],{"id":1034},"related","Related",[19,1037,1038,1044,1051,1058],{},[22,1039,1040,1043],{},[51,1041,1042],{"href":53},"pytest-asyncio in Depth"," — the full scope model this guide applies.",[22,1045,1046,1050],{},[51,1047,1049],{"href":1048},"\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fconfiguring-asyncio-mode-auto-versus-strict\u002F","Configuring asyncio_mode: auto versus strict"," — the collection half of the configuration.",[22,1052,1053,1057],{},[51,1054,1056],{"href":1055},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Ffixing-scopemismatch-errors-in-pytest\u002F","Fixing ScopeMismatch Errors in pytest"," — the synchronous analogue of the narrowing rule.",[22,1059,1060,1064],{},[51,1061,1063],{"href":1062},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Fdebugging-event-loop-is-closed-runtimeerror\u002F","Debugging the Event Loop is Closed RuntimeError"," — what the mismatched version of this looks like when it fails.",[10,1066,1067,1068],{},"← Back to ",[51,1069,1042],{"href":53},[1071,1072,1073],"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":69,"searchDepth":82,"depth":82,"links":1075},[1076,1077,1078,1079,1080,1081,1082,1083,1084],{"id":16,"depth":82,"text":17},{"id":58,"depth":82,"text":59},{"id":489,"depth":82,"text":490},{"id":519,"depth":82,"text":520},{"id":572,"depth":82,"text":573},{"id":719,"depth":82,"text":720},{"id":821,"depth":82,"text":822},{"id":996,"depth":82,"text":997},{"id":1034,"depth":82,"text":1035},"Use pytest-asyncio loop_scope to keep one event loop alive for a module or session, so connection pools and servers survive between tests without cross-loop errors.","md",{"slug":1088,"type":1089,"breadcrumb":1090,"datePublished":1091,"dateModified":1091,"faq":1092,"howto":1099},"sharing-an-event-loop-across-a-test-module","article","Sharing a Loop","2026-09-18",[1093,1095,1097],{"q":1002,"a":1094},"It means they share whatever was created on that loop. A connection pool, a running server and any lock or queue built inside it persist across the tests in that scope. Per-test isolation then has to come from something else — a transaction that rolls back, a fresh connection from the pool, an explicit reset in a fixture.",{"q":1008,"a":1096},"Yes. loop_scope is declared per test or per module, so a module with pytestmark set to a session loop coexists with modules that say nothing and get function loops. The only rule is that a test may not request a fixture bound to a narrower loop than its own.",{"q":1020,"a":1098},"Assert on id(asyncio.get_running_loop()) in both. Matching identifiers confirm the shared loop; differing ones mean the loop_scope did not apply, usually because the marker was placed on the class rather than the module or the fixture's loop_scope was left at its default.",{"name":1100,"description":1101,"steps":1102},"How to share one event loop across a test module","Declare the loop scope on the module, match the fixtures to it, and keep per-test isolation in the data rather than the loop.",[1103,1106,1109,1112,1115],{"name":1104,"text":1105},"Set the module-level marker","Assign pytestmark = pytest.mark.asyncio(loop_scope='module') so every test in the file runs on one loop.",{"name":1107,"text":1108},"Match the expensive fixture's loop scope","Declare the pool or server fixture with both scope and loop_scope set to the same value so it cannot outlive its loop.",{"name":1110,"text":1111},"Keep per-test state narrow","Acquire a connection or open a transaction per test from the shared resource rather than rebuilding the resource.",{"name":1113,"text":1114},"Assert the loop identity once","Add a temporary test that prints id(asyncio.get_running_loop()) in two tests and confirm the values match.",{"name":1116,"text":1117},"Guard against leaked tasks","Add an autouse fixture asserting no tasks remain pending at the end of each test, since a shared loop carries leaks forward.","\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fsharing-an-event-loop-across-a-test-module",{"title":5,"description":1085},"testing-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fsharing-an-event-loop-across-a-test-module\u002Findex","dMpC9i1Q0syd1QOH1gsOf_GzCoaZuxLb955DNJLHAXY",1789718768321]