[{"data":1,"prerenderedAt":1860},["ShallowReactive",2],{"page-\u002Ftesting-async-and-concurrent-python\u002F":3},{"id":4,"title":5,"body":6,"description":1838,"extension":1839,"meta":1840,"navigation":350,"path":1856,"seo":1857,"stem":1858,"__hash__":1859},"content\u002Ftesting-async-and-concurrent-python\u002Findex.md","Testing Async & Concurrent Python",{"type":7,"value":8,"toc":1825},"minimark",[9,18,49,54,69,76,259,284,288,291,323,453,467,474,613,617,635,771,787,791,801,890,906,1022,1026,1033,1052,1121,1132,1136,1165,1277,1292,1313,1426,1430,1433,1452,1458,1470,1474,1481,1506,1514,1592,1599,1603,1702,1706,1730,1739,1753,1765,1771,1775,1815,1821],[10,11,12,13,17],"p",{},"Concurrency turns a test suite's assumptions into scheduling bets. A synchronous test either passes or fails on the code; an async test also depends on which loop is running, which task got to the ",[14,15,16],"code",{},"await"," first, and whether the fixture that built the connection pool outlived the loop it was bound to. The result is the failure mode every team with an async service eventually meets: tests that are green alone, green locally, and red on a loaded CI runner about one run in forty.",[10,19,20,21,26,27,31,32,36,37,41,42,45,46,48],{},"This section is about removing those bets. It covers the mechanics of how a coroutine test is collected and executed, how ",[22,23,25],"a",{"href":24},"\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002F","pytest-asyncio's modes and loop scopes"," decide which loop your fixtures see, how ",[22,28,30],{"href":29},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002F","AnyIO runs one test body on both asyncio and Trio",", how to make a ",[22,33,35],{"href":34},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002F","race condition reproduce on demand"," instead of once a fortnight, and how ",[22,38,40],{"href":39},"\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002F","timeouts and cancellation"," keep a hung await from consuming a CI job's entire budget. Readers are assumed fluent in ",[14,43,44],{},"async","\u002F",[14,47,16],{},", tasks, and the difference between concurrency and parallelism; nothing here re-explains the event loop from scratch.",[50,51,53],"h2",{"id":52},"how-an-async-test-actually-runs","How an async test actually runs",[10,55,56,57,60,61,64,65,68],{},"A coroutine function is not a test. When pytest collects ",[14,58,59],{},"async def test_x()",", calling it produces a coroutine object and returns it — the body never executes. Without a plugin, pytest sees a test that returned a non-",[14,62,63],{},"None"," value, emits ",[14,66,67],{},"PytestUnhandledCoroutineWarning",", and skips it. Since pytest 8.4 that warning is an error by default, which is an improvement: the older behaviour silently reported a passing suite in which none of the async tests had run.",[10,70,71,72,75],{},"A plugin fixes this by hooking ",[14,73,74],{},"pytest_pyfunc_call",", intercepting the coroutine before pytest treats it as a return value, and driving it to completion on a loop the plugin owns. Everything that makes async testing subtle follows from that one substitution: the loop is created by the plugin, on a schedule you configure rather than one you call, and every async fixture is bound to whichever loop was current when it ran.",[77,78,81,255],"figure",{"className":79},[80],"diagram",[82,83,90,91,90,95,90,99,90,117,90,125,90,134,90,143,90,149,90,154,90,161,90,165,90,168,90,171,90,175,90,183,90,190,90,195,90,200,90,208,90,211,90,215,90,219,90,223,90,228,90,234,90,238,90,243,90,247,90,251],"svg",{"viewBox":84,"role":85,"ariaLabelledBy":86,"xmlns":89},"0 0 860 300","img",[87,88],"asyncrun-t","asyncrun-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[92,93,94],"title",{"id":87},"How a coroutine test reaches an event loop",[96,97,98],"desc",{"id":88},"Collection produces a coroutine function. Without a plugin, calling it returns an un-awaited coroutine and pytest errors. With a plugin, the pytest_pyfunc_call hook intercepts the call, obtains a loop for the configured scope, runs the coroutine to completion, and reports the result as an ordinary test outcome.",[100,101,102,103,90],"defs",{},"\n    ",[104,105,112],"marker",{"id":106,"viewBox":107,"refX":108,"refY":109,"markerWidth":110,"markerHeight":110,"orient":111},"asyncrun-a","0 0 10 10","9","5","7","auto-start-reverse",[113,114],"path",{"d":115,"fill":116},"M0 0 L10 5 L0 10 z","#3d405b",[118,119],"rect",{"x":120,"y":120,"width":121,"height":122,"rx":123,"fill":124},"0","860","300","14","#fffdf8",[126,127,133],"text",{"x":128,"y":129,"textAnchor":130,"fontSize":131,"fontWeight":132,"fill":116},"430","30","middle","17","700","Collection produces a coroutine; something has to await it",[118,135],{"x":136,"y":137,"width":138,"height":139,"rx":140,"fill":141,"stroke":116,"strokeWidth":142},"28","56","180","62","10","#f4f1de","1.6",[126,144,148],{"x":145,"y":146,"textAnchor":130,"fontSize":147,"fontWeight":132,"fill":116},"118","82","12.5","collection",[126,150,153],{"x":145,"y":151,"textAnchor":130,"fontSize":152,"fill":116},"101","11","async def test_x found",[155,156],"line",{"x1":157,"y1":158,"x2":159,"y2":158,"stroke":116,"strokeWidth":142,"markerEnd":160},"212","87","250","url(#asyncrun-a)",[118,162],{"x":163,"y":137,"width":164,"height":139,"rx":140,"fill":141,"stroke":116,"strokeWidth":142},"256","190",[126,166,74],{"x":167,"y":146,"textAnchor":130,"fontSize":147,"fontWeight":132,"fill":116},"351",[126,169,170],{"x":167,"y":151,"textAnchor":130,"fontSize":152,"fill":116},"first hook wins",[155,172],{"x1":167,"y1":173,"x2":167,"y2":174,"stroke":116,"strokeWidth":142,"markerEnd":160},"122","150",[155,176],{"x1":177,"y1":158,"x2":178,"y2":158,"stroke":179,"strokeWidth":180,"strokeDashArray":181,"markerEnd":160},"450","620","rgba(61,64,91,0.4)","1.5",[109,182],"4",[118,184],{"x":185,"y":137,"width":186,"height":139,"rx":140,"fill":187,"stroke":188,"strokeWidth":189},"626","206","#fbe9e3","#e07a5f","2",[126,191,194],{"x":192,"y":193,"textAnchor":130,"fontSize":147,"fontWeight":132,"fill":116},"729","80","no plugin claims it",[126,196,199],{"x":192,"y":197,"textAnchor":130,"fontSize":152,"fill":198},"100","#8f3d22","coroutine never awaited",[118,201],{"x":202,"y":203,"width":204,"height":205,"rx":140,"fill":206,"stroke":207,"strokeWidth":189},"200","156","302","60","#e6f0ea","#81b29a",[126,209,210],{"x":167,"y":138,"textAnchor":130,"fontSize":147,"fontWeight":132,"fill":116},"plugin obtains a loop for the scope",[126,212,214],{"x":167,"y":202,"textAnchor":130,"fontSize":152,"fill":213},"#2a5f49","function · module · session",[155,216],{"x1":167,"y1":217,"x2":167,"y2":218,"stroke":116,"strokeWidth":142,"markerEnd":160},"220","244",[118,220],{"x":202,"y":221,"width":204,"height":222,"rx":108,"fill":124,"stroke":116,"strokeWidth":142},"248","38",[126,224,227],{"x":167,"y":225,"textAnchor":130,"fontSize":226,"fill":116},"272","12","loop.run_until_complete(coro) → pass or fail",[118,229],{"x":230,"y":231,"width":225,"height":232,"rx":140,"fill":124,"stroke":233,"strokeWidth":180},"560","176","110","rgba(61,64,91,0.35)",[126,235,237],{"x":236,"y":202,"textAnchor":130,"fontSize":226,"fontWeight":132,"fill":116},"696","What binds to that loop",[126,239,242],{"x":240,"y":241,"fontSize":152,"fill":116},"578","222","• async fixtures set up around it",[126,244,246],{"x":240,"y":245,"fontSize":152,"fill":116},"242","• locks, queues, events created inside",[126,248,250],{"x":240,"y":249,"fontSize":152,"fill":116},"262","• connection pools and their sockets",[126,252,254],{"x":240,"y":253,"fontSize":152,"fill":116},"280","• anything caching get_event_loop()",[256,257,258],"figcaption",{},"Every async-testing problem in this section traces back to this diagram: the loop is created by the plugin at a scope you configure, and everything built inside it inherits that lifetime.",[10,260,261,262,266,267,270,271,274,275,278,279,283],{},"The practical consequence is a rule worth memorising: ",[263,264,265],"strong",{},"an object's loop affinity is decided at construction, not at use."," ",[14,268,269],{},"asyncio.Lock()"," records no loop in Python 3.10+, but the moment it is first awaited it attaches to the running loop and stays there. A session-scoped fixture that builds an ",[14,272,273],{},"asyncpg"," pool under a function-scoped loop leaves a pool holding sockets registered with a selector that gets closed after the first test. The second test then fails with ",[14,276,277],{},"RuntimeError: Event loop is closed"," or, worse, hangs — a failure explored in detail in ",[22,280,282],{"href":281},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Fdebugging-event-loop-is-closed-runtimeerror\u002F","debugging the event loop is closed RuntimeError",".",[50,285,287],{"id":286},"choosing-a-runner","Choosing a runner",[10,289,290],{},"Three options are in real use, and the choice is about the library under test rather than taste.",[10,292,293,296,297,300,301,304,305,300,308,311,312,314,315,318,319,322],{},[14,294,295],{},"pytest-asyncio"," is the default for asyncio-only code. Since 0.23 it separates ",[298,299,148],"em",{}," (",[14,302,303],{},"asyncio_mode",") from ",[298,306,307],{},"loop lifetime",[14,309,310],{},"loop_scope","), and since 0.24 the ",[14,313,310],{}," argument is available on both ",[14,316,317],{},"@pytest.mark.asyncio"," and ",[14,320,321],{},"@pytest_asyncio.fixture",". That separation is the feature: it lets one module share a loop across its tests while the rest of the suite stays function-scoped.",[324,325,330],"pre",{"className":326,"code":327,"language":328,"meta":329,"style":329},"language-python shiki shiki-themes github-light github-dark","# pyproject.toml equivalent config lives in [tool.pytest.ini_options]\nimport asyncio\n\nimport pytest\n\npytestmark = pytest.mark.asyncio(loop_scope=\"module\")  # one loop for this module\n\n\n@pytest.fixture(scope=\"module\")\ndef module_state():\n    return {\"seen\": []}\n\n\nasync def test_first(module_state):\n    module_state[\"seen\"].append(id(asyncio.get_running_loop()))\n    assert True\n\n\nasync def test_second(module_state):\n    # Same loop id as test_first, because loop_scope=\"module\" kept it alive.\n    assert module_state[\"seen\"][0] == id(asyncio.get_running_loop())\n","python","",[14,331,332,339,345,352,358,363,369,374,379,385,391,397,402,407,413,419,425,430,435,441,447],{"__ignoreMap":329},[333,334,336],"span",{"class":155,"line":335},1,[333,337,338],{},"# pyproject.toml equivalent config lives in [tool.pytest.ini_options]\n",[333,340,342],{"class":155,"line":341},2,[333,343,344],{},"import asyncio\n",[333,346,348],{"class":155,"line":347},3,[333,349,351],{"emptyLinePlaceholder":350},true,"\n",[333,353,355],{"class":155,"line":354},4,[333,356,357],{},"import pytest\n",[333,359,361],{"class":155,"line":360},5,[333,362,351],{"emptyLinePlaceholder":350},[333,364,366],{"class":155,"line":365},6,[333,367,368],{},"pytestmark = pytest.mark.asyncio(loop_scope=\"module\")  # one loop for this module\n",[333,370,372],{"class":155,"line":371},7,[333,373,351],{"emptyLinePlaceholder":350},[333,375,377],{"class":155,"line":376},8,[333,378,351],{"emptyLinePlaceholder":350},[333,380,382],{"class":155,"line":381},9,[333,383,384],{},"@pytest.fixture(scope=\"module\")\n",[333,386,388],{"class":155,"line":387},10,[333,389,390],{},"def module_state():\n",[333,392,394],{"class":155,"line":393},11,[333,395,396],{},"    return {\"seen\": []}\n",[333,398,400],{"class":155,"line":399},12,[333,401,351],{"emptyLinePlaceholder":350},[333,403,405],{"class":155,"line":404},13,[333,406,351],{"emptyLinePlaceholder":350},[333,408,410],{"class":155,"line":409},14,[333,411,412],{},"async def test_first(module_state):\n",[333,414,416],{"class":155,"line":415},15,[333,417,418],{},"    module_state[\"seen\"].append(id(asyncio.get_running_loop()))\n",[333,420,422],{"class":155,"line":421},16,[333,423,424],{},"    assert True\n",[333,426,428],{"class":155,"line":427},17,[333,429,351],{"emptyLinePlaceholder":350},[333,431,433],{"class":155,"line":432},18,[333,434,351],{"emptyLinePlaceholder":350},[333,436,438],{"class":155,"line":437},19,[333,439,440],{},"async def test_second(module_state):\n",[333,442,444],{"class":155,"line":443},20,[333,445,446],{},"    # Same loop id as test_first, because loop_scope=\"module\" kept it alive.\n",[333,448,450],{"class":155,"line":449},21,[333,451,452],{},"    assert module_state[\"seen\"][0] == id(asyncio.get_running_loop())\n",[10,454,455,458,459,462,463,466],{},[14,456,457],{},"anyio"," takes the opposite approach: it defines the test in terms of an abstract async runtime and parametrises over backends, so one test body runs on asyncio and on Trio. For a library author that is not a convenience, it is the test matrix. The cost is that backend-specific APIs (",[14,460,461],{},"asyncio.get_running_loop",", ",[14,464,465],{},"loop.call_soon",") are off-limits inside those tests.",[10,468,469,470,473],{},"Plain ",[14,471,472],{},"asyncio.run()"," inside a synchronous test remains a legitimate third option for a handful of tests in an otherwise synchronous suite. It brings no plugin, no fixture integration, and no shared loop — which is exactly why it never surprises anyone.",[77,475,477,610],{"className":476},[80],[82,478,90,483,90,486,90,489,90,496,90,500,90,504,90,509,90,513,90,518,90,522,90,527,90,532,90,536,90,539,90,543,90,545,90,550,90,554,90,558,90,560,90,563,90,566,90,571,90,573,90,576,90,579,90,583,90,586,90,590,90,596,90,600,90,603,90,607],{"viewBox":479,"role":85,"ariaLabelledBy":480,"xmlns":89},"0 0 800 330",[481,482],"runner-t","runner-d",[92,484,485],{"id":481},"Decision path for choosing an async test runner",[96,487,488],{"id":482},"A decision tree. If the library must support Trio as well as asyncio, choose AnyIO. Otherwise, if only a handful of tests are async, call asyncio.run inside synchronous tests. Otherwise choose pytest-asyncio, and pick a loop scope based on whether expensive resources are shared across tests.",[100,490,102,491,90],{},[104,492,494],{"id":493,"viewBox":107,"refX":108,"refY":109,"markerWidth":110,"markerHeight":110,"orient":111},"runner-a",[113,495],{"d":115,"fill":207},[118,497],{"x":120,"y":120,"width":498,"height":499,"rx":123,"fill":124},"800","330",[126,501,503],{"x":502,"y":129,"textAnchor":130,"fontSize":131,"fontWeight":132,"fill":116},"400","Which runner, and at which loop scope",[118,505],{"x":253,"y":506,"width":507,"height":508,"rx":140,"fill":141,"stroke":116,"strokeWidth":142},"50","240","46",[126,510,512],{"x":502,"y":511,"textAnchor":130,"fontSize":147,"fontWeight":132,"fill":116},"78","Must Trio be supported?",[155,514],{"x1":253,"y1":515,"x2":138,"y2":516,"stroke":207,"strokeWidth":142,"markerEnd":517},"73","112","url(#runner-a)",[126,519,521],{"x":186,"y":520,"fontSize":152,"fill":213},"90","yes",[118,523],{"x":524,"y":525,"width":526,"height":137,"rx":140,"fill":206,"stroke":207,"strokeWidth":189},"40","116","230",[126,528,531],{"x":529,"y":530,"textAnchor":130,"fontSize":147,"fontWeight":132,"fill":116},"155","140","AnyIO",[126,533,535],{"x":529,"y":534,"textAnchor":130,"fontSize":152,"fill":116},"159","anyio_backend parametrises",[155,537],{"x1":538,"y1":515,"x2":178,"y2":516,"stroke":179,"strokeWidth":142,"markerEnd":517},"520",[126,540,542],{"x":541,"y":520,"fontSize":152,"fill":116},"566","no",[118,544],{"x":538,"y":525,"width":159,"height":508,"rx":140,"fill":141,"stroke":116,"strokeWidth":142},[126,546,549],{"x":547,"y":548,"textAnchor":130,"fontSize":226,"fill":116},"645","144","Fewer than ~5 async tests?",[155,551],{"x1":547,"y1":552,"x2":547,"y2":553,"stroke":179,"strokeWidth":142,"markerEnd":517},"166","196",[126,555,521],{"x":556,"y":557,"fontSize":152,"fill":116},"662","186",[118,559],{"x":538,"y":202,"width":159,"height":506,"rx":140,"fill":124,"stroke":233,"strokeWidth":142},[126,561,562],{"x":547,"y":241,"textAnchor":130,"fontSize":226,"fontWeight":132,"fill":116},"asyncio.run() in a sync test",[126,564,565],{"x":547,"y":507,"textAnchor":130,"fontSize":152,"fill":116},"no plugin, no surprises",[155,567],{"x1":538,"y1":568,"x2":569,"y2":570,"stroke":207,"strokeWidth":142,"markerEnd":517},"139","420","178",[126,572,542],{"x":177,"y":203,"fontSize":152,"fill":213},[118,574],{"x":138,"y":575,"width":159,"height":137,"rx":140,"fill":206,"stroke":207,"strokeWidth":189},"182",[126,577,295],{"x":578,"y":186,"textAnchor":130,"fontSize":147,"fontWeight":132,"fill":116},"305",[126,580,582],{"x":578,"y":581,"textAnchor":130,"fontSize":152,"fill":116},"225","asyncio_mode = auto",[155,584],{"x1":578,"y1":245,"x2":578,"y2":585,"stroke":116,"strokeWidth":142,"markerEnd":517},"266",[118,587],{"x":588,"y":589,"width":138,"height":508,"rx":108,"fill":124,"stroke":116,"strokeWidth":180},"120","270",[126,591,595],{"x":592,"y":593,"textAnchor":130,"fontSize":594,"fontWeight":132,"fill":116},"210","290","11.5","isolated state",[126,597,599],{"x":592,"y":598,"textAnchor":130,"fontSize":152,"fill":116},"307","loop_scope=\"function\"",[118,601],{"x":602,"y":589,"width":202,"height":508,"rx":108,"fill":124,"stroke":116,"strokeWidth":180},"316",[126,604,606],{"x":605,"y":593,"textAnchor":130,"fontSize":594,"fontWeight":132,"fill":116},"416","shared pool or server",[126,608,609],{"x":605,"y":598,"textAnchor":130,"fontSize":152,"fill":116},"loop_scope=\"module\"",[256,611,612],{},"The runner follows from the target runtimes; the loop scope follows from what the fixtures build. Mixing two plugins inside one module is the one combination with no good outcome.",[50,614,616],{"id":615},"fixtures-that-live-on-the-right-loop","Fixtures that live on the right loop",[10,618,619,620,622,623,626,627,630,631,283],{},"The single most productive habit in an async suite is to make the fixture that creates a resource and the loop that owns it share a lifetime. ",[14,621,295],{}," expresses that directly: ",[14,624,625],{},"@pytest_asyncio.fixture(loop_scope=\"session\", scope=\"session\")"," states both halves. Get them out of step and you have written a ",[14,628,629],{},"ScopeMismatch"," waiting to happen — the synchronous analogue of which is covered in ",[22,632,634],{"href":633},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Ffixing-scopemismatch-errors-in-pytest\u002F","fixing ScopeMismatch errors in pytest",[324,636,638],{"className":326,"code":637,"language":328,"meta":329,"style":329},"import asyncpg\nimport pytest_asyncio\n\n\n@pytest_asyncio.fixture(scope=\"session\", loop_scope=\"session\")\nasync def pool():\n    # Both scopes say \"session\": the pool and the loop it registers sockets\n    # with are created and destroyed together.\n    pool = await asyncpg.create_pool(dsn=\"postgresql:\u002F\u002Flocalhost\u002Ftest\", min_size=1)\n    try:\n        yield pool\n    finally:\n        await pool.close()          # runs while the session loop is still alive\n\n\n@pytest_asyncio.fixture(loop_scope=\"session\")\nasync def conn(pool):\n    # Function-scoped fixture, session-scoped loop: the connection is per test,\n    # the loop is shared, and nothing crosses loops.\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()   # every test leaves the database clean\n",[14,639,640,645,650,654,658,663,668,673,678,683,688,693,698,703,707,711,716,721,726,731,736,741,747,753,759,765],{"__ignoreMap":329},[333,641,642],{"class":155,"line":335},[333,643,644],{},"import asyncpg\n",[333,646,647],{"class":155,"line":341},[333,648,649],{},"import pytest_asyncio\n",[333,651,652],{"class":155,"line":347},[333,653,351],{"emptyLinePlaceholder":350},[333,655,656],{"class":155,"line":354},[333,657,351],{"emptyLinePlaceholder":350},[333,659,660],{"class":155,"line":360},[333,661,662],{},"@pytest_asyncio.fixture(scope=\"session\", loop_scope=\"session\")\n",[333,664,665],{"class":155,"line":365},[333,666,667],{},"async def pool():\n",[333,669,670],{"class":155,"line":371},[333,671,672],{},"    # Both scopes say \"session\": the pool and the loop it registers sockets\n",[333,674,675],{"class":155,"line":376},[333,676,677],{},"    # with are created and destroyed together.\n",[333,679,680],{"class":155,"line":381},[333,681,682],{},"    pool = await asyncpg.create_pool(dsn=\"postgresql:\u002F\u002Flocalhost\u002Ftest\", min_size=1)\n",[333,684,685],{"class":155,"line":387},[333,686,687],{},"    try:\n",[333,689,690],{"class":155,"line":393},[333,691,692],{},"        yield pool\n",[333,694,695],{"class":155,"line":399},[333,696,697],{},"    finally:\n",[333,699,700],{"class":155,"line":404},[333,701,702],{},"        await pool.close()          # runs while the session loop is still alive\n",[333,704,705],{"class":155,"line":409},[333,706,351],{"emptyLinePlaceholder":350},[333,708,709],{"class":155,"line":415},[333,710,351],{"emptyLinePlaceholder":350},[333,712,713],{"class":155,"line":421},[333,714,715],{},"@pytest_asyncio.fixture(loop_scope=\"session\")\n",[333,717,718],{"class":155,"line":427},[333,719,720],{},"async def conn(pool):\n",[333,722,723],{"class":155,"line":432},[333,724,725],{},"    # Function-scoped fixture, session-scoped loop: the connection is per test,\n",[333,727,728],{"class":155,"line":437},[333,729,730],{},"    # the loop is shared, and nothing crosses loops.\n",[333,732,733],{"class":155,"line":443},[333,734,735],{},"    async with pool.acquire() as connection:\n",[333,737,738],{"class":155,"line":449},[333,739,740],{},"        transaction = connection.transaction()\n",[333,742,744],{"class":155,"line":743},22,[333,745,746],{},"        await transaction.start()\n",[333,748,750],{"class":155,"line":749},23,[333,751,752],{},"        try:\n",[333,754,756],{"class":155,"line":755},24,[333,757,758],{},"            yield connection\n",[333,760,762],{"class":155,"line":761},25,[333,763,764],{},"        finally:\n",[333,766,768],{"class":155,"line":767},26,[333,769,770],{},"            await transaction.rollback()   # every test leaves the database clean\n",[10,772,773,774,778,779,782,783,283],{},"In CI this matters twice over. Creating a connection pool per test costs tens of milliseconds of handshake each time, which on a thousand-test suite is minutes of wall clock; and a per-test pool multiplies the server's connection count by the worker count under ",[22,775,777],{"href":776},"\u002Fadvanced-pytest-architecture-configuration\u002Foptimizing-test-discovery\u002Fpytest-xdist-vs-pytest-parallel-performance-comparison\u002F","pytest-xdist",", which is how a test job exhausts ",[14,780,781],{},"max_connections"," on a shared database. The transactional pattern above — one durable pool, one per-test transaction rolled back at teardown — is developed further in ",[22,784,786],{"href":785},"\u002Fintegration-database-and-service-testing\u002Fdatabase-fixtures-and-transactional-tests\u002F","database fixtures and transactional tests",[50,788,790],{"id":789},"threads-tasks-and-the-shape-of-a-race","Threads, tasks, and the shape of a race",[10,792,793,794,797,798,800],{},"Async code is single-threaded, which removes data races between instructions but not between ",[298,795,796],{},"await points",". Any read-modify-write that spans an ",[14,799,16],{}," is interruptible, and a second task entering the same function will observe the intermediate state. That is a genuine race with a deterministic reproduction, because the scheduler is cooperative: if you control where the awaits are, you control the interleaving.",[324,802,804],{"className":326,"code":803,"language":328,"meta":329,"style":329},"import asyncio\n\n\nclass Counter:\n    def __init__(self):\n        self.value = 0\n\n    async def increment(self, store):\n        current = await store.read()      # suspension point: another task can run here\n        await asyncio.sleep(0)            # make the window explicit for the test\n        await store.write(current + 1)    # writes a value that may now be stale\n\n\nasync def test_lost_update_is_reproducible(store):\n    counter = Counter()\n    # Two tasks, one shared store: the second read happens before the first write.\n    await asyncio.gather(counter.increment(store), counter.increment(store))\n    assert await store.read() == 2       # fails: the increment is lost\n",[14,805,806,810,814,818,823,828,833,837,842,847,852,857,861,865,870,875,880,885],{"__ignoreMap":329},[333,807,808],{"class":155,"line":335},[333,809,344],{},[333,811,812],{"class":155,"line":341},[333,813,351],{"emptyLinePlaceholder":350},[333,815,816],{"class":155,"line":347},[333,817,351],{"emptyLinePlaceholder":350},[333,819,820],{"class":155,"line":354},[333,821,822],{},"class Counter:\n",[333,824,825],{"class":155,"line":360},[333,826,827],{},"    def __init__(self):\n",[333,829,830],{"class":155,"line":365},[333,831,832],{},"        self.value = 0\n",[333,834,835],{"class":155,"line":371},[333,836,351],{"emptyLinePlaceholder":350},[333,838,839],{"class":155,"line":376},[333,840,841],{},"    async def increment(self, store):\n",[333,843,844],{"class":155,"line":381},[333,845,846],{},"        current = await store.read()      # suspension point: another task can run here\n",[333,848,849],{"class":155,"line":387},[333,850,851],{},"        await asyncio.sleep(0)            # make the window explicit for the test\n",[333,853,854],{"class":155,"line":393},[333,855,856],{},"        await store.write(current + 1)    # writes a value that may now be stale\n",[333,858,859],{"class":155,"line":399},[333,860,351],{"emptyLinePlaceholder":350},[333,862,863],{"class":155,"line":404},[333,864,351],{"emptyLinePlaceholder":350},[333,866,867],{"class":155,"line":409},[333,868,869],{},"async def test_lost_update_is_reproducible(store):\n",[333,871,872],{"class":155,"line":415},[333,873,874],{},"    counter = Counter()\n",[333,876,877],{"class":155,"line":421},[333,878,879],{},"    # Two tasks, one shared store: the second read happens before the first write.\n",[333,881,882],{"class":155,"line":427},[333,883,884],{},"    await asyncio.gather(counter.increment(store), counter.increment(store))\n",[333,886,887],{"class":155,"line":432},[333,888,889],{},"    assert await store.read() == 2       # fails: the increment is lost\n",[10,891,892,895,896,318,899,902,903,283],{},[14,893,894],{},"asyncio.sleep(0)"," is the one sleep that belongs in a test. It yields to the loop without waiting for wall-clock time, so the interleaving is forced rather than hoped for, and the test fails every run instead of one in a hundred. The same discipline applied to real threads needs ",[14,897,898],{},"threading.Barrier",[14,900,901],{},"threading.Event"," instead, because preemption there is genuinely non-deterministic — the techniques are in ",[22,904,905],{"href":34},"testing threads and race conditions",[77,907,909,1016],{"className":908},[80],[82,910,90,915,90,918,90,921,90,925,90,930,90,935,90,939,90,942,90,947,90,950,90,953,90,960,90,964,90,970,90,973,90,975,90,979,90,981,90,984,90,987,90,990,90,993,90,995,90,997,90,1000,90,1002,90,1005,90,1012],{"viewBox":911,"role":85,"ariaLabelledBy":912,"xmlns":89},"0 0 820 260",[913,914],"race-t","race-d",[92,916,917],{"id":913},"A lost update across an await point",[96,919,920],{"id":914},"A timeline of two tasks sharing one store. Task A reads zero, then suspends at an await. Task B reads zero, adds one and writes one. Task A resumes with its stale value of zero, adds one and writes one, overwriting task B's update so the final value is one instead of two.",[118,922],{"x":120,"y":120,"width":923,"height":924,"rx":123,"fill":124},"820","260",[126,926,929],{"x":927,"y":136,"textAnchor":130,"fontSize":928,"fontWeight":132,"fill":116},"410","16","Two tasks, one await point, one lost update",[126,931,934],{"x":932,"y":933,"fontSize":226,"fontWeight":132,"fill":116},"58","76","task A",[126,936,938],{"x":932,"y":937,"fontSize":226,"fontWeight":132,"fill":116},"152","task B",[126,940,941],{"x":932,"y":241,"fontSize":226,"fontWeight":132,"fill":116},"store",[155,943],{"x1":588,"y1":944,"x2":945,"y2":944,"stroke":233,"strokeWidth":946},"72","770","1.4",[155,948],{"x1":588,"y1":949,"x2":945,"y2":949,"stroke":233,"strokeWidth":946},"148",[155,951],{"x1":588,"y1":952,"x2":945,"y2":952,"stroke":233,"strokeWidth":946},"218",[118,954],{"x":955,"y":956,"width":588,"height":957,"rx":958,"fill":206,"stroke":207,"strokeWidth":959},"134","54","34","8","1.8",[126,961,963],{"x":962,"y":933,"textAnchor":130,"fontSize":594,"fill":116},"194","read → 0",[118,965],{"x":966,"y":956,"width":967,"height":957,"rx":958,"fill":968,"stroke":969,"strokeWidth":959},"286","130","#f7f0da","#f2cc8f",[126,971,972],{"x":167,"y":933,"textAnchor":130,"fontSize":594,"fill":116},"await: suspended",[118,974],{"x":230,"y":956,"width":174,"height":957,"rx":958,"fill":187,"stroke":188,"strokeWidth":959},[126,976,978],{"x":977,"y":933,"textAnchor":130,"fontSize":594,"fill":116},"635","write 0 + 1 = 1",[118,980],{"x":122,"y":967,"width":588,"height":957,"rx":958,"fill":206,"stroke":207,"strokeWidth":959},[126,982,963],{"x":983,"y":937,"textAnchor":130,"fontSize":594,"fill":116},"360",[118,985],{"x":986,"y":967,"width":967,"height":957,"rx":958,"fill":206,"stroke":207,"strokeWidth":959},"440",[126,988,978],{"x":989,"y":937,"textAnchor":130,"fontSize":594,"fill":116},"505",[991,992],"circle",{"cx":962,"cy":952,"r":110,"fill":207},[126,994,120],{"x":962,"y":245,"textAnchor":130,"fontSize":152,"fill":116},[991,996],{"cx":989,"cy":952,"r":110,"fill":207},[126,998,999],{"x":989,"y":245,"textAnchor":130,"fontSize":152,"fill":116},"1",[991,1001],{"cx":977,"cy":952,"r":110,"fill":188},[126,1003,1004],{"x":977,"y":245,"textAnchor":130,"fontSize":152,"fill":198},"1 (expected 2)",[155,1006],{"x1":989,"y1":1007,"x2":989,"y2":1008,"stroke":179,"strokeWidth":1009,"strokeDashArray":1010},"164","208","1.3",[182,1011],"3",[155,1013],{"x1":977,"y1":1014,"x2":977,"y2":1008,"stroke":179,"strokeWidth":1009,"strokeDashArray":1015},"88",[182,1011],[256,1017,1018,1019,1021],{},"The window is the await, not the thread. Because the scheduler is cooperative, inserting ",[14,1020,894],{}," at the suspension point turns an intermittent production bug into a test that fails every time.",[50,1023,1025],{"id":1024},"deadlines-are-not-optional","Deadlines are not optional",[10,1027,1028,1029,1032],{},"An async test that hangs does not fail — it consumes the job's entire time budget and then gets killed, usually with no traceback and no indication of which test was running. A suite without deadlines therefore has a failure mode strictly worse than a red test: a red ",[298,1030,1031],{},"pipeline"," with no diagnosis.",[10,1034,1035,1036,1039,1040,1043,1044,1047,1048,1051],{},"Two layers are worth having. ",[14,1037,1038],{},"pytest-timeout"," sets a per-test wall-clock ceiling for the whole suite, with ",[14,1041,1042],{},"--timeout-method=thread"," producing a stack dump rather than a bare kill. Inside individual tests, ",[14,1045,1046],{},"asyncio.timeout()"," (3.11+) or ",[14,1049,1050],{},"async_timeout"," scopes a deadline to the operation actually being exercised, which is what turns \"something hung\" into \"the retry loop never exits when the server sends a partial response\".",[324,1053,1055],{"className":326,"code":1054,"language":328,"meta":329,"style":329},"import asyncio\n\nimport pytest\n\n\nasync def test_partial_response_does_not_hang(client):\n    # A per-operation deadline: this asserts the timeout behaviour of the code,\n    # not merely the health of the test runner.\n    with pytest.raises(TimeoutError):\n        async with asyncio.timeout(0.5):\n            await client.fetch_until_complete(\"\u002Fslow-stream\")\n\n    # Cleanup must still have run — the deadline is not an excuse to leak.\n    assert client.open_connections == 0\n",[14,1056,1057,1061,1065,1069,1073,1077,1082,1087,1092,1097,1102,1107,1111,1116],{"__ignoreMap":329},[333,1058,1059],{"class":155,"line":335},[333,1060,344],{},[333,1062,1063],{"class":155,"line":341},[333,1064,351],{"emptyLinePlaceholder":350},[333,1066,1067],{"class":155,"line":347},[333,1068,357],{},[333,1070,1071],{"class":155,"line":354},[333,1072,351],{"emptyLinePlaceholder":350},[333,1074,1075],{"class":155,"line":360},[333,1076,351],{"emptyLinePlaceholder":350},[333,1078,1079],{"class":155,"line":365},[333,1080,1081],{},"async def test_partial_response_does_not_hang(client):\n",[333,1083,1084],{"class":155,"line":371},[333,1085,1086],{},"    # A per-operation deadline: this asserts the timeout behaviour of the code,\n",[333,1088,1089],{"class":155,"line":376},[333,1090,1091],{},"    # not merely the health of the test runner.\n",[333,1093,1094],{"class":155,"line":381},[333,1095,1096],{},"    with pytest.raises(TimeoutError):\n",[333,1098,1099],{"class":155,"line":387},[333,1100,1101],{},"        async with asyncio.timeout(0.5):\n",[333,1103,1104],{"class":155,"line":393},[333,1105,1106],{},"            await client.fetch_until_complete(\"\u002Fslow-stream\")\n",[333,1108,1109],{"class":155,"line":399},[333,1110,351],{"emptyLinePlaceholder":350},[333,1112,1113],{"class":155,"line":404},[333,1114,1115],{},"    # Cleanup must still have run — the deadline is not an excuse to leak.\n",[333,1117,1118],{"class":155,"line":409},[333,1119,1120],{},"    assert client.open_connections == 0\n",[10,1122,1123,1124,1127,1128,283],{},"The second assertion is the important one. Cancellation in Python is delivered as an exception at the next await, so every ",[14,1125,1126],{},"finally"," on the stack runs — but only if the code actually has one. Tests that assert on post-cancellation state are the only reliable way to keep connection and lock cleanup honest, and they are covered in ",[22,1129,1131],{"href":1130},"\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002Ftesting-cancellation-and-cleanup-paths\u002F","testing cancellation and cleanup paths",[50,1133,1135],{"id":1134},"structured-concurrency-changes-what-a-test-asserts","Structured concurrency changes what a test asserts",[10,1137,1138,1141,1142,1145,1146,1149,1150,1153,1154,1157,1158,1161,1162,1164],{},[14,1139,1140],{},"asyncio.TaskGroup"," (3.11+) and ",[14,1143,1144],{},"anyio.create_task_group()"," change the failure contract of concurrent code, and tests written for the old contract quietly stop checking anything. Under ",[14,1147,1148],{},"asyncio.gather()",", one task raising leaves its siblings running and returns a single exception; under a task group, the first failure cancels every sibling and the block exits with an ",[14,1151,1152],{},"ExceptionGroup"," containing every exception that actually escaped. A test that still writes ",[14,1155,1156],{},"pytest.raises(ValueError)"," around a task group will fail with \"DID NOT RAISE\" even though a ",[14,1159,1160],{},"ValueError"," was raised, because what propagated was an ",[14,1163,1152],{}," wrapping it.",[324,1166,1168],{"className":326,"code":1167,"language":328,"meta":329,"style":329},"import asyncio\n\nimport pytest\n\n\nasync def fan_out(urls, fetch):\n    results = []\n    async with asyncio.TaskGroup() as tg:          # 3.11+\n        tasks = [tg.create_task(fetch(url)) for url in urls]\n    results.extend(task.result() for task in tasks)\n    return results\n\n\nasync def test_one_failure_cancels_the_group(fetch_that_fails_on_second):\n    # except* semantics: match inside the group, not against it.\n    with pytest.raises(ExceptionGroup) as excinfo:\n        await fan_out([\"\u002Fa\", \"\u002Fb\", \"\u002Fc\"], fetch_that_fails_on_second)\n\n    # Assert on the contents, which is where the useful information lives.\n    assert len(excinfo.value.exceptions) == 1\n    assert isinstance(excinfo.value.exceptions[0], TimeoutError)\n    # And assert the siblings were cancelled rather than left running.\n    assert fetch_that_fails_on_second.cancelled == [\"\u002Fc\"]\n",[14,1169,1170,1174,1178,1182,1186,1190,1195,1200,1205,1210,1215,1220,1224,1228,1233,1238,1243,1248,1252,1257,1262,1267,1272],{"__ignoreMap":329},[333,1171,1172],{"class":155,"line":335},[333,1173,344],{},[333,1175,1176],{"class":155,"line":341},[333,1177,351],{"emptyLinePlaceholder":350},[333,1179,1180],{"class":155,"line":347},[333,1181,357],{},[333,1183,1184],{"class":155,"line":354},[333,1185,351],{"emptyLinePlaceholder":350},[333,1187,1188],{"class":155,"line":360},[333,1189,351],{"emptyLinePlaceholder":350},[333,1191,1192],{"class":155,"line":365},[333,1193,1194],{},"async def fan_out(urls, fetch):\n",[333,1196,1197],{"class":155,"line":371},[333,1198,1199],{},"    results = []\n",[333,1201,1202],{"class":155,"line":376},[333,1203,1204],{},"    async with asyncio.TaskGroup() as tg:          # 3.11+\n",[333,1206,1207],{"class":155,"line":381},[333,1208,1209],{},"        tasks = [tg.create_task(fetch(url)) for url in urls]\n",[333,1211,1212],{"class":155,"line":387},[333,1213,1214],{},"    results.extend(task.result() for task in tasks)\n",[333,1216,1217],{"class":155,"line":393},[333,1218,1219],{},"    return results\n",[333,1221,1222],{"class":155,"line":399},[333,1223,351],{"emptyLinePlaceholder":350},[333,1225,1226],{"class":155,"line":404},[333,1227,351],{"emptyLinePlaceholder":350},[333,1229,1230],{"class":155,"line":409},[333,1231,1232],{},"async def test_one_failure_cancels_the_group(fetch_that_fails_on_second):\n",[333,1234,1235],{"class":155,"line":415},[333,1236,1237],{},"    # except* semantics: match inside the group, not against it.\n",[333,1239,1240],{"class":155,"line":421},[333,1241,1242],{},"    with pytest.raises(ExceptionGroup) as excinfo:\n",[333,1244,1245],{"class":155,"line":427},[333,1246,1247],{},"        await fan_out([\"\u002Fa\", \"\u002Fb\", \"\u002Fc\"], fetch_that_fails_on_second)\n",[333,1249,1250],{"class":155,"line":432},[333,1251,351],{"emptyLinePlaceholder":350},[333,1253,1254],{"class":155,"line":437},[333,1255,1256],{},"    # Assert on the contents, which is where the useful information lives.\n",[333,1258,1259],{"class":155,"line":443},[333,1260,1261],{},"    assert len(excinfo.value.exceptions) == 1\n",[333,1263,1264],{"class":155,"line":449},[333,1265,1266],{},"    assert isinstance(excinfo.value.exceptions[0], TimeoutError)\n",[333,1268,1269],{"class":155,"line":743},[333,1270,1271],{},"    # And assert the siblings were cancelled rather than left running.\n",[333,1273,1274],{"class":155,"line":749},[333,1275,1276],{},"    assert fetch_that_fails_on_second.cancelled == [\"\u002Fc\"]\n",[10,1278,1279,1280,1283,1284,1287,1288,1291],{},"Two assertions matter here and neither is obvious. The first is the ",[298,1281,1282],{},"shape"," of the exception group: ",[14,1285,1286],{},"len(excinfo.value.exceptions)"," distinguishes \"one thing went wrong and the rest were cancelled cleanly\" from \"three things went wrong independently\", which are very different bugs with identical log lines. The second is that the siblings were genuinely cancelled — the whole point of structured concurrency is that no task outlives its block, and that guarantee is worth a test, because a sibling that swallows ",[14,1289,1290],{},"CancelledError"," breaks it silently.",[10,1293,1294,1295,1297,1298,1301,1302,318,1304,1307,1308,1312],{},"For codebases still on 3.10, ",[14,1296,1144],{}," provides the same semantics and the ",[14,1299,1300],{},"exceptiongroup"," backport supplies ",[14,1303,1152],{},[14,1305,1306],{},"except*",", which makes the migration a version bump rather than a rewrite. The ",[22,1309,1311],{"href":1310},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Ftesting-code-that-uses-task-groups\u002F","task group guide"," works through the assertions in full, including the nesting rules that decide whether a group flattens or wraps.",[77,1314,1316,1416],{"className":1315},[80],[82,1317,90,1322,90,1325,90,1328,90,1331,90,1334,90,1339,90,1342,90,1346,90,1351,90,1354,90,1357,90,1359,90,1363,90,1366,90,1370,90,1375,90,1379,90,1382,90,1386,90,1389,90,1392,90,1395,90,1398,90,1401,90,1405,90,1408,90,1411,90,1413],{"viewBox":1318,"role":85,"ariaLabelledBy":1319,"xmlns":89},"0 0 800 268",[1320,1321],"tg-t","tg-d",[92,1323,1324],{"id":1320},"gather versus task group when one child fails",[96,1326,1327],{"id":1321},"Two panels. Under asyncio.gather, a failing child returns one exception while the sibling tasks keep running unsupervised. Under a task group, the first failure cancels every sibling and the block raises an ExceptionGroup, so nothing outlives the block.",[118,1329],{"x":120,"y":120,"width":498,"height":1330,"rx":123,"fill":124},"268",[126,1332,1333],{"x":502,"y":136,"textAnchor":130,"fontSize":928,"fontWeight":132,"fill":116},"What escapes the block when a child fails",[118,1335],{"x":1336,"y":1337,"width":983,"height":1338,"rx":226,"fill":141,"stroke":233,"strokeWidth":180},"26","48","198",[126,1340,1148],{"x":186,"y":944,"textAnchor":130,"fontSize":1341,"fontWeight":132,"fill":116},"13",[118,1343],{"x":506,"y":1344,"width":1345,"height":957,"rx":958,"fill":187,"stroke":188,"strokeWidth":959},"86","96",[126,1347,1350],{"x":1348,"y":1349,"textAnchor":130,"fontSize":152,"fill":116},"98","108","child A ✗",[118,1352],{"x":1353,"y":1344,"width":1345,"height":957,"rx":958,"fill":206,"stroke":207,"strokeWidth":959},"158",[126,1355,1356],{"x":186,"y":1349,"textAnchor":130,"fontSize":152,"fill":116},"child B",[118,1358],{"x":585,"y":1344,"width":1345,"height":957,"rx":958,"fill":206,"stroke":207,"strokeWidth":959},[126,1360,1362],{"x":1361,"y":1349,"textAnchor":130,"fontSize":152,"fill":116},"314","child C",[126,1364,1365],{"x":186,"y":174,"textAnchor":130,"fontSize":594,"fill":116},"first exception propagates",[126,1367,1369],{"x":186,"y":1368,"textAnchor":130,"fontSize":594,"fill":198},"170","B and C keep running unsupervised",[118,1371],{"x":139,"y":1372,"width":1373,"height":1374,"rx":108,"fill":124,"stroke":188,"strokeWidth":142},"188","288","42",[126,1376,1378],{"x":186,"y":1377,"textAnchor":130,"fontSize":594,"fontWeight":132,"fill":116},"214","raises ValueError",[118,1380],{"x":1381,"y":1337,"width":983,"height":1338,"rx":226,"fill":141,"stroke":233,"strokeWidth":180},"414",[126,1383,1385],{"x":1384,"y":944,"textAnchor":130,"fontSize":1341,"fontWeight":132,"fill":116},"594","async with TaskGroup()",[118,1387],{"x":1388,"y":1344,"width":1345,"height":957,"rx":958,"fill":187,"stroke":188,"strokeWidth":959},"438",[126,1390,1350],{"x":1391,"y":1349,"textAnchor":130,"fontSize":152,"fill":116},"486",[118,1393],{"x":1394,"y":1344,"width":1345,"height":957,"rx":958,"fill":968,"stroke":969,"strokeWidth":959},"546",[126,1396,1397],{"x":1384,"y":1349,"textAnchor":130,"fontSize":152,"fill":116},"B cancelled",[118,1399],{"x":1400,"y":1344,"width":1345,"height":957,"rx":958,"fill":968,"stroke":969,"strokeWidth":959},"654",[126,1402,1404],{"x":1403,"y":1349,"textAnchor":130,"fontSize":152,"fill":116},"702","C cancelled",[126,1406,1407],{"x":1384,"y":174,"textAnchor":130,"fontSize":594,"fill":116},"siblings cancelled at the next await",[126,1409,1410],{"x":1384,"y":1368,"textAnchor":130,"fontSize":594,"fill":213},"nothing outlives the block",[118,1412],{"x":177,"y":1372,"width":1373,"height":1374,"rx":108,"fill":124,"stroke":207,"strokeWidth":142},[126,1414,1415],{"x":1384,"y":1377,"textAnchor":130,"fontSize":594,"fontWeight":132,"fill":116},"raises ExceptionGroup([ValueError])",[256,1417,1418,1419,1422,1423,283],{},"Migrating from ",[14,1420,1421],{},"gather"," to a task group changes the type that reaches the test. Matching on the group's contents keeps the assertion specific instead of degrading to a bare ",[14,1424,1425],{},"except Exception",[50,1427,1429],{"id":1428},"keeping-an-async-suite-fast-without-making-it-flaky","Keeping an async suite fast without making it flaky",[10,1431,1432],{},"Async suites are usually slow for one of two reasons, and the two want opposite remedies. The first is genuine I\u002FO: hundreds of tests each opening a connection, each paying a TLS handshake. The second is artificial waiting: sleeps inserted to \"let things settle\", which cost the same time on every run whether or not the thing they wait for has happened.",[10,1434,1435,1436,1439,1440,1443,1444,1447,1448,283],{},"Fix the second first, because it also fixes flakiness. Every ",[14,1437,1438],{},"await asyncio.sleep(0.2)"," in a suite is simultaneously a 200 ms tax and a bet that 200 ms is enough on the slowest runner that will ever execute it. Replacing it with an ",[14,1441,1442],{},"asyncio.Event"," that the code under test sets, or with a bounded polling loop that asserts a condition, usually reduces the wait to a few milliseconds ",[298,1445,1446],{},"and"," removes the failure mode. The mechanics — including how to keep the polling loop from becoming a busy wait — are in ",[22,1449,1451],{"href":1450},"\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002Freplacing-sleep-based-waits-with-polling-assertions\u002F","replacing sleep-based waits with polling assertions",[10,1453,1454,1455,1457],{},"Only then attack the I\u002FO, and do it by widening scope rather than by adding concurrency. A session-scoped loop with a session-scoped pool amortises the expensive setup across the whole run; per-test isolation comes from a transaction that rolls back, not from a fresh connection. Where real parallelism is needed, ",[14,1456,777],{}," process workers each get their own loop and their own pool, which is safe precisely because nothing is shared across processes — but it multiplies the resource footprint by the worker count, so the database's connection limit becomes the real ceiling.",[10,1459,1460,1461,1464,1465,1469],{},"One measurement is worth taking before any of this: run with ",[14,1462,1463],{},"--durations=25"," and check whether the slowest tests are slow in setup or in the call phase. Setup-heavy suites are fixed by scope; call-heavy suites are fixed by removing sleeps or by faking the remote service entirely with the techniques in ",[22,1466,1468],{"href":1467},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002F","mocking network and HTTP calls",". Guessing which of the two you have is how teams spend a week widening fixture scopes on a suite that was sleeping the whole time.",[50,1471,1473],{"id":1472},"deciding-what-to-fake-at-the-async-boundary","Deciding what to fake at the async boundary",[10,1475,1476,1477,1480],{},"The hardest judgement in an async suite is where to stop being real. Faking too little turns unit tests into integration tests that need a network; faking too much produces a suite that passes while the service cannot talk to anything. The boundary that works is the ",[298,1478,1479],{},"protocol"," boundary — the last place where your code's own types cross into somebody else's.",[10,1482,1483,1484,1487,1488,318,1491,1487,1494,1497,1498,1501,1502,283],{},"For HTTP that means faking at the transport, not at the client object. ",[14,1485,1486],{},"respx"," for ",[14,1489,1490],{},"httpx",[14,1492,1493],{},"aioresponses",[14,1495,1496],{},"aiohttp"," intercept below the client's public API, so the code under test still builds real requests, still runs its own retry and header logic, and still parses real response objects. A hand-rolled ",[14,1499,1500],{},"AsyncMock"," client, by contrast, skips all of that: it asserts that you called a method, which is a claim about your own code rather than about the exchange. The full argument is in ",[22,1503,1505],{"href":1504},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-httpx-clients-with-respx\u002F","mocking httpx clients with respx",[10,1507,1508,1509,1513],{},"For databases the calculation flips. A fake repository is cheap and fast for testing business rules, but any test that exercises SQL, migrations, constraints or isolation semantics has to run against the real engine, because those behaviours exist only in the engine. That is what ",[22,1510,1512],{"href":1511},"\u002Fintegration-database-and-service-testing\u002Fspinning-up-services-with-testcontainers\u002F","Testcontainers"," is for: a real Postgres, started once per session, reachable over a real socket from the session loop.",[1515,1516,1517,1533],"table",{},[1518,1519,1520],"thead",{},[1521,1522,1523,1527,1530],"tr",{},[1524,1525,1526],"th",{},"Boundary",[1524,1528,1529],{},"Fake it when",[1524,1531,1532],{},"Run it for real when",[1534,1535,1536,1548,1559,1570,1581],"tbody",{},[1521,1537,1538,1542,1545],{},[1539,1540,1541],"td",{},"HTTP to a third party",[1539,1543,1544],{},"testing your retry, backoff and parsing logic",[1539,1546,1547],{},"verifying the contract, via recorded or schema-checked exchanges",[1521,1549,1550,1553,1556],{},[1539,1551,1552],{},"Your own database",[1539,1554,1555],{},"the test only exercises business rules above the repository",[1539,1557,1558],{},"SQL, constraints, migrations or transaction semantics are under test",[1521,1560,1561,1564,1567],{},[1539,1562,1563],{},"Message broker",[1539,1565,1566],{},"asserting that a handler publishes the right payload",[1539,1568,1569],{},"testing acknowledgement, redelivery or ordering guarantees",[1521,1571,1572,1575,1578],{},[1539,1573,1574],{},"Clock and timers",[1539,1576,1577],{},"any test whose behaviour depends on elapsed time",[1539,1579,1580],{},"never — a real clock in a test is a sleep in disguise",[1521,1582,1583,1586,1589],{},[1539,1584,1585],{},"The event loop itself",[1539,1587,1588],{},"never",[1539,1590,1591],{},"always: a fake loop reproduces none of the scheduling you care about",[10,1593,1594,1595,283],{},"The last row deserves emphasis. Every few years someone proposes a deterministic fake event loop that fires timers instantly. It makes tests fast and makes them test a different program, because the interleavings that produce real bugs are exactly the ones the fake removes. Control time with an injected clock, control I\u002FO with a transport-level fake, and let the loop be the loop — the reasoning behind injecting rather than patching is in ",[22,1596,1598],{"href":1597},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Finjecting-a-clock-instead-of-patching-datetime\u002F","injecting a clock instead of patching datetime",[50,1600,1602],{"id":1601},"common-pitfalls-and-antipatterns","Common pitfalls and antipatterns",[1604,1605,1606,1619,1636,1658,1671,1684],"ol",{},[1607,1608,1609,266,1612,1615,1616,1618],"li",{},[263,1610,1611],{},"Creating a shared primitive at import time.",[14,1613,1614],{},"LOCK = asyncio.Lock()"," at module scope binds to whichever loop first awaits it, then fails in every later test. Root cause: module-level state outliving the loop. Fix: build it inside a fixture whose ",[14,1617,310],{}," matches its use.",[1607,1620,1621,1629,1630,1632,1633,283],{},[263,1622,1623,1624,318,1626,1628],{},"Mixing ",[14,1625,295],{},[14,1627,457],{}," in one module."," Both claim ",[14,1631,74],{},"; whichever is registered first wins, and the other's markers are silently ignored. Root cause: two plugins, one hook. Fix: one runner per module, enforced by keeping backend-parametrised tests in their own directory with a local ",[14,1634,1635],{},"conftest.py",[1607,1637,1638,1644,1645,1648,1649,1652,1653,1657],{},[263,1639,1640,1643],{},[14,1641,1642],{},"time.sleep()"," inside an async test."," It blocks the loop, so every other task stops, including the one the test is waiting on. Root cause: a synchronous call on the loop thread. Fix: ",[14,1646,1647],{},"await asyncio.sleep(...)",", or push blocking work to ",[14,1650,1651],{},"asyncio.to_thread",", and enable ",[22,1654,1656],{"href":1655},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Ffinding-blocking-calls-with-asyncio-debug-mode\u002F","asyncio debug mode"," so the loop reports the block.",[1607,1659,1660,266,1663,1666,1667,1670],{},[263,1661,1662],{},"Fire-and-forget tasks with no reference.",[14,1664,1665],{},"asyncio.create_task(coro)"," without storing the result lets the garbage collector reclaim the task mid-flight, producing the ",[14,1668,1669],{},"Task was destroyed but it is pending!"," warning and a test that passes because the work never happened. Root cause: no strong reference. Fix: keep the task in a set, or use a task group that awaits it.",[1607,1672,1673,266,1676,1679,1680,283],{},[263,1674,1675],{},"Asserting on wall-clock durations.",[14,1677,1678],{},"assert elapsed \u003C 0.1"," is a measurement of the CI runner, not the code. Root cause: a timing assertion standing in for a behavioural one. Fix: assert on ordering and on call counts, and reserve durations for benchmarks run with ",[22,1681,1683],{"href":1682},"\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fbenchmarking-with-pytest-benchmark\u002F","pytest-benchmark",[1607,1685,1686,1691,1692,1694,1695,1698,1699,1701],{},[263,1687,1688,1689,283],{},"Swallowing ",[14,1690,1290],{}," A bare ",[14,1693,1425],{}," around an await does not catch it in 3.8+, but ",[14,1696,1697],{},"except BaseException"," does, and a coroutine that absorbs cancellation makes a timeout unable to stop it. Root cause: over-broad exception handling. Fix: re-raise ",[14,1700,1290],{}," after cleanup, and assert in tests that it propagates.",[50,1703,1705],{"id":1704},"frequently-asked-questions","Frequently Asked Questions",[10,1707,1708,1711,1712,462,1715,1718,1719,1722,1723,318,1726,1729],{},[263,1709,1710],{},"Why do async tests pass alone but fail when the whole suite runs?","\nAlmost always because an object was created on one event loop and used on another. Connection pools, ",[14,1713,1714],{},"asyncio.Lock",[14,1716,1717],{},"asyncio.Queue"," and anything that captured ",[14,1720,1721],{},"get_event_loop()"," at construction bind to the loop that was running at the time. Run the suite with ",[14,1724,1725],{},"-p no:randomly",[14,1727,1728],{},"pytest --setup-show"," to see which fixture scope created the object, then match the fixture's loop scope to the object's lifetime.",[10,1731,1732,1735,1736,1738],{},[263,1733,1734],{},"Should I use pytest-asyncio or AnyIO for a new project?","\nUse AnyIO when the library under test targets both asyncio and Trio, or when you want structured concurrency primitives in the tests themselves. Use ",[14,1737,295],{}," when the code is asyncio-only and you need fine-grained control over loop scope. Both can coexist in one repository, but never in one test module — two plugins collecting the same coroutine function causes double execution or a silent skip.",[10,1740,1741,1744,1745,1748,1749,1752],{},[263,1742,1743],{},"How do I test that code handles cancellation correctly?","\nStart the coroutine as a task, let it reach the await you care about, then call ",[14,1746,1747],{},"task.cancel()"," and await the task inside ",[14,1750,1751],{},"pytest.raises(asyncio.CancelledError)",". Assert on the side effects the cleanup path was supposed to produce — a released lock, a closed connection, a flushed buffer — rather than on the exception alone.",[10,1754,1755,1758,1759,1761,1762,1764],{},[263,1756,1757],{},"Is asyncio.sleep() acceptable in a test?","\nOnly as a yield point of zero duration. ",[14,1760,894],{}," hands control back to the loop so another task can run and is deterministic. Any positive duration is a bet on scheduling that will eventually lose on a loaded CI runner; replace it with an ",[14,1763,1442],{},", a queue await, or a polling assertion with a deadline.",[10,1766,1767,1770],{},[263,1768,1769],{},"Do I need locks in tests if the code under test is single-threaded async?","\nNo, but you still need to reason about interleaving. A single event loop gives you atomicity only between await points, so any read-modify-write that spans an await can be interleaved by another task. Tests that drive two tasks through the same object are how that class of bug is found.",[50,1772,1774],{"id":1773},"related-guides","Related guides",[1776,1777,1778,1785,1792,1797,1803],"ul",{},[1607,1779,1780,1781,1784],{},"Start with ",[22,1782,1783],{"href":24},"pytest-asyncio in depth"," for modes, loop scopes and the fixture rules that follow from them.",[1607,1786,1787,1788,1791],{},"Cover two runtimes with one test body using ",[22,1789,1790],{"href":29},"AnyIO and Trio",", which also explains what you give up in exchange.",[1607,1793,1794,1795,283],{},"Make intermittent concurrency bugs reproducible with ",[22,1796,905],{"href":34},[1607,1798,1799,1800,283],{},"Keep a hung await from eating the pipeline with ",[22,1801,1802],{"href":39},"timeouts, cancellation and deadlines",[1607,1804,1805,1806,1810,1811,283],{},"When an async test fails for reasons the assertion does not explain, move to ",[22,1807,1809],{"href":1808},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002F","debugging async code and event loops"," and the scoping rules in ",[22,1812,1814],{"href":1813},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002F","mastering pytest fixtures",[10,1816,1817,1818],{},"← Back to ",[22,1819,1820],{"href":45},"all guides",[1822,1823,1824],"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":329,"searchDepth":341,"depth":341,"links":1826},[1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837],{"id":52,"depth":341,"text":53},{"id":286,"depth":341,"text":287},{"id":615,"depth":341,"text":616},{"id":789,"depth":341,"text":790},{"id":1024,"depth":341,"text":1025},{"id":1134,"depth":341,"text":1135},{"id":1428,"depth":341,"text":1429},{"id":1472,"depth":341,"text":1473},{"id":1601,"depth":341,"text":1602},{"id":1704,"depth":341,"text":1705},{"id":1773,"depth":341,"text":1774},"Write async tests that stay deterministic: event-loop lifecycles, pytest-asyncio and AnyIO trade-offs, thread-safety checks, cancellation semantics, and timeout budgets.","md",{"slug":1841,"type":1842,"breadcrumb":1843,"datePublished":1844,"dateModified":1844,"faq":1845},"testing-async-and-concurrent-python","guide","Async & Concurrency","2026-09-18",[1846,1848,1850,1852,1854],{"q":1710,"a":1847},"Almost always because an object was created on one event loop and used on another. Connection pools, asyncio.Lock, asyncio.Queue and anything that captured get_event_loop() at construction bind to the loop that was running at the time. Run the suite with -p no:randomly and pytest --setup-show to see which fixture scope created the object, then match the fixture's loop scope to the object's lifetime.",{"q":1734,"a":1849},"Use AnyIO when the library under test targets both asyncio and Trio, or when you want structured concurrency primitives in the tests themselves. Use pytest-asyncio when the code is asyncio-only and you need fine-grained control over loop scope. Both can coexist in one repository, but never in one test module — two plugins collecting the same coroutine function causes double execution or a silent skip.",{"q":1743,"a":1851},"Start the coroutine as a task, let it reach the await you care about, then call task.cancel() and await the task inside pytest.raises(asyncio.CancelledError). Assert on the side effects the cleanup path was supposed to produce — a released lock, a closed connection, a flushed buffer — rather than on the exception alone.",{"q":1757,"a":1853},"Only as a yield point of zero duration. asyncio.sleep(0) hands control back to the loop so another task can run and is deterministic. Any positive duration is a bet on scheduling that will eventually lose on a loaded CI runner; replace it with an asyncio.Event, a queue await, or a polling assertion with a deadline.",{"q":1769,"a":1855},"No, but you still need to reason about interleaving. A single event loop gives you atomicity only between await points, so any read-modify-write that spans an await can be interleaved by another task. Tests that drive two tasks through the same object are how that class of bug is found.","\u002Ftesting-async-and-concurrent-python",{"title":5,"description":1838},"testing-async-and-concurrent-python\u002Findex","d0ET_zDnQTCeO-h8eu95eGP-vxUZKKVgjJQ3d4Alk2c",1789718765717]