Pytest/asyncio/Explanation/yield vs return in async pytest fixtures
Properties
- tags
- Diataxis/Explanation, Pytest/Fixture
Explanation:
yieldvsreturnin asyncpytestfixtures- This document focuses on why and when you choose one pattern over the other rather than how to write the code step-by-step.
Purpose
The life-cycle principle
*If your fixture *owns something that must be cleaned up later, use
yield.- If it merely hands back data, use
return.** - The rule mirrors synchronous fixtures;
pytestjust happens to run the teardown code after theawait-friendlyyieldsection on the same event loop official docs.
When
returnis enoughWhen to prefer
yield- The fixture allocates or opens anything that must be closed, cancelled, or rolled back:
- You need guaranteed teardown, even if the test crashes.
- Example with an async context manager:
- Code after
yield(or afterasync with) executes as teardown inside the running loop, so you can stillawaitcleanup work safely — a detail confirmed in thepytest-asynciofixture reference docs.
Decision Table: yield vs return
How async generator fixtures work under the hood
pytestsees an async generator (async def …withyield).- It awaits the setup portion before
yield. - The yielded value is injected into dependent fixtures or tests.
- After the test finishes,
pytestresumes the generator so that any code afteryield(or the implicitasync withclose) runs as teardown on the same loop. - Because
pytestcontrols the coroutine, you never callawaiton a fixture in the test body; you only await inside the fixture itself. Accidentally returning a coroutine rather than its awaited result leads to "coroutine was never awaited" warnings, a common Stack Overflow pitfall discussion.
Composing fixtures (dependency injection)
- python
fixture async def db_engine() -> AsyncEngine: engine = create_async_engine(DB_URL) yield engine await engine.dispose() fixture async def db_session(db_engine): # <-- no `await` async with async_sessionmaker(db_engine)() as session: yield session - No
awaitondb_engineindb_session.pytestinjects the resolved engine. - Each fixture still awaits its own async work internally.
- The dependency graph is resolved once; results are cached per scope pytest docs.
Teardown patterns and edge cases
- Multiple cleanup steps: chain them after
yieldwithtry/finally. - Dynamic teardown: fall back to
request.addfinalizer()only if you must register callbacks at runtime; otherwise,yieldis clearer discussion.
- Multiple cleanup steps: chain them after
Summary