[{"data":1,"prerenderedAt":1163},["ShallowReactive",2],{"page-\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fconfiguring-asyncio-mode-auto-versus-strict\u002F":3},{"id":4,"title":5,"body":6,"description":1130,"extension":1131,"meta":1132,"navigation":190,"path":1159,"seo":1160,"stem":1161,"__hash__":1162},"content\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fconfiguring-asyncio-mode-auto-versus-strict\u002Findex.md","Configuring asyncio_mode: auto versus strict",{"type":7,"value":8,"toc":1119},"minimark",[9,30,35,65,69,72,137,169,216,219,361,365,391,404,408,473,477,480,489,509,531,581,588,602,728,732,735,816,822,840,870,874,877,940,946,960,1039,1043,1055,1068,1077,1081,1110,1115],[10,11,12,13,17,18,21,22,25,26,29],"p",{},"A suite reports 340 passed, and eleven of those tests never executed a line of their body. That is the failure ",[14,15,16],"code",{},"asyncio_mode"," exists to prevent in one direction and can cause in the other: in ",[14,19,20],{},"strict"," mode a coroutine test without the marker is not run by the plugin at all, and in ",[14,23,24],{},"auto"," mode ",[14,27,28],{},"pytest-asyncio"," claims coroutine tests that another plugin was meant to handle. Setting it deliberately — and in the right place — takes two lines and removes both.",[31,32,34],"h2",{"id":33},"prerequisites","Prerequisites",[36,37,38,49,56],"ul",{},[39,40,41,44,45,48],"li",{},[14,42,43],{},"pytest >= 8.0"," and ",[14,46,47],{},"pytest-asyncio >= 0.24",".",[39,50,51,52,55],{},"Knowledge of which async plugins the environment has; ",[14,53,54],{},"pip list | grep -Ei \"asyncio|anyio|trio|tornasync\""," answers it.",[39,57,58,59,64],{},"The loop-lifetime rules from ",[60,61,63],"a",{"href":62},"\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002F","pytest-asyncio in depth",", which are configured separately.",[31,66,68],{"id":67},"solution","Solution",[10,70,71],{},"Declare the mode in the project's configuration, and override it for any subtree that belongs to a different plugin.",[73,74,79],"pre",{"className":75,"code":76,"language":77,"meta":78,"style":78},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml — the default for the whole repository\n[tool.pytest.ini_options]\n# auto: every `async def test_*` and async fixture is claimed automatically,\n# so no test needs @pytest.mark.asyncio.\nasyncio_mode = \"auto\"\n# Pin this too: leaving it unset means a plugin upgrade can move every\n# fixture onto a different loop without any change in your repository.\nasyncio_default_fixture_loop_scope = \"function\"\ntestpaths = [\"tests\"]\n","toml","",[14,80,81,89,95,101,107,113,119,125,131],{"__ignoreMap":78},[82,83,86],"span",{"class":84,"line":85},"line",1,[82,87,88],{},"# pyproject.toml — the default for the whole repository\n",[82,90,92],{"class":84,"line":91},2,[82,93,94],{},"[tool.pytest.ini_options]\n",[82,96,98],{"class":84,"line":97},3,[82,99,100],{},"# auto: every `async def test_*` and async fixture is claimed automatically,\n",[82,102,104],{"class":84,"line":103},4,[82,105,106],{},"# so no test needs @pytest.mark.asyncio.\n",[82,108,110],{"class":84,"line":109},5,[82,111,112],{},"asyncio_mode = \"auto\"\n",[82,114,116],{"class":84,"line":115},6,[82,117,118],{},"# Pin this too: leaving it unset means a plugin upgrade can move every\n",[82,120,122],{"class":84,"line":121},7,[82,123,124],{},"# fixture onto a different loop without any change in your repository.\n",[82,126,128],{"class":84,"line":127},8,[82,129,130],{},"asyncio_default_fixture_loop_scope = \"function\"\n",[82,132,134],{"class":84,"line":133},9,[82,135,136],{},"testpaths = [\"tests\"]\n",[73,138,142],{"className":139,"code":140,"language":141,"meta":78,"style":78},"language-ini shiki shiki-themes github-light github-dark","; tests\u002Fanyio\u002Fpytest.ini — this subtree belongs to AnyIO\n[pytest]\n; strict: pytest-asyncio claims nothing here unless explicitly marked,\n; leaving the anyio plugin free to collect these coroutine tests.\nasyncio_mode = strict\n","ini",[14,143,144,149,154,159,164],{"__ignoreMap":78},[82,145,146],{"class":84,"line":85},[82,147,148],{},"; tests\u002Fanyio\u002Fpytest.ini — this subtree belongs to AnyIO\n",[82,150,151],{"class":84,"line":91},[82,152,153],{},"[pytest]\n",[82,155,156],{"class":84,"line":97},[82,157,158],{},"; strict: pytest-asyncio claims nothing here unless explicitly marked,\n",[82,160,161],{"class":84,"line":103},[82,162,163],{},"; leaving the anyio plugin free to collect these coroutine tests.\n",[82,165,166],{"class":84,"line":109},[82,167,168],{},"asyncio_mode = strict\n",[73,170,174],{"className":171,"code":172,"language":173,"meta":78,"style":78},"language-python shiki shiki-themes github-light github-dark","# tests\u002Fanyio\u002Fconftest.py\nimport pytest\n\n\n@pytest.fixture(params=[\"asyncio\", \"trio\"])\ndef anyio_backend(request):\n    # Safe now: pytest-asyncio is in strict mode here and ignores these tests.\n    return request.param\n","python",[14,175,176,181,186,192,196,201,206,211],{"__ignoreMap":78},[82,177,178],{"class":84,"line":85},[82,179,180],{},"# tests\u002Fanyio\u002Fconftest.py\n",[82,182,183],{"class":84,"line":91},[82,184,185],{},"import pytest\n",[82,187,188],{"class":84,"line":97},[82,189,191],{"emptyLinePlaceholder":190},true,"\n",[82,193,194],{"class":84,"line":103},[82,195,191],{"emptyLinePlaceholder":190},[82,197,198],{"class":84,"line":109},[82,199,200],{},"@pytest.fixture(params=[\"asyncio\", \"trio\"])\n",[82,202,203],{"class":84,"line":115},[82,204,205],{},"def anyio_backend(request):\n",[82,207,208],{"class":84,"line":121},[82,209,210],{},"    # Safe now: pytest-asyncio is in strict mode here and ignores these tests.\n",[82,212,213],{"class":84,"line":127},[82,214,215],{},"    return request.param\n",[10,217,218],{},"The split is the whole technique. One mode for the bulk of the suite, a narrower one where a different runner owns the tests, and no per-test markers anywhere.",[220,221,224,357],"figure",{"className":222},[223],"diagram",[225,226,233,234,233,238,233,242,233,250,233,260,233,270,233,276,233,282,233,286,233,291,233,295,233,299,233,304,233,309,233,311,233,315,233,318,233,321,233,324,233,328,233,331,233,334,233,338,233,342,233,345,233,348,233,351,233,354],"svg",{"viewBox":227,"role":228,"ariaLabelledBy":229,"xmlns":232},"0 0 820 268","img",[230,231],"mode-t","mode-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[235,236,237],"title",{"id":230},"What each collection mode claims",[239,240,241],"desc",{"id":231},"Three columns of coroutine tests. In strict mode only tests carrying the asyncio marker are claimed and the rest are errors or skips. In auto mode every coroutine test and async fixture is claimed, including ones intended for another plugin. A per-directory override restores strict mode for the AnyIO subtree so both plugins coexist.",[243,244],"rect",{"x":245,"y":245,"width":246,"height":247,"rx":248,"fill":249},"0","820","268","14","#fffdf8",[251,252,259],"text",{"x":253,"y":254,"textAnchor":255,"fontSize":256,"fontWeight":257,"fill":258},"410","28","middle","16","700","#3d405b","Collection, not execution",[243,261],{"x":262,"y":263,"width":264,"height":265,"rx":266,"fill":267,"stroke":268,"strokeWidth":269},"26","52","244","188","12","#f7f0da","#f2cc8f","2",[251,271,275],{"x":272,"y":273,"textAnchor":255,"fontSize":274,"fontWeight":257,"fill":258},"148","78","12.5","strict (default)",[251,277,281],{"x":278,"y":279,"fontSize":280,"fill":258},"44","106","11","claims: marked tests only",[251,283,285],{"x":278,"y":284,"fontSize":280,"fill":258},"130","unmarked coroutine test",[251,287,290],{"x":278,"y":288,"fontSize":280,"fill":289},"150","#8a5a00","→ error since pytest 8.4",[251,292,294],{"x":278,"y":293,"fontSize":280,"fill":258},"180","every test needs",[251,296,298],{"x":278,"y":297,"fontSize":280,"fill":258},"200","@pytest.mark.asyncio",[251,300,303],{"x":278,"y":301,"fontSize":280,"fill":302},"226","#2a5f49","safe beside other plugins",[243,305],{"x":306,"y":263,"width":264,"height":265,"rx":266,"fill":307,"stroke":308,"strokeWidth":269},"288","#e6f0ea","#81b29a",[251,310,24],{"x":253,"y":273,"textAnchor":255,"fontSize":274,"fontWeight":257,"fill":258},[251,312,314],{"x":313,"y":279,"fontSize":280,"fill":258},"306","claims: every coroutine",[251,316,317],{"x":313,"y":284,"fontSize":280,"fill":258},"test and async fixture",[251,319,320],{"x":313,"y":288,"fontSize":280,"fill":302},"no markers needed",[251,322,323],{"x":313,"y":293,"fontSize":280,"fill":258},"including ones meant",[251,325,327],{"x":313,"y":297,"fontSize":280,"fill":326},"#8f3d22","for anyio or trio",[251,329,330],{"x":313,"y":301,"fontSize":280,"fill":258},"right for asyncio-only suites",[243,332],{"x":333,"y":263,"width":264,"height":265,"rx":266,"fill":307,"stroke":308,"strokeWidth":269},"550",[251,335,337],{"x":336,"y":273,"textAnchor":255,"fontSize":274,"fontWeight":257,"fill":258},"672","auto + local override",[251,339,341],{"x":340,"y":279,"fontSize":280,"fill":258},"568","root: auto",[251,343,344],{"x":340,"y":284,"fontSize":280,"fill":258},"tests\u002Fanyio: strict",[251,346,347],{"x":340,"y":288,"fontSize":280,"fill":302},"both plugins coexist",[251,349,350],{"x":340,"y":293,"fontSize":280,"fill":258},"nearest config wins",[251,352,353],{"x":340,"y":297,"fontSize":280,"fill":258},"no per-test markers",[251,355,356],{"x":340,"y":301,"fontSize":280,"fontWeight":257,"fill":302},"the arrangement to copy",[358,359,360],"figcaption",{},"The third column is the one to adopt in any repository that has both plugins. The first two are complete answers only when exactly one async plugin is installed.",[31,362,364],{"id":363},"why-this-works","Why this works",[10,366,367,369,370,372,373,376,377,379,380,383,384,386,387,390],{},[14,368,16],{}," is consulted during collection, when ",[14,371,28],{}," decides which items to wrap with its own ",[14,374,375],{},"pytest_pyfunc_call"," implementation. In ",[14,378,20],{}," mode it inspects each coroutine item for the ",[14,381,382],{},"asyncio"," marker and passes on anything unmarked; in ",[14,385,24],{}," mode it claims every coroutine function and every async generator fixture it sees. Because pytest reads configuration from the nearest applicable file, a ",[14,388,389],{},"pytest.ini"," inside a subdirectory changes that decision for exactly that subtree, which is what lets one repository host two runners.",[10,392,393,394,397,398,400,401,403],{},"Execution is untouched by the setting. Which loop runs a test, and how long that loop lives, comes from ",[14,395,396],{},"loop_scope"," on the marker or the fixture. Conflating the two is the most common misreading: switching to ",[14,399,24],{}," does not change any test's loop, and switching ",[14,402,396],{}," does not change what is collected.",[31,405,407],{"id":406},"edge-cases-and-failure-modes","Edge cases and failure modes",[36,409,410,427,440,452,461],{},[39,411,412,416,417,419,420,422,423,426],{},[413,414,415],"strong",{},"Two plugins, one module."," Putting an AnyIO-parametrised test in a directory governed by ",[14,418,24],{}," mode means ",[14,421,28],{}," may claim it first, and the ",[14,424,425],{},"anyio_backend"," fixture is then never used. Separate directories, separate modes.",[39,428,429,432,433,436,437,439],{},[413,430,431],{},"Markers left behind after switching to auto."," They are harmless but misleading, and ",[14,434,435],{},"--strict-markers"," will not flag them since ",[14,438,382],{}," is a real marker. Remove them in the same change.",[39,441,442,447,448,451],{},[413,443,444,446],{},[14,445,16],{}," on the command line."," ",[14,449,450],{},"-o asyncio_mode=auto"," works, but an IDE's test runner will not pass it, so the same test behaves differently depending on how it was launched. Configuration only.",[39,453,454,457,458,460],{},[413,455,456],{},"Async fixtures in auto mode that nothing awaits."," In ",[14,459,24],{}," mode an async generator fixture is claimed even if every test using it is synchronous, which produces a confusing \"coroutine was never awaited\" at setup. Make the fixture synchronous if it does not await.",[39,462,463,447,466,468,469,472],{},[413,464,465],{},"Upgrading from 0.21 or earlier.",[14,467,24],{}," mode existed, but async fixtures were handled differently; combine the mode change with removing any ",[14,470,471],{},"event_loop"," override rather than doing them separately.",[31,474,476],{"id":475},"deciding-which-mode-a-repository-should-use","Deciding which mode a repository should use",[10,478,479],{},"The decision is nearly mechanical once the inventory is done.",[10,481,482,483,485,486,488],{},"If ",[14,484,28],{}," is the only async plugin installed, use ",[14,487,24],{},". It removes a decorator from every async test, removes the class of bug where somebody forgets one, and has no downside — there is no other plugin for it to steal tests from.",[10,490,482,491,494,495,498,499,502,503,505,506,508],{},[14,492,493],{},"anyio",", ",[14,496,497],{},"pytest-trio"," or ",[14,500,501],{},"pytest-tornasync"," is also present, decide which one owns the majority of the suite. That one gets the root configuration; the minority gets a subdirectory with an overriding configuration file. Trying to keep both in ",[14,504,20],{}," mode and marking every test individually works, but it relies on every future test being marked correctly, which is precisely the discipline ",[14,507,24],{}," exists to remove.",[10,510,511,512,514,515,517,518,520,521,494,524,527,528,530],{},"A wrinkle worth checking: ",[14,513,493],{}," ships a pytest plugin that is active whenever ",[14,516,493],{}," is importable, and ",[14,519,493],{}," is a transitive dependency of ",[14,522,523],{},"httpx",[14,525,526],{},"starlette"," and several other common packages. Many teams have it installed without knowing, which makes ",[14,529,24],{}," mode riskier than it appears.",[73,532,536],{"className":533,"code":534,"language":535,"meta":78,"style":78},"language-bash shiki shiki-themes github-light github-dark","# What is actually registered, as opposed to what you think is installed.\npytest --trace-config 2>&1 | grep -i \"plugin registered\" | grep -Ei \"asyncio|anyio|trio\"\n","bash",[14,537,538,544],{"__ignoreMap":78},[82,539,540],{"class":84,"line":85},[82,541,543],{"class":542},"sJ8bj","# What is actually registered, as opposed to what you think is installed.\n",[82,545,546,550,554,558,561,564,567,571,573,575,578],{"class":84,"line":91},[82,547,549],{"class":548},"sScJk","pytest",[82,551,553],{"class":552},"sj4cs"," --trace-config",[82,555,557],{"class":556},"szBVR"," 2>&1",[82,559,560],{"class":556}," |",[82,562,563],{"class":548}," grep",[82,565,566],{"class":552}," -i",[82,568,570],{"class":569},"sZZnC"," \"plugin registered\"",[82,572,560],{"class":556},[82,574,563],{"class":548},[82,576,577],{"class":552}," -Ei",[82,579,580],{"class":569}," \"asyncio|anyio|trio\"\n",[73,582,586],{"className":583,"code":585,"language":251,"meta":78},[584],"language-text","plugin registered: \u003Cmodule 'anyio.pytest_plugin'>\nplugin registered: \u003Cmodule 'pytest_asyncio.plugin'>\n",[14,587,585],{"__ignoreMap":78},[10,589,590,591,593,594,597,598,601],{},"Two registered async plugins with ",[14,592,24],{}," mode at the root is the configuration that produces silently skipped tests. Either disable the one you do not use — ",[14,595,596],{},"-p no:anyio"," in ",[14,599,600],{},"addopts"," — or adopt the per-directory split above.",[220,603,605,719],{"className":604},[223],[225,606,233,611,233,614,233,617,233,634,233,638,233,642,233,651,233,654,233,660,233,665,233,671,233,676,233,680,233,684,233,687,233,691,233,694,233,698,233,702,233,705,233,712,233,716],{"viewBox":607,"role":228,"ariaLabelledBy":608,"xmlns":232},"0 0 800 246",[609,610],"pick-t","pick-d",[235,612,613],{"id":609},"Choosing a mode from the installed plugin inventory",[239,615,616],{"id":610},"A decision path. If pytest-asyncio is the only async plugin registered, choose auto mode everywhere. Otherwise decide which runner owns most of the suite, give that one the root configuration in auto mode, and add a strict-mode configuration file in the minority runner's directory.",[618,619,620,621,233],"defs",{},"\n    ",[622,623,630],"marker",{"id":624,"viewBox":625,"refX":626,"refY":627,"markerWidth":628,"markerHeight":628,"orient":629},"pick-a","0 0 10 10","9","5","7","auto-start-reverse",[631,632],"path",{"d":633,"fill":258},"M0 0 L10 5 L0 10 z",[243,635],{"x":245,"y":245,"width":636,"height":637,"rx":248,"fill":249},"800","246",[251,639,641],{"x":640,"y":254,"textAnchor":255,"fontSize":256,"fontWeight":257,"fill":258},"400","Start from what is registered, not from preference",[243,643],{"x":644,"y":645,"width":646,"height":647,"rx":648,"fill":649,"stroke":258,"strokeWidth":650},"250","50","300","46","10","#f4f1de","1.6",[251,652,653],{"x":640,"y":273,"textAnchor":255,"fontSize":266,"fill":258},"More than one async plugin registered?",[84,655],{"x1":644,"y1":656,"x2":657,"y2":658,"stroke":258,"strokeWidth":650,"markerEnd":659},"73","170","112","url(#pick-a)",[251,661,664],{"x":662,"y":663,"fontSize":280,"fill":302},"196","90","no",[243,666],{"x":667,"y":668,"width":669,"height":670,"rx":280,"fill":307,"stroke":308,"strokeWidth":269},"34","116","260","98",[251,672,675],{"x":673,"y":674,"textAnchor":255,"fontSize":266,"fontWeight":257,"fill":258},"164","142","asyncio_mode = auto",[251,677,679],{"x":263,"y":678,"fontSize":280,"fill":258},"168","everywhere, no markers",[251,681,683],{"x":263,"y":682,"fontSize":280,"fill":302},"190","nothing to collide with",[84,685],{"x1":333,"y1":656,"x2":686,"y2":658,"stroke":258,"strokeWidth":650,"markerEnd":659},"630",[251,688,690],{"x":689,"y":663,"fontSize":280,"fill":289},"596","yes",[243,692],{"x":693,"y":668,"width":669,"height":670,"rx":280,"fill":267,"stroke":268,"strokeWidth":269},"506",[251,695,697],{"x":696,"y":674,"textAnchor":255,"fontSize":266,"fontWeight":257,"fill":258},"636","split by directory",[251,699,701],{"x":700,"y":678,"fontSize":280,"fill":258},"524","majority runner: root config",[251,703,704],{"x":700,"y":682,"fontSize":280,"fill":258},"minority: strict in its subtree",[243,706],{"x":707,"y":288,"width":708,"height":709,"rx":648,"fill":249,"stroke":710,"strokeWidth":711},"312","176","64","rgba(61,64,91,0.35)","1.5",[251,713,715],{"x":640,"y":714,"textAnchor":255,"fontSize":280,"fill":258},"174","or disable one with",[251,717,596],{"x":640,"y":718,"textAnchor":255,"fontSize":280,"fill":258},"194",[358,720,721,722,724,725,727],{},"The inventory step is the one people skip, and ",[14,723,493],{}," arriving as a transitive dependency of ",[14,726,523],{}," is the usual surprise.",[31,729,731],{"id":730},"verifying-the-arrangement-before-trusting-it","Verifying the arrangement before trusting it",[10,733,734],{},"Two commands confirm that every async test is collected exactly once and by the plugin you intended.",[73,736,738],{"className":533,"code":737,"language":535,"meta":78,"style":78},"# 1. Nothing is silently unclaimed: no warnings about coroutines.\npytest -q -W error::RuntimeWarning --collect-only 2>&1 | tail -5\n\n# 2. The AnyIO subtree is parametrised by backend, and the rest is not.\npytest tests\u002Fanyio --collect-only -q | head -4\npytest tests\u002Funit --collect-only -q | head -4\n",[14,739,740,745,771,775,780,799],{"__ignoreMap":78},[82,741,742],{"class":84,"line":85},[82,743,744],{"class":542},"# 1. Nothing is silently unclaimed: no warnings about coroutines.\n",[82,746,747,749,752,755,758,761,763,765,768],{"class":84,"line":91},[82,748,549],{"class":548},[82,750,751],{"class":552}," -q",[82,753,754],{"class":552}," -W",[82,756,757],{"class":569}," error::RuntimeWarning",[82,759,760],{"class":552}," --collect-only",[82,762,557],{"class":556},[82,764,560],{"class":556},[82,766,767],{"class":548}," tail",[82,769,770],{"class":552}," -5\n",[82,772,773],{"class":84,"line":97},[82,774,191],{"emptyLinePlaceholder":190},[82,776,777],{"class":84,"line":103},[82,778,779],{"class":542},"# 2. The AnyIO subtree is parametrised by backend, and the rest is not.\n",[82,781,782,784,787,789,791,793,796],{"class":84,"line":109},[82,783,549],{"class":548},[82,785,786],{"class":569}," tests\u002Fanyio",[82,788,760],{"class":552},[82,790,751],{"class":552},[82,792,560],{"class":556},[82,794,795],{"class":548}," head",[82,797,798],{"class":552}," -4\n",[82,800,801,803,806,808,810,812,814],{"class":84,"line":115},[82,802,549],{"class":548},[82,804,805],{"class":569}," tests\u002Funit",[82,807,760],{"class":552},[82,809,751],{"class":552},[82,811,560],{"class":556},[82,813,795],{"class":548},[82,815,798],{"class":552},[73,817,820],{"className":818,"code":819,"language":251,"meta":78},[584],"tests\u002Fanyio\u002Ftest_streams.py::test_send_receive[asyncio]\ntests\u002Fanyio\u002Ftest_streams.py::test_send_receive[trio]\ntests\u002Funit\u002Ftest_client.py::test_fetch_returns_json\ntests\u002Funit\u002Ftest_client.py::test_fetch_raises_on_404\n",[14,821,819],{"__ignoreMap":78},[10,823,824,825,828,829,831,832,835,836,839],{},"Backend suffixes in one tree and not the other is the shape that says both plugins are collecting the tests they own. A missing suffix in ",[14,826,827],{},"tests\u002Fanyio"," means ",[14,830,28],{}," claimed those tests despite the override — usually because the override file is in the wrong directory, or because ",[14,833,834],{},"pyproject.toml","'s ",[14,837,838],{},"[tool.pytest.ini_options]"," at the root is being treated as the rootdir configuration and the subdirectory file is not a recognised configuration filename.",[10,841,842,843,494,845,494,847,44,850,853,854,858,859,861,862,865,866,869],{},"The last point is worth stating precisely: pytest recognises ",[14,844,389],{},[14,846,834],{},[14,848,849],{},"tox.ini",[14,851,852],{},"setup.cfg"," as configuration files, but only ",[855,856,857],"em",{},"one"," of them is used per run — the one found at the rootdir. A ",[14,860,389],{}," in a subdirectory does not override the root configuration in the way a ",[14,863,864],{},"conftest.py"," overrides fixtures. Where a genuine per-directory override is required, the reliable mechanism is a separate invocation for that subtree, or the ",[14,867,868],{},"--override-ini"," flag in a dedicated CI step, and the marker-based approach becomes the simpler answer.",[31,871,873],{"id":872},"migrating-a-marked-suite-to-auto-mode","Migrating a marked suite to auto mode",[10,875,876],{},"For an asyncio-only repository the migration is a single mechanical change, and it is worth doing because every future async test is then correct by default.",[73,878,880],{"className":533,"code":879,"language":535,"meta":78,"style":78},"# Remove the decorator form and the module-level pytestmark assignments.\ngrep -rln \"pytest.mark.asyncio\" tests\u002F | xargs sed -i \\\n  -e '\u002F^@pytest\\.mark\\.asyncio$\u002Fd' \\\n  -e '\u002F^pytestmark = pytest\\.mark\\.asyncio$\u002Fd'\npytest -q                     # must report the same number of tests as before\n",[14,881,882,887,914,924,931],{"__ignoreMap":78},[82,883,884],{"class":84,"line":85},[82,885,886],{"class":542},"# Remove the decorator form and the module-level pytestmark assignments.\n",[82,888,889,892,895,898,901,903,906,909,911],{"class":84,"line":91},[82,890,891],{"class":548},"grep",[82,893,894],{"class":552}," -rln",[82,896,897],{"class":569}," \"pytest.mark.asyncio\"",[82,899,900],{"class":569}," tests\u002F",[82,902,560],{"class":556},[82,904,905],{"class":548}," xargs",[82,907,908],{"class":569}," sed",[82,910,566],{"class":552},[82,912,913],{"class":552}," \\\n",[82,915,916,919,922],{"class":84,"line":97},[82,917,918],{"class":552},"  -e",[82,920,921],{"class":569}," '\u002F^@pytest\\.mark\\.asyncio$\u002Fd'",[82,923,913],{"class":552},[82,925,926,928],{"class":84,"line":103},[82,927,918],{"class":552},[82,929,930],{"class":569}," '\u002F^pytestmark = pytest\\.mark\\.asyncio$\u002Fd'\n",[82,932,933,935,937],{"class":84,"line":109},[82,934,549],{"class":548},[82,936,751],{"class":552},[82,938,939],{"class":542},"                     # must report the same number of tests as before\n",[10,941,942,943,945],{},"The check afterwards is the part that matters: the collected count must be unchanged. A drop means some tests are no longer being claimed, which in ",[14,944,24],{}," mode should be impossible unless another plugin took them — the inventory problem above, discovered at the right moment.",[10,947,948,949,952,953,955,956,959],{},"Markers carrying arguments need more care. ",[14,950,951],{},"@pytest.mark.asyncio(loop_scope=\"module\")"," is not redundant under ",[14,954,24],{}," mode, because the loop scope is not implied by the collection mode; deleting it silently moves those tests back to a function-scoped loop and can reintroduce the ",[14,957,958],{},"Event loop is closed"," failures the marker was added to fix. Delete only the bare form, and keep every marker that carries an argument.",[220,961,963,1032],{"className":962},[223],[225,964,233,969,233,972,233,975,233,978,233,981,233,985,233,989,233,995,233,998,233,1001,233,1005,233,1009,233,1014,233,1018,233,1021,233,1024,233,1026,233,1029],{"viewBox":965,"role":228,"ariaLabelledBy":966,"xmlns":232},"0 0 800 234",[967,968],"mig2-t","mig2-d",[235,970,971],{"id":967},"Which markers are safe to delete when switching to auto",[239,973,974],{"id":968},"Two categories. A bare asyncio marker, whether as a decorator or a module-level pytestmark assignment, is redundant under auto mode and can be removed. A marker carrying a loop scope argument is not redundant, because collection mode does not imply loop lifetime, and deleting it changes how the tests execute.",[243,976],{"x":245,"y":245,"width":636,"height":977,"rx":248,"fill":249},"234",[251,979,980],{"x":640,"y":254,"textAnchor":255,"fontSize":256,"fontWeight":257,"fill":258},"Two markers that look alike and are not",[243,982],{"x":262,"y":263,"width":983,"height":984,"rx":266,"fill":307,"stroke":308,"strokeWidth":269},"360","156",[251,986,988],{"x":987,"y":273,"textAnchor":255,"fontSize":274,"fontWeight":257,"fill":258},"206","safe to delete",[243,990],{"x":647,"y":991,"width":992,"height":667,"rx":993,"fill":249,"stroke":710,"strokeWidth":994},"94","320","8","1.4",[251,996,298],{"x":997,"y":668,"fontSize":280,"fill":258},"62",[243,999],{"x":647,"y":1000,"width":992,"height":667,"rx":993,"fill":249,"stroke":710,"strokeWidth":994},"136",[251,1002,1004],{"x":997,"y":1003,"fontSize":280,"fill":258},"158","pytestmark = pytest.mark.asyncio",[251,1006,1008],{"x":647,"y":1007,"fontSize":280,"fill":302},"192","auto mode already claims these tests",[243,1010],{"x":1011,"y":263,"width":983,"height":984,"rx":266,"fill":1012,"stroke":1013,"strokeWidth":269},"414","#fbe9e3","#e07a5f",[251,1015,1017],{"x":1016,"y":273,"textAnchor":255,"fontSize":274,"fontWeight":257,"fill":258},"594","must be kept",[243,1019],{"x":1020,"y":991,"width":992,"height":667,"rx":993,"fill":249,"stroke":710,"strokeWidth":994},"434",[251,1022,951],{"x":1023,"y":668,"fontSize":280,"fill":258},"450",[243,1025],{"x":1020,"y":1000,"width":992,"height":667,"rx":993,"fill":249,"stroke":710,"strokeWidth":994},[251,1027,1028],{"x":1023,"y":1003,"fontSize":280,"fill":258},"…(loop_scope=\"session\")",[251,1030,1031],{"x":1020,"y":1007,"fontSize":280,"fill":326},"deleting these changes the loop lifetime",[358,1033,1034,1035,1038],{},"The ",[14,1036,1037],{},"sed"," patterns above anchor on the whole line precisely so that the parametrised forms are left untouched.",[31,1040,1042],{"id":1041},"frequently-asked-questions","Frequently Asked Questions",[10,1044,1045,1048,1049,1051,1052,1054],{},[413,1046,1047],{},"Does asyncio_mode change how tests execute?","\nNo. It only decides which coroutine tests and fixtures ",[14,1050,28],{}," claims at collection time. Execution — which loop, how long it lives — is controlled separately by ",[14,1053,396],{},". A suite can switch from strict to auto without any test behaving differently, as long as no other async plugin is installed.",[10,1056,1057,1060,1061,1063,1064,1067],{},[413,1058,1059],{},"Can I use auto mode in one directory and strict in another?","\nPer-directory configuration files are not merged the way ",[14,1062,864],{}," files are, so the reliable split is a separate pytest invocation for that subtree, or ",[14,1065,1066],{},"--override-ini=asyncio_mode=strict"," in the job that runs it. Markers remain the fallback when a single invocation must cover both.",[10,1069,1070,1073,1074,1076],{},[413,1071,1072],{},"What happens if both pytest-asyncio and anyio try to run the same test?","\nWhichever plugin's ",[14,1075,375],{}," implementation runs first claims the test, and plugin order is not something you should rely on. The test then either runs on the wrong backend or is reported as passing without executing. Keep them in separate directories with separate invocations.",[31,1078,1080],{"id":1079},"related","Related",[36,1082,1083,1089,1096,1103],{},[39,1084,1085,1088],{},[60,1086,1087],{"href":62},"pytest-asyncio in Depth"," — the loop-scope half of the configuration, which this setting does not affect.",[39,1090,1091,1095],{},[60,1092,1094],{"href":1093},"\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fsharing-an-event-loop-across-a-test-module\u002F","Sharing an Event Loop Across a Test Module"," — what to reach for once collection is settled.",[39,1097,1098,1102],{},[60,1099,1101],{"href":1100},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002F","Testing with AnyIO & Trio"," — the plugin on the other side of this decision.",[39,1104,1105,1109],{},[60,1106,1108],{"href":1107},"\u002Fadvanced-pytest-architecture-configuration\u002Fpytest-configuration-best-practices\u002Fturning-warnings-into-errors-with-filterwarnings\u002F","Turning Warnings into Errors with filterwarnings"," — how the never-awaited warning becomes a failure instead of noise.",[10,1111,1112,1113],{},"← Back to ",[60,1114,1087],{"href":62},[1116,1117,1118],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":78,"searchDepth":91,"depth":91,"links":1120},[1121,1122,1123,1124,1125,1126,1127,1128,1129],{"id":33,"depth":91,"text":34},{"id":67,"depth":91,"text":68},{"id":363,"depth":91,"text":364},{"id":406,"depth":91,"text":407},{"id":475,"depth":91,"text":476},{"id":730,"depth":91,"text":731},{"id":872,"depth":91,"text":873},{"id":1041,"depth":91,"text":1042},{"id":1079,"depth":91,"text":1080},"Choose pytest-asyncio's collection mode deliberately: what auto claims, when strict is required, and how to keep two async plugins from fighting over the same test.","md",{"slug":1133,"type":1134,"breadcrumb":16,"datePublished":1135,"dateModified":1135,"faq":1136,"howto":1143},"configuring-asyncio-mode-auto-versus-strict","article","2026-09-18",[1137,1139,1141],{"q":1047,"a":1138},"No. It only decides which coroutine tests and fixtures pytest-asyncio claims at collection time. Execution — which loop, how long it lives — is controlled separately by loop_scope. A suite can switch from strict to auto without any test behaving differently, as long as no other async plugin is installed.",{"q":1059,"a":1140},"Yes. asyncio_mode is read from the nearest configuration file, and a pytest.ini or its equivalent in a subdirectory overrides the root setting for that subtree. That is the supported way to keep AnyIO tests separate from pytest-asyncio ones in a single repository.",{"q":1072,"a":1142},"Whichever plugin's pytest_pyfunc_call implementation runs first claims the test, and plugin order is not something you should rely on. The test then either runs on the wrong backend or is reported as passing without executing. Keep them in separate directories with separate modes.",{"name":1144,"description":1145,"steps":1146},"How to choose and set pytest-asyncio's collection mode","Pick auto for an asyncio-only suite, strict where another async plugin is present, and scope the choice per directory.",[1147,1150,1153,1156],{"name":1148,"text":1149},"Check which async plugins are installed","Run pip list and look for anyio, trio, pytest-trio or pytest-tornasync alongside pytest-asyncio.",{"name":1151,"text":1152},"Set the mode in configuration, not on the command line","Put asyncio_mode in pyproject.toml so every invocation — local, CI and IDE — behaves identically.",{"name":1154,"text":1155},"Scope strict mode to the directories that need it","Add a configuration file in the AnyIO or Trio subtree setting asyncio_mode to strict so pytest-asyncio leaves those tests alone.",{"name":1157,"text":1158},"Verify with collect-only","Confirm that every async test is collected once and by the intended plugin before relying on the arrangement.","\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fconfiguring-asyncio-mode-auto-versus-strict",{"title":5,"description":1130},"testing-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fconfiguring-asyncio-mode-auto-versus-strict\u002Findex","x-yLvi3Q5XpY_V510RMx309fzjbC_kSMGrXJZ3kgIxo",1789718768270]