Pytest Fixtures Deep
Deep dive · part of Python Testing
Fixtures are reusable test setup. They can be scoped per function, class, module or session; they can yield to provide teardown; they can be parametrised; and they can depend on other fixtures.
pytest fixtures package setup and teardown into reusable dependencies injected by parameter name. Scope function, class, module, package, or session trades isolation for speed—database once per session, temp dir per test.
yield fixtures run teardown after the test; parametrize and indirect parametrization generate matrix tests without copy-paste. conftest.py shares fixtures across directories silently.
Production code combines this topic with logging, tests, and clear module boundaries so refactors stay safe when requirements grow.
Scope session speeds up expensive setup; function scope is default safest.
fixture autouse=True applies without naming in test signatures—use sparingly.
@pytest.mark.parametrize expands one test function across input tuples.
indirect=True passes param values into fixture before test receives result.
conftest.py fixtures are discovered upward in directory tree.
fixture finalizers via yield or request.addfinalizer ensure cleanup.
Factory fixtures return callables (def make_user(): ...) when each test needs unique instances. Dynamic scope is advanced; most teams pick static scope per resource cost.
Plugins like pytest-django and pytest-asyncio provide event_loop fixtures—read their docs to avoid conflicting custom loops.
Document session-scoped Docker fixtures with startup timeouts; pytest_plugins shares fixtures across internal packages.
Session-scoped containers need documented startup timeouts and health checks in conftest README.
Read the parent tutorial on pythondeck.com for runnable snippets, then reproduce them locally in a virtual environment with pinned dependency versions matching your deployment target.
When pairing with teammates, agree on one idiomatic pattern per concern—mixed styles in one repo slow reviews and invite subtle integration bugs during merges.
Session-scoped mutable fixtures shared and mutated by tests—order-dependent failures.
Forgetting yield teardown, leaking temp directories and ports.
Overusing autouse hiding dependencies tests actually need.
Parametrize without ids= making failure output unreadable.
Name fixtures after the resource they provide (db_session, client).
Keep expensive fixtures session-scoped with clear docs on mutability.
Use tmp_path fixture instead of manual mkdtemp in each test.
Run pytest --fixtures to document available fixtures for the team.
Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.
The program below demonstrates scope + teardown. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# conftest.py
import pytest, tempfile, shutil
@pytest.fixture(scope="session")
def workdir():
d = tempfile.mkdtemp()
yield d
shutil.rmtree(d)
# test_x.py
def test_uses_dir(workdir):
assert workdir
This sample walks through parametrise in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# Example: Parametrise
# Run in the REPL or save as a .py file and execute with python.
import pytest
@pytest.mark.parametrize("a, b, expected", [
(1, 1, 2),
(2, 3, 5),
(10, -3, 7),
])
def test_add(a, b, expected):
assert a + b == expected
Here is a hands-on illustration of indirect fixture. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.
# Example: Indirect fixture
# Run in the REPL or save as a .py file and execute with python.
import pytest
@pytest.fixture
def user(request):
return {"name": request.param, "role": "admin"}
@pytest.mark.parametrize("user", ["ada", "grace"], indirect=True)
def test_role(user):
assert user["role"] == "admin"
The program below demonstrates fixture scope. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# Fixtures run at function/class/module/session scope
import pytest # pytest
@pytest.fixture(scope="module") # once per module
def db_conn(): # fake connection
conn = {"id": 1} # stand-in object
yield conn # provide to tests
conn.clear() # teardown
def test_uses_conn(db_conn): # inject fixture
assert db_conn["id"] == 1 # assertion
# Run pytest on this file to execute
This sample walks through parametrize indirect in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# indirect=True passes param value into fixture
import pytest # pytest
@pytest.fixture # dynamic user fixture
def user(request): # request.param from indirect
return {"name": request.param} # build dict
@pytest.mark.parametrize("user", ["ada", "grace"], indirect=True) # cases
def test_name(user): # test
assert user["name"] in ("ada", "grace") # verify param applied
Related deep dives on Python Testing: