[{"data":1,"prerenderedAt":1210},["ShallowReactive",2],{"page-\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fhow-to-scope-pytest-fixtures-for-async-tests\u002F":3},{"id":4,"title":5,"body":6,"description":1203,"extension":1204,"meta":1205,"navigation":256,"path":1206,"seo":1207,"stem":1208,"__hash__":1209},"content\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fhow-to-scope-pytest-fixtures-for-async-tests\u002Findex.md","How to Scope Pytest Fixtures for Async Tests",{"type":7,"value":8,"toc":1194},"minimark",[9,13,41,53,58,67,87,107,111,135,151,173,185,205,209,216,360,363,437,457,461,467,539,564,569,611,614,618,621,635,641,692,698,711,753,762,781,824,830,885,891,895,901,907,967,979,985,1058,1069,1074,1108,1112,1132,1147,1170,1190],[10,11,5],"h1",{"id":12},"how-to-scope-pytest-fixtures-for-async-tests",[14,15,16,17,21,22,25,26,28,29,32,33,36,37,40],"p",{},"Integrating ",[18,19,20],"code",{},"pytest-asyncio"," into a mature test suite requires a fundamental shift in how fixture lifecycles are conceptualized. Unlike synchronous testing, where fixture teardown aligns predictably with Python's garbage collection and pytest's internal cleanup hooks, asynchronous testing introduces an independent execution model governed by the ",[18,23,24],{},"asyncio"," event loop. The core challenge lies in decoupling fixture scope from test execution while ensuring that teardown operations execute within an active, valid event loop. Modern ",[18,27,20],{}," (v0.23+) resolves this through explicit ",[18,30,31],{},"loop_scope"," configuration and strict dependency injection validation, but misalignment remains a leading cause of ",[18,34,35],{},"RuntimeError: Event loop is closed"," and ",[18,38,39],{},"ScopeMismatch"," failures in production CI\u002FCD pipelines.",[14,42,43,44,46,47,52],{},"Understanding how to scope pytest fixtures for async tests requires mapping pytest's hierarchical fixture manager to ",[18,45,24],{},"'s task scheduler. When scopes are misconfigured, fixtures may attempt to yield resources after the loop has terminated, or background tasks may leak across test boundaries, causing non-deterministic failures and memory exhaustion. For teams navigating the broader ecosystem of test configuration, aligning plugin behavior with architectural constraints is critical. See ",[48,49,51],"a",{"href":50},"\u002Fadvanced-pytest-architecture-configuration\u002F","Advanced Pytest Architecture & Configuration"," for foundational strategies on plugin lifecycle management and configuration inheritance. This guide provides a production-grade methodology for scoping async fixtures, diagnosing loop lifecycle conflicts, and implementing robust teardown guarantees.",[54,55,57],"h2",{"id":56},"the-core-problem-async-event-loops-vs-traditional-fixture-scopes","The Core Problem: Async Event Loops vs. Traditional Fixture Scopes",[14,59,60,61,63,64,66],{},"Pytest's traditional fixture resolution operates synchronously. When a test completes, pytest iterates through the fixture stack in reverse order, executing teardown logic before moving to the next test. This model assumes a single-threaded, blocking execution flow where resource cleanup occurs immediately after the test function returns. In asynchronous contexts, this assumption breaks down. The ",[18,62,24],{}," event loop manages its own task queue, coroutine scheduling, and I\u002FO multiplexing. When ",[18,65,20],{}," wraps an async test, it creates a dedicated event loop, executes the test coroutine, and then closes the loop to prevent cross-test state pollution.",[14,68,69,70,72,73,75,76,79,80,83,84,86],{},"The conflict arises when a fixture's scope outlives the test's event loop. Consider a module-scoped async fixture that initializes a database connection pool. If the fixture yields the pool and attempts to close it during teardown, but the test's event loop has already been terminated by ",[18,71,20],{},", the teardown coroutine will raise ",[18,74,35],{},". This occurs because the fixture manager attempts to run the async generator's post-yield block outside the active loop context. Historically, developers worked around this by forcing synchronous teardown using ",[18,77,78],{},"asyncio.run()"," or ",[18,81,82],{},"loop.run_until_complete()",", but these patterns introduce reentrancy issues, mask underlying scope mismatches, and violate ",[18,85,24],{},"'s single-loop-per-thread policy.",[14,88,89,90,93,94,96,97,100,101,103,104,106],{},"Furthermore, pytest's scope inheritance rules do not natively account for event loop boundaries. A session-scoped fixture instantiated in ",[18,91,92],{},"conftest.py"," expects to persist across the entire test run. However, if ",[18,95,20],{}," is configured to recreate the event loop per test (the default in older versions or ",[18,98,99],{},"strict"," mode), the session-scoped fixture loses its loop context after the first test completes. Subsequent tests attempting to use the fixture will either fail with loop closure errors or trigger silent loop recreation, leading to resource duplication and unpredictable teardown ordering. The resolution requires explicit alignment between pytest's fixture scope, ",[18,102,20],{},"'s ",[18,105,31],{}," parameter, and the underlying event loop policy. Without this alignment, teardown race conditions and state leakage become inevitable in concurrent or parallelized test execution.",[54,108,110],{"id":109},"mapping-fixture-scopes-to-async-lifecycles","Mapping Fixture Scopes to Async Lifecycles",[14,112,113,114,117,118,117,121,124,125,128,129,131,132,134],{},"Pytest defines four primary fixture scopes: ",[18,115,116],{},"function",", ",[18,119,120],{},"class",[18,122,123],{},"module",", and ",[18,126,127],{},"session",". In synchronous testing, these scopes dictate instantiation frequency and teardown timing. In asynchronous testing, they additionally dictate event loop attachment and lifecycle boundaries. ",[18,130,20],{}," v0.23+ introduces the ",[18,133,31],{}," parameter to explicitly bind a fixture's event loop to its pytest scope, decoupling test execution loops from fixture management loops.",[14,136,137,138,141,142,117,145,147,148,150],{},"When ",[18,139,140],{},"asyncio_mode = \"auto\""," is configured in ",[18,143,144],{},"pyproject.toml",[18,146,20],{}," automatically detects async tests and wraps them in an event loop. However, automatic detection does not resolve scope mismatches. You must explicitly declare ",[18,149,31],{}," to match the fixture's pytest scope. For example:",[152,153,154,161,167],"ul",{},[155,156,157,160],"li",{},[18,158,159],{},"@pytest.fixture(scope=\"function\", loop_scope=\"function\")",": Creates a new event loop per test. Ideal for isolated unit tests, HTTP client mocks, or ephemeral database transactions. The loop closes immediately after teardown.",[155,162,163,166],{},[18,164,165],{},"@pytest.fixture(scope=\"module\", loop_scope=\"module\")",": Shares a single event loop across all tests in a file. Suitable for module-level resource initialization like local Redis instances or file-based test servers.",[155,168,169,172],{},[18,170,171],{},"@pytest.fixture(scope=\"session\", loop_scope=\"session\")",": Maintains a single event loop for the entire test run. Required for expensive resources like Dockerized database containers, Kafka brokers, or connection pools.",[14,174,175,176,178,179,181,182,184],{},"The ",[18,177,31],{}," parameter ensures that the fixture's setup and teardown execute within the same loop context. Without it, ",[18,180,20],{}," defaults to ",[18,183,116],{}," loop scope, causing higher-scoped fixtures to lose their loop attachment when tests complete. This misalignment is the primary cause of teardown failures in CI environments where tests run in parallel or across multiple workers.",[14,186,187,188,190,191,193,194,196,197,199,200,204],{},"Understanding how scope inheritance interacts with ",[18,189,92],{}," hierarchy is equally critical. Fixtures defined in parent directories inherit their scope relative to the test file's location, but event loop policies do not automatically propagate. If a root ",[18,192,92],{}," defines a session-scoped async fixture, child directories will share the fixture instance, but each test file may still trigger loop recreation if ",[18,195,31],{}," is omitted. Explicitly declaring ",[18,198,31],{}," eliminates this ambiguity and ensures deterministic lifecycle management. For teams seeking deeper insight into fixture lifecycle fundamentals and scope inheritance patterns, refer to ",[48,201,203],{"href":202},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002F","Mastering Pytest Fixtures"," for comprehensive architectural guidance.",[54,206,208],{"id":207},"implementing-async-generator-fixtures-with-correct-scoping","Implementing Async Generator Fixtures with Correct Scoping",[14,210,211,212,215],{},"Async generator fixtures using ",[18,213,214],{},"yield"," provide the cleanest pattern for setup\u002Fteardown separation in asynchronous contexts. The generator yields the resource to the test, suspends execution, and resumes after the test completes to perform cleanup. However, async generators require strict adherence to event loop context and exception handling to prevent resource leaks.",[217,218,223],"pre",{"className":219,"code":220,"language":221,"meta":222,"style":222},"language-python shiki shiki-themes github-light github-dark","import pytest\nimport asyncio\nfrom typing import AsyncGenerator\nfrom myapp.db import AsyncConnectionPool\n\n@pytest.fixture(scope=\"module\", loop_scope=\"module\")\nasync def db_pool() -> AsyncGenerator[AsyncConnectionPool, None]:\n \"\"\"Module-scoped async database connection pool with guaranteed teardown.\"\"\"\n pool = AsyncConnectionPool(dsn=\"postgresql+asyncpg:\u002F\u002Ftest:test@localhost:5432\u002Ftestdb\")\n await pool.connect()\n \n try:\n yield pool\n finally:\n # Teardown MUST run inside the active event loop\n loop = asyncio.get_running_loop()\n # Ensure all pending tasks are cancelled before closing connections\n pending = asyncio.all_tasks(loop)\n for task in pending:\n task.cancel()\n await asyncio.gather(*pending, return_exceptions=True)\n await pool.close()\n","python","",[18,224,225,233,239,245,251,258,264,270,276,282,288,294,300,306,312,318,324,330,336,342,348,354],{"__ignoreMap":222},[226,227,230],"span",{"class":228,"line":229},"line",1,[226,231,232],{},"import pytest\n",[226,234,236],{"class":228,"line":235},2,[226,237,238],{},"import asyncio\n",[226,240,242],{"class":228,"line":241},3,[226,243,244],{},"from typing import AsyncGenerator\n",[226,246,248],{"class":228,"line":247},4,[226,249,250],{},"from myapp.db import AsyncConnectionPool\n",[226,252,254],{"class":228,"line":253},5,[226,255,257],{"emptyLinePlaceholder":256},true,"\n",[226,259,261],{"class":228,"line":260},6,[226,262,263],{},"@pytest.fixture(scope=\"module\", loop_scope=\"module\")\n",[226,265,267],{"class":228,"line":266},7,[226,268,269],{},"async def db_pool() -> AsyncGenerator[AsyncConnectionPool, None]:\n",[226,271,273],{"class":228,"line":272},8,[226,274,275],{}," \"\"\"Module-scoped async database connection pool with guaranteed teardown.\"\"\"\n",[226,277,279],{"class":228,"line":278},9,[226,280,281],{}," pool = AsyncConnectionPool(dsn=\"postgresql+asyncpg:\u002F\u002Ftest:test@localhost:5432\u002Ftestdb\")\n",[226,283,285],{"class":228,"line":284},10,[226,286,287],{}," await pool.connect()\n",[226,289,291],{"class":228,"line":290},11,[226,292,293],{}," \n",[226,295,297],{"class":228,"line":296},12,[226,298,299],{}," try:\n",[226,301,303],{"class":228,"line":302},13,[226,304,305],{}," yield pool\n",[226,307,309],{"class":228,"line":308},14,[226,310,311],{}," finally:\n",[226,313,315],{"class":228,"line":314},15,[226,316,317],{}," # Teardown MUST run inside the active event loop\n",[226,319,321],{"class":228,"line":320},16,[226,322,323],{}," loop = asyncio.get_running_loop()\n",[226,325,327],{"class":228,"line":326},17,[226,328,329],{}," # Ensure all pending tasks are cancelled before closing connections\n",[226,331,333],{"class":228,"line":332},18,[226,334,335],{}," pending = asyncio.all_tasks(loop)\n",[226,337,339],{"class":228,"line":338},19,[226,340,341],{}," for task in pending:\n",[226,343,345],{"class":228,"line":344},20,[226,346,347],{}," task.cancel()\n",[226,349,351],{"class":228,"line":350},21,[226,352,353],{}," await asyncio.gather(*pending, return_exceptions=True)\n",[226,355,357],{"class":228,"line":356},22,[226,358,359],{}," await pool.close()\n",[14,361,362],{},"Key implementation rules:",[364,365,366,389,402,416],"ol",{},[155,367,368,376,377,380,381,384,385,388],{},[369,370,371,372,375],"strong",{},"Always use ",[18,373,374],{},"asyncio.get_running_loop()"," in teardown",": The deprecated ",[18,378,379],{},"asyncio.get_event_loop()"," may return a closed loop or a loop from a different thread, causing ",[18,382,383],{},"RuntimeError"," or silent failures. ",[18,386,387],{},"get_running_loop()"," guarantees access to the currently executing loop context.",[155,390,391,397,398,401],{},[369,392,393,394],{},"Wrap teardown in ",[18,395,396],{},"try\u002Ffinally",": Async fixtures can raise exceptions during test execution. The ",[18,399,400],{},"finally"," block ensures cleanup executes regardless of test outcome, preventing connection pool exhaustion.",[155,403,404,407,408,411,412,415],{},[369,405,406],{},"Cancel background tasks explicitly",": If the fixture spawns background workers or listeners, they must be cancelled before closing the primary resource. ",[18,409,410],{},"asyncio.all_tasks()"," retrieves pending tasks, and ",[18,413,414],{},"asyncio.gather(..., return_exceptions=True)"," prevents unhandled cancellation errors from propagating.",[155,417,418,421,422,425,426,429,430,432,433,436],{},[369,419,420],{},"Avoid synchronous cleanup in async fixtures",": Calling ",[18,423,424],{},"pool.close()"," synchronously or using ",[18,427,428],{},"time.sleep()"," blocks the event loop and triggers ",[18,431,20],{},"'s timeout mechanisms. All teardown must be ",[18,434,435],{},"await","ed.",[14,438,439,440,443,444,36,447,450,451,453,454,456],{},"When using ",[18,441,442],{},"async with"," context managers, ensure the manager implements ",[18,445,446],{},"__aenter__",[18,448,449],{},"__aexit__"," correctly. ",[18,452,20],{}," does not automatically wrap context managers; you must explicitly manage the lifecycle via ",[18,455,214],{},".",[54,458,460],{"id":459},"edge-case-cross-scope-fixture-dependencies-event-loop-isolation","Edge Case: Cross-Scope Fixture Dependencies & Event Loop Isolation",[14,462,463,464,466],{},"Pytest enforces strict dependency injection rules: a fixture cannot request a fixture with a narrower scope. A session-scoped fixture cannot depend on a function-scoped fixture because the narrower-scoped resource would be destroyed before the wider-scoped fixture completes its lifecycle. In async contexts, this violation triggers ",[18,465,39],{}," errors and often masks underlying event loop isolation failures.",[217,468,470],{"className":219,"code":469,"language":221,"meta":222,"style":222},"# BROKEN: ScopeMismatch violation\n@pytest.fixture(scope=\"session\", loop_scope=\"session\")\nasync def session_cache():\n return {}\n\n@pytest.fixture(scope=\"function\", loop_scope=\"function\")\nasync def ephemeral_token():\n return \"temp_token\"\n\n# This will raise ScopeMismatch: session_cache requests ephemeral_token\n@pytest.fixture(scope=\"session\", loop_scope=\"session\")\nasync def broken_session_fixture(session_cache, ephemeral_token):\n session_cache[\"token\"] = ephemeral_token\n return session_cache\n",[18,471,472,477,482,487,492,496,501,506,511,515,520,524,529,534],{"__ignoreMap":222},[226,473,474],{"class":228,"line":229},[226,475,476],{},"# BROKEN: ScopeMismatch violation\n",[226,478,479],{"class":228,"line":235},[226,480,481],{},"@pytest.fixture(scope=\"session\", loop_scope=\"session\")\n",[226,483,484],{"class":228,"line":241},[226,485,486],{},"async def session_cache():\n",[226,488,489],{"class":228,"line":247},[226,490,491],{}," return {}\n",[226,493,494],{"class":228,"line":253},[226,495,257],{"emptyLinePlaceholder":256},[226,497,498],{"class":228,"line":260},[226,499,500],{},"@pytest.fixture(scope=\"function\", loop_scope=\"function\")\n",[226,502,503],{"class":228,"line":266},[226,504,505],{},"async def ephemeral_token():\n",[226,507,508],{"class":228,"line":272},[226,509,510],{}," return \"temp_token\"\n",[226,512,513],{"class":228,"line":278},[226,514,257],{"emptyLinePlaceholder":256},[226,516,517],{"class":228,"line":284},[226,518,519],{},"# This will raise ScopeMismatch: session_cache requests ephemeral_token\n",[226,521,522],{"class":228,"line":290},[226,523,481],{},[226,525,526],{"class":228,"line":296},[226,527,528],{},"async def broken_session_fixture(session_cache, ephemeral_token):\n",[226,530,531],{"class":228,"line":302},[226,532,533],{}," session_cache[\"token\"] = ephemeral_token\n",[226,535,536],{"class":228,"line":308},[226,537,538],{}," return session_cache\n",[14,540,541,542,545,546,549,550,553,554,557,558,560,561,563],{},"The error occurs because ",[18,543,544],{},"pytest","'s dependency resolver detects that ",[18,547,548],{},"ephemeral_token"," will be torn down after the first test, but ",[18,551,552],{},"broken_session_fixture"," expects to persist across the session. In async testing, this is compounded by loop scope mismatches: ",[18,555,556],{},"session_cache"," attaches to the session loop, while ",[18,559,548],{}," attaches to a per-test loop. Attempting to inject the function-scoped fixture into the session-scoped one forces cross-loop execution, which ",[18,562,24],{}," explicitly forbids.",[14,565,566],{},[369,567,568],{},"Resolution Strategies:",[364,570,571,584,594,600],{},[155,572,573,576,577,579,580,583],{},[369,574,575],{},"Elevate Dependent Fixture Scope",": Change ",[18,578,548],{}," to ",[18,581,582],{},"scope=\"session\""," if the token does not require per-test isolation.",[155,585,586,589,590,593],{},[369,587,588],{},"Parameterization Over Injection",": Pass dynamic values via ",[18,591,592],{},"@pytest.mark.parametrize"," instead of fixture injection. This keeps scopes aligned while providing test-specific data.",[155,595,596,599],{},[369,597,598],{},"Factory Pattern",": Replace the fixture with a factory function that returns a coroutine. The session-scoped fixture calls the factory during test execution, avoiding direct dependency injection.",[155,601,602,607,608,610],{},[369,603,604,605],{},"Scope Isolation via ",[18,606,31],{},": If cross-scope injection is unavoidable (e.g., testing middleware), explicitly configure ",[18,609,31],{}," to match the narrowest scope, but be aware this forces loop recreation per test and degrades performance.",[14,612,613],{},"Refactoring to eliminate cross-scope dependencies is the only production-safe approach. Async event loops are not designed for shared mutable state across different loop contexts. Isolating resources by scope prevents race conditions and ensures deterministic teardown ordering.",[54,615,617],{"id":616},"rapid-diagnosis-minimal-reproducible-examples-profiling","Rapid Diagnosis: Minimal Reproducible Examples & Profiling",[14,619,620],{},"When async fixture scoping fails, rapid diagnosis requires isolating the loop lifecycle violation and tracing fixture instantiation order. The following workflow accelerates root cause identification in complex test suites.",[14,622,623,626,627,630,631,634],{},[369,624,625],{},"1. Visualize Fixture Execution Order","\nRun ",[18,628,629],{},"pytest --setup-show"," to display fixture setup and teardown sequence. This reveals whether teardown occurs before or after the event loop closes. Combine with ",[18,632,633],{},"pytest --fixtures"," to inspect scope declarations and dependency chains.",[14,636,637,640],{},[369,638,639],{},"2. Trace Event Loop Boundaries","\nInsert diagnostic logging at fixture setup and teardown boundaries:",[217,642,644],{"className":219,"code":643,"language":221,"meta":222,"style":222},"import logging\nimport asyncio\n\nlogger = logging.getLogger(__name__)\n\n@pytest.fixture(scope=\"function\", loop_scope=\"function\")\nasync def debug_fixture():\n logger.info(f\"Setup: Loop ID {id(asyncio.get_running_loop())}\")\n yield \"resource\"\n logger.info(f\"Teardown: Loop ID {id(asyncio.get_running_loop())}\")\n",[18,645,646,651,655,659,664,668,672,677,682,687],{"__ignoreMap":222},[226,647,648],{"class":228,"line":229},[226,649,650],{},"import logging\n",[226,652,653],{"class":228,"line":235},[226,654,238],{},[226,656,657],{"class":228,"line":241},[226,658,257],{"emptyLinePlaceholder":256},[226,660,661],{"class":228,"line":247},[226,662,663],{},"logger = logging.getLogger(__name__)\n",[226,665,666],{"class":228,"line":253},[226,667,257],{"emptyLinePlaceholder":256},[226,669,670],{"class":228,"line":260},[226,671,500],{},[226,673,674],{"class":228,"line":266},[226,675,676],{},"async def debug_fixture():\n",[226,678,679],{"class":228,"line":272},[226,680,681],{}," logger.info(f\"Setup: Loop ID {id(asyncio.get_running_loop())}\")\n",[226,683,684],{"class":228,"line":278},[226,685,686],{}," yield \"resource\"\n",[226,688,689],{"class":228,"line":284},[226,690,691],{}," logger.info(f\"Teardown: Loop ID {id(asyncio.get_running_loop())}\")\n",[14,693,694,695,697],{},"If teardown logs show a different loop ID or raise ",[18,696,383],{},", the fixture is executing outside its expected loop context.",[14,699,700,703,704,706,707,710],{},[369,701,702],{},"3. Profile Memory and Task Leaks","\nEnable ",[18,705,24],{}," debug mode and ",[18,708,709],{},"tracemalloc"," to detect unclosed resources:",[217,712,714],{"className":219,"code":713,"language":221,"meta":222,"style":222},"import tracemalloc\nimport asyncio\n\ntracemalloc.start()\nasyncio.set_debug(True)\n\n# Run tests with: pytest --asyncio-mode=auto\n# Check output for: \"Task was destroyed but it is pending!\" or \"Unclosed connection\"\n",[18,715,716,721,725,729,734,739,743,748],{"__ignoreMap":222},[226,717,718],{"class":228,"line":229},[226,719,720],{},"import tracemalloc\n",[226,722,723],{"class":228,"line":235},[226,724,238],{},[226,726,727],{"class":228,"line":241},[226,728,257],{"emptyLinePlaceholder":256},[226,730,731],{"class":228,"line":247},[226,732,733],{},"tracemalloc.start()\n",[226,735,736],{"class":228,"line":253},[226,737,738],{},"asyncio.set_debug(True)\n",[226,740,741],{"class":228,"line":260},[226,742,257],{"emptyLinePlaceholder":256},[226,744,745],{"class":228,"line":266},[226,746,747],{},"# Run tests with: pytest --asyncio-mode=auto\n",[226,749,750],{"class":228,"line":272},[226,751,752],{},"# Check output for: \"Task was destroyed but it is pending!\" or \"Unclosed connection\"\n",[14,754,755,758,759,761],{},[18,756,757],{},"asyncio.set_debug(True)"," logs slow callbacks, unhandled exceptions, and pending task destruction. ",[18,760,709],{}," identifies memory leaks from unclosed async generators or connection pools.",[14,763,764,767,768,117,771,774,775,777,778,780],{},[369,765,766],{},"4. Isolate Loop Policy Conflicts","\nCustom event loop policies (e.g., ",[18,769,770],{},"uvloop",[18,772,773],{},"asyncio.ProactorEventLoop",") can interfere with ",[18,776,20],{},"'s loop management. If tests pass locally but fail in CI, verify that the CI environment uses the same Python version and loop policy. Force a consistent policy in ",[18,779,92],{},":",[217,782,784],{"className":219,"code":783,"language":221,"meta":222,"style":222},"import sys\nimport asyncio\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef enforce_loop_policy():\n if sys.platform == \"win32\":\n asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())\n yield\n",[18,785,786,791,795,799,804,809,814,819],{"__ignoreMap":222},[226,787,788],{"class":228,"line":229},[226,789,790],{},"import sys\n",[226,792,793],{"class":228,"line":235},[226,794,238],{},[226,796,797],{"class":228,"line":241},[226,798,257],{"emptyLinePlaceholder":256},[226,800,801],{"class":228,"line":247},[226,802,803],{},"@pytest.fixture(scope=\"session\", autouse=True)\n",[226,805,806],{"class":228,"line":253},[226,807,808],{},"def enforce_loop_policy():\n",[226,810,811],{"class":228,"line":260},[226,812,813],{}," if sys.platform == \"win32\":\n",[226,815,816],{"class":228,"line":266},[226,817,818],{}," asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())\n",[226,820,821],{"class":228,"line":272},[226,822,823],{}," yield\n",[14,825,826,829],{},[369,827,828],{},"5. Minimal Reproducible Example (MRE) Template","\nWhen reporting issues, strip all business logic and isolate the fixture:",[217,831,833],{"className":219,"code":832,"language":221,"meta":222,"style":222},"import pytest\nimport asyncio\n\n@pytest.fixture(scope=\"session\", loop_scope=\"session\")\nasync def session_fixture():\n await asyncio.sleep(0)\n yield \"data\"\n\n@pytest.mark.asyncio\nasync def test_session(session_fixture):\n assert session_fixture == \"data\"\n",[18,834,835,839,843,847,851,856,861,866,870,875,880],{"__ignoreMap":222},[226,836,837],{"class":228,"line":229},[226,838,232],{},[226,840,841],{"class":228,"line":235},[226,842,238],{},[226,844,845],{"class":228,"line":241},[226,846,257],{"emptyLinePlaceholder":256},[226,848,849],{"class":228,"line":247},[226,850,481],{},[226,852,853],{"class":228,"line":253},[226,854,855],{},"async def session_fixture():\n",[226,857,858],{"class":228,"line":260},[226,859,860],{}," await asyncio.sleep(0)\n",[226,862,863],{"class":228,"line":266},[226,864,865],{}," yield \"data\"\n",[226,867,868],{"class":228,"line":272},[226,869,257],{"emptyLinePlaceholder":256},[226,871,872],{"class":228,"line":278},[226,873,874],{},"@pytest.mark.asyncio\n",[226,876,877],{"class":228,"line":284},[226,878,879],{},"async def test_session(session_fixture):\n",[226,881,882],{"class":228,"line":290},[226,883,884],{}," assert session_fixture == \"data\"\n",[14,886,887,888,890],{},"If the MRE fails, the issue is a ",[18,889,20],{}," version incompatibility or configuration error. If it passes, the failure originates from application-level async code or cross-scope dependencies.",[54,892,894],{"id":893},"advanced-configuration-customizing-loop-policies-fixture-teardown-order","Advanced Configuration: Customizing Loop Policies & Fixture Teardown Order",[14,896,897,898,900],{},"Production test suites often require fine-grained control over event loop behavior, particularly when integrating with third-party async libraries or optimizing CI\u002FCD execution. ",[18,899,20],{}," allows explicit loop policy injection and teardown order customization through configuration overrides and plugin hooks.",[14,902,903,906],{},[369,904,905],{},"Custom Loop Policy Configuration","\nOverride the default event loop policy to match your runtime environment. This is critical for Windows CI runners or high-throughput Linux environments:",[217,908,910],{"className":219,"code":909,"language":221,"meta":222,"style":222},"# conftest.py\nimport asyncio\nimport sys\nimport pytest\n\ndef pytest_configure(config):\n if sys.platform == \"linux\":\n try:\n import uvloop\n asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())\n except ImportError:\n pass\n",[18,911,912,917,921,925,929,933,938,943,947,952,957,962],{"__ignoreMap":222},[226,913,914],{"class":228,"line":229},[226,915,916],{},"# conftest.py\n",[226,918,919],{"class":228,"line":235},[226,920,238],{},[226,922,923],{"class":228,"line":241},[226,924,790],{},[226,926,927],{"class":228,"line":247},[226,928,232],{},[226,930,931],{"class":228,"line":253},[226,932,257],{"emptyLinePlaceholder":256},[226,934,935],{"class":228,"line":260},[226,936,937],{},"def pytest_configure(config):\n",[226,939,940],{"class":228,"line":266},[226,941,942],{}," if sys.platform == \"linux\":\n",[226,944,945],{"class":228,"line":272},[226,946,299],{},[226,948,949],{"class":228,"line":278},[226,950,951],{}," import uvloop\n",[226,953,954],{"class":228,"line":284},[226,955,956],{}," asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())\n",[226,958,959],{"class":228,"line":290},[226,960,961],{}," except ImportError:\n",[226,963,964],{"class":228,"line":296},[226,965,966],{}," pass\n",[14,968,969,970,972,973,36,975,978],{},"Ensure ",[18,971,31],{}," declarations remain consistent with the policy. ",[18,974,770],{},[18,976,977],{},"proactor"," loops handle task cancellation differently; verify that your fixture teardown explicitly cancels pending tasks rather than relying on implicit garbage collection.",[14,980,981,984],{},[369,982,983],{},"Managing Background Tasks During Teardown","\nLong-running async fixtures (e.g., WebSocket servers, message queue consumers) must gracefully shut down. Implement a teardown coordinator:",[217,986,988],{"className":219,"code":987,"language":221,"meta":222,"style":222},"@pytest.fixture(scope=\"module\", loop_scope=\"module\")\nasync def async_server():\n server = start_background_server()\n task = asyncio.create_task(server.run_forever())\n \n try:\n yield server\n finally:\n server.stop()\n task.cancel()\n try:\n await asyncio.wait_for(task, timeout=5.0)\n except asyncio.TimeoutError:\n task.cancel()\n await asyncio.gather(task, return_exceptions=True)\n",[18,989,990,994,999,1004,1009,1013,1017,1022,1026,1031,1035,1039,1044,1049,1053],{"__ignoreMap":222},[226,991,992],{"class":228,"line":229},[226,993,263],{},[226,995,996],{"class":228,"line":235},[226,997,998],{},"async def async_server():\n",[226,1000,1001],{"class":228,"line":241},[226,1002,1003],{}," server = start_background_server()\n",[226,1005,1006],{"class":228,"line":247},[226,1007,1008],{}," task = asyncio.create_task(server.run_forever())\n",[226,1010,1011],{"class":228,"line":253},[226,1012,293],{},[226,1014,1015],{"class":228,"line":260},[226,1016,299],{},[226,1018,1019],{"class":228,"line":266},[226,1020,1021],{}," yield server\n",[226,1023,1024],{"class":228,"line":272},[226,1025,311],{},[226,1027,1028],{"class":228,"line":278},[226,1029,1030],{}," server.stop()\n",[226,1032,1033],{"class":228,"line":284},[226,1034,347],{},[226,1036,1037],{"class":228,"line":290},[226,1038,299],{},[226,1040,1041],{"class":228,"line":296},[226,1042,1043],{}," await asyncio.wait_for(task, timeout=5.0)\n",[226,1045,1046],{"class":228,"line":302},[226,1047,1048],{}," except asyncio.TimeoutError:\n",[226,1050,1051],{"class":228,"line":308},[226,1052,347],{},[226,1054,1055],{"class":228,"line":314},[226,1056,1057],{}," await asyncio.gather(task, return_exceptions=True)\n",[14,1059,1060,1061,1064,1065,1068],{},"Using ",[18,1062,1063],{},"asyncio.wait_for()"," prevents CI hangs caused by unresponsive background tasks. The timeout ensures the test suite proceeds even if a task deadlocks, while ",[18,1066,1067],{},"return_exceptions=True"," prevents cancellation errors from failing the entire test run.",[14,1070,1071],{},[369,1072,1073],{},"CI\u002FCD Optimization Strategies",[152,1075,1076,1085,1099],{},[155,1077,1078,1081,1082,1084],{},[369,1079,1080],{},"Disable Debug Mode in CI",": ",[18,1083,757],{}," adds significant overhead. Enable it only in local debugging sessions.",[155,1086,1087,1090,1091,1094,1095,1098],{},[369,1088,1089],{},"Parallelize by Module Scope",": Use ",[18,1092,1093],{},"pytest-xdist"," with ",[18,1096,1097],{},"--dist=loadgroup"," to isolate module-scoped fixtures per worker. This prevents cross-worker loop conflicts.",[155,1100,1101,1104,1105,1107],{},[369,1102,1103],{},"Explicit Teardown Ordering",": If multiple session-scoped fixtures depend on each other, declare them in reverse teardown order in ",[18,1106,92],{}," to ensure deterministic cleanup.",[54,1109,1111],{"id":1110},"faq-async-fixture-scoping","FAQ: Async Fixture Scoping",[14,1113,1114,1117,1118,1121,1122,1124,1125,1121,1128,1131],{},[369,1115,1116],{},"Can I use session-scoped fixtures with pytest-asyncio?","\nYes, but you must configure ",[18,1119,1120],{},"asyncio_mode = auto"," in ",[18,1123,144],{}," and ensure the event loop persists for the session. Use ",[18,1126,1127],{},"@pytest.fixture(scope='session', loop_scope='session')",[18,1129,1130],{},"pytest-asyncio >= 0.23",". This binds the fixture to a single loop that survives across all tests, preventing premature closure and enabling efficient resource sharing.",[14,1133,1134,1137,1139,1140,1143,1144,1146],{},[369,1135,1136],{},"Why does my async fixture raise 'ScopeMismatch' when injected into a sync test?",[18,1138,20],{}," enforces strict scope alignment. Async fixtures must be consumed by async tests or explicitly wrapped. Converting the test to ",[18,1141,1142],{},"async def"," resolves the mismatch. Alternatively, downgrade the fixture scope to match the test runner's lifecycle, or use a synchronous wrapper that calls ",[18,1145,78],{}," internally. Mixing sync and async scopes without explicit bridging violates pytest's dependency graph.",[14,1148,1149,626,1152,1154,1155,1094,1157,1160,1161,1163,1164,1166,1167,1169],{},[369,1150,1151],{},"How do I debug fixture teardown order in async tests?",[18,1153,629],{}," to visualize execution order. Use logging inside fixture setup\u002Fteardown blocks to trace loop IDs and execution timestamps. For profiling, integrate ",[18,1156,20],{},[18,1158,1159],{},"async-profiler"," or enable ",[18,1162,757],{}," to trace event loop blocking during teardown. Verify that teardown executes before the loop closes by checking ",[18,1165,374],{}," in the ",[18,1168,400],{}," block.",[14,1171,1172,1175,1176,1178,1179,1181,1182,1185,1186,1189],{},[369,1173,1174],{},"Does conftest.py hierarchy affect async fixture scoping?","\nYes. Async fixtures defined in parent ",[18,1177,92],{}," files inherit their scope relative to the test file's directory. Cross-directory scope resolution can cause unexpected loop recreation if ",[18,1180,31],{}," is not explicitly declared. Pin scopes explicitly and avoid implicit ",[18,1183,1184],{},"conftest"," inheritance for async resources. Isolate session-scoped async fixtures in a dedicated ",[18,1187,1188],{},"tests\u002Fconftest.py"," to prevent accidental scope dilution across subdirectories.",[1191,1192,1193],"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":222,"searchDepth":235,"depth":235,"links":1195},[1196,1197,1198,1199,1200,1201,1202],{"id":56,"depth":235,"text":57},{"id":109,"depth":235,"text":110},{"id":207,"depth":235,"text":208},{"id":459,"depth":235,"text":460},{"id":616,"depth":235,"text":617},{"id":893,"depth":235,"text":894},{"id":1110,"depth":235,"text":1111},"Integrating pytest-asyncio into a mature test suite requires a fundamental shift in how fixture lifecycles are conceptualized. Unlike synchronous testing, where fixture teardown aligns predictably with Python's garbage collection and pytest's internal cleanup hooks, asynchronous testing introduces an independent execution model governed by the asyncio event loop. The core challenge lies in decoupling fixture scope from test execution while ensuring that teardown operations execute within an active, valid event loop. Modern pytest-asyncio (v0.23+) resolves this through explicit loop_scope configuration and strict dependency injection validation, but misalignment remains a leading cause of RuntimeError: Event loop is closed and ScopeMismatch failures in production CI\u002FCD pipelines.","md",{},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fhow-to-scope-pytest-fixtures-for-async-tests",{"title":5,"description":1203},"advanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fhow-to-scope-pytest-fixtures-for-async-tests\u002Findex","1iKuFMx8Iigrc21YcZjg39StkiTOZyhERHI1uGWG5RI",1778004578579]