Python testing · 7 min read
Pytest Fixtures Without the Confusion: A Working Mental Model
A fixture supplies a known starting point
Think of a fixture as a named promise: before this test runs, provide this object or state. The test asks for the fixture by parameter name, and pytest resolves it. That is dependency injection for test setup.
The best fixtures make a test easier to read. A fixture named authenticated_client communicates more than fifteen lines of login setup inside each test.
Choose the narrowest useful scope
Function scope is the safest default because every test receives fresh state. Module, class, package, and session scopes can reduce expensive setup, but they also increase the chance that tests influence one another.
Use a broader scope only when setup is genuinely expensive and the shared object is read-only or reliably reset.
Return values and teardown
A fixture can return a value normally. When cleanup is required, use yield: code before yield prepares the resource, and code after yield releases it even when the test fails. This is ideal for temporary files, database records, browser contexts, and server processes.
- Keep one fixture responsible for one resource.
- Compose fixtures instead of creating a giant all-purpose fixture.
- Avoid autouse unless the setup truly applies to every test.
- Name fixtures by the state they provide, not the steps they perform.
Spot fixture misuse early
If a test's result depends on which test ran first, shared state has leaked. If understanding a test requires opening five fixture files, abstraction has gone too far. If every test overrides most fixture data, create a small factory instead.
Fixtures should remove noise while preserving intent. The test must still tell the reader which behavior matters and why the assertion is meaningful.