Pytest Fixtures and Parametrization
Two pytest features do most of the work in a well-structured Python test suite. Fixtures provide the things tests need — a database connection, a temporary directory, an authenticated client, a factory for test users — by name, with setup and teardown handled automatically and lifetimes controlled by scope. Parametrization runs the same test logic against many inputs, each reported as a separate test, so edge cases become one-line additions rather than copy-pasted functions.
Used well, these features keep tests short, isolated, and fast. Used carelessly, they create hidden dependencies, slow session-wide setup, and tests that pass alone but fail in parallel. This page covers the patterns that scale.
TL;DR
- Fixtures are functions decorated with
@pytest.fixture; tests receive them by naming parameters. yieldseparates setup from teardown; teardown runs even when the test fails.- Scopes — function, class, module, package, session — trade isolation for speed.
- Share fixtures through
conftest.pyfiles; closer definitions override outer ones. - Factory fixtures return functions that create objects with test-specific values.
@pytest.mark.parametrizeturns one test into many;pytest.paramadds ids and marks. Keep fixtures parallel-safe for pytest-xdist.
Quick Example
Shared fixtures in conftest.py: one database per parallel worker, a rolled-back transaction per test, and a user factory:
Tests that use them, including a parametrized table of cases:
Core Concepts
Fixtures as Dependency Injection
Pytest inspects a test's parameters and supplies a fixture for each name, resolving fixtures that themselves request other fixtures. The dependency graph is explicit in function signatures, and each test receives only what it asks for. pytest --fixtures lists available fixtures with their docstrings, and --setup-show shows setup and teardown order.
Scopes
A fixture may only request fixtures of the same or broader scope. Wider scopes are faster but share state between tests — keep session-scoped fixtures immutable or reset them per test.
Teardown With yield
Code after yield runs after the test, whether it passed or failed — the place to roll back transactions, delete files, and close connections. For multiple cleanup steps, request.addfinalizer or contextlib's ExitStack keeps cleanup correct even if one step fails.
conftest.py and Overriding
Fixtures defined in a conftest.py are available to every test in that directory and below, without imports. A fixture with the same name in a deeper conftest.py or in a test module overrides the outer one, which lets integration tests replace a fake client with a real one, for example. Plugins also provide fixtures this way.
Factory Fixtures
When tests need several objects with different attributes, a fixture that returns a function is more flexible than many fixed fixtures. The factory can record what it created for cleanup, apply defaults, and keep tests explicit about the values that matter to them. See Test Data and Environment Management.
Built-In Fixtures
See Mocking and Stubbing for when to replace dependencies.
Parametrization
Parametrizing Tests
@pytest.mark.parametrize takes argument names and a list of value tuples; each tuple becomes a separate test with its own pass or fail result. Give cases readable ids (via ids= or pytest.param(..., id=...)) so failures name the case. Attach marks to individual cases — pytest.param(value, marks=pytest.mark.xfail(reason="...")) — for known bugs or platform-specific cases. Stacking several parametrize decorators produces the cartesian product of their values.
Parametrizing Fixtures
@pytest.fixture(params=[...]) runs every test using the fixture once per parameter, available as request.param — useful for running the same tests against several backends, such as SQLite and PostgreSQL. Indirect parametrization (indirect=True) passes parameters from a test to a fixture, letting each case control how a resource is built.
Parallel Runs and Isolation
pytest-xdist runs tests in several worker processes, which exposes hidden coupling:
- Session fixtures run per worker, not once globally. Use
worker_idto create per-worker resources (databases, ports, directories), or a file lock to set up shared resources once. - Avoid shared mutable state between tests: global variables, module-level caches, shared files, and fixed ports.
- Do not depend on test order; plugins such as pytest-randomly shuffle order to reveal hidden dependencies.
- Use unique names for external resources (queues, buckets, rows) per test or worker.
Best Practices
Keep Fixtures Small and Composable
Build complex setups from small fixtures that request each other, rather than one fixture that does everything.
Prefer Function Scope Unless Setup Is Expensive
Isolation is worth a little speed. Widen scope only for genuinely expensive, read-only resources, and reset state between tests.
Name Parametrized Cases
Ids such as empty, too-long, and unicode-email make failures self-explanatory in CI output.
Make Dependencies Visible
Avoid autouse=True except for truly universal concerns (for example blocking network access); hidden fixtures make tests hard to understand.
Common Mistakes
Session-Scoped Mutable Fixtures
Copy-Pasted Tests Instead of Parameters
Fixed Resources Under xdist
FAQ
What is a pytest fixture?
A function decorated with @pytest.fixture that provides a value or resource to tests that list its name as a parameter, with optional teardown after yield and a configurable lifetime called scope.
What is the difference between fixture scopes?
Scope controls how often a fixture is created: once per test (function), class, module, package, or the whole session. Wider scopes are faster but share state across more tests.
Where should I put shared fixtures?
In conftest.py files. Pytest discovers them automatically, and fixtures defined in a directory's conftest.py are available to all tests in that directory and its subdirectories.
How do I run one test with many inputs?
Use @pytest.mark.parametrize with argument names and a list of values. Each set of values runs as a separate test case, and pytest.param lets you add ids and marks per case.
How do I keep fixtures safe with pytest-xdist?
Remember that session fixtures run once per worker, create per-worker resources using the worker_id fixture, avoid shared mutable state and fixed ports, and do not rely on test order.
Related Topics
- pytest — The pytest framework
- Python — The language
- Unit Testing — Testing principles
- Test Data and Environment Management — Data and isolation strategies
- Integration Testing — Tests with real dependencies
- Mocking and Stubbing — Replacing dependencies in tests