Fixtures are pytest's answer to a question every test suite eventually asks: where does the stuff my tests need come from? A fixture is a function that builds something — a database connection, a temp directory, a logged-in client — and pytest calls it for you whenever a test asks for it by name. Once that single idea clicks, everything else in this tutorial — yield teardown, scopes, conftest.py, parametrisation — is a variation on it.
Without fixtures, setup code gets copy-pasted. Here is a suite three weeks into its life:
def test_add_item():
db = Database(":memory:")
db.migrate()
cart = Cart(db)
cart.add("apple", 2)
assert cart.count() == 2
def test_remove_item():
db = Database(":memory:")
db.migrate()
cart = Cart(db)
cart.add("apple", 2)
cart.remove("apple")
assert cart.count() == 0
The first three lines of each test are identical, and they will be identical in the next forty tests too. When Database grows a required argument, you edit every one of them.
The classic xUnit fix is a setUp method on a test class. That removes the duplication but creates a different problem: every test in the class gets the same setup whether it needs it or not, and the setup grows into a grab-bag serving the union of all tests. Fixtures invert the relationship. Each test declares what it needs, and pytest builds exactly that — nothing more.
Define a fixture with the @pytest.fixture decorator. Use it by naming it as a test argument:
import pytest
@pytest.fixture
def cart():
db = Database(":memory:")
db.migrate()
return Cart(db)
def test_add_item(cart):
cart.add("apple", 2)
assert cart.count() == 2
def test_remove_item(cart):
cart.add("apple", 2)
cart.remove("apple")
assert cart.count() == 0
When pytest collects test_add_item, it sees a parameter called cart, looks for a fixture with that exact name, calls it, and passes the return value in. The matching is purely by name — there is no import, no registration, no decorator on the test. This surprises people coming from dependency-injection frameworks, but it is the whole mechanism.
By default each test gets a fresh call to the fixture, so test_add_item and test_remove_item each receive their own Cart and cannot contaminate each other.
Returning a value covers setup. For teardown, replace return with yield:
@pytest.fixture
def db():
conn = Database("test.db")
conn.migrate()
yield conn
conn.close()
Everything before the yield runs before the test; everything after it runs afterwards — including when the test fails. The yielded value is what the test receives.
pytest also offers request.addfinalizer() for registering teardown callbacks, and older tutorials lean on it. Prefer yield for most cases: the teardown sits directly under the setup, the fixture reads top to bottom like the sequence of events it describes, and cleanup code shares the fixture's local variables without closures. addfinalizer earns its keep only when you need to register cleanup before a risky setup step completes, or several independent finalizers that must each run even if another raises.
One honest caveat about yield: if the code before the yield raises, the code after it never runs. If your fixture does two risky setup steps, split it into two fixtures — each one's teardown is then guaranteed independently.
By default a fixture runs once per test (scope="function"). You can widen that: class, module, package, or session.
@pytest.fixture(scope="session")
def engine():
engine = create_engine(POSTGRES_URL)
run_migrations(engine) # ~8 seconds
yield engine
engine.dispose()
This is where scope earns real money. If running migrations takes 8 seconds and your suite has 500 tests touching the database, function scope costs you over an hour per run; session scope costs 8 seconds total. Expensive, effectively read-only resources — a migrated schema, a compiled model, a Docker container — are the textbook candidates.
The danger is shared state. A session-scoped object lives across every test, so anything one test mutates, the next test inherits. That produces the worst kind of failure: tests that pass alone and fail in the suite, or fail only in a particular order. The standard resolution is to pair scopes — an expensive session-scoped resource wrapped by a cheap function-scoped fixture that guarantees isolation:
@pytest.fixture
def db_session(engine):
connection = engine.connect()
transaction = connection.begin()
yield Session(bind=connection)
transaction.rollback()
connection.close()
Migrations run once; every test still gets a clean slate because its transaction is rolled back.
A fixture defined inside a test file is visible only in that file. To share it, move it into a file named conftest.py. pytest discovers this file automatically — you never import it — and its fixtures become available to every test in the same directory and all directories below it.
tests/
├── conftest.py # engine, db_session → available everywhere
├── test_cart.py
└── api/
├── conftest.py # auth_headers → available only under api/
└── test_orders.py
Discovery works top-down: when pytest runs tests/api/test_orders.py, it loads the root conftest.py, then tests/conftest.py, then tests/api/conftest.py. If two levels define a fixture with the same name, the nearest one wins, so a subdirectory can override a general fixture with a specialised version — useful, and also a source of confusion if you do it by accident.
A practical rule: keep the root conftest.py small and generic, and define specialised fixtures in the deepest directory that needs them. A 600-line root conftest that every test in the repository depends on is a maintenance liability.
You can experiment with the plain-Python parts of these examples — closures, generators, the yield pattern — in the free browser editor; running a full pytest suite still needs a local install.
A fixture can request another fixture the same way a test does — by naming it as a parameter:
@pytest.fixture
def user(db_session):
u = User(email="[email protected]")
db_session.add(u)
db_session.commit()
return u
@pytest.fixture
def logged_in_client(client, user):
client.login(user.email, "password")
return client
A test that asks for logged_in_client transparently gets the whole chain: engine, session, user, client, login. Teardown runs in reverse order, so dependencies outlive their dependents.
One constraint: a fixture may only depend on fixtures of equal or wider scope. A function-scoped fixture can use a session-scoped one; the reverse raises a ScopeMismatch error, because a per-session object cannot sensibly depend on something rebuilt for every test.
A fixture can carry parameters. Every test that uses it then runs once per parameter:
@pytest.fixture(params=["sqlite", "postgres"])
def db(request):
if request.param == "sqlite":
yield make_sqlite()
else:
yield make_postgres()
Twenty tests using db become forty test runs, each labelled with its parameter in the output. Compare that with parametrising a single test:
@pytest.mark.parametrize("raw, expected", [("1", 1), ("-3", -3), ("007", 7)])
def test_parse_int(raw, expected):
assert parse_int(raw) == expected
The rule of thumb: parametrise the fixture when you are varying the world the tests run in — every consumer should be exercised against sqlite and Postgres alike. Parametrise the test when you are varying the inputs and expected outputs of one specific behaviour. Fixture params fan out broadly and implicitly; mark params stay local and explicit. Reaching for params= to feed test data into a single test is a sign you wanted @pytest.mark.parametrize.
autouse=True applies a fixture to every test in its scope without anyone requesting it:
@pytest.fixture(autouse=True)
def no_network(monkeypatch):
monkeypatch.delattr("socket.socket")
This is genuinely useful for suite-wide invariants — blocking network access, freezing time, isolating environment variables. It is also the easiest way to make a test suite incomprehensible. An autouse fixture is invisible at the test site: a reader of the test cannot tell that setup happened, and a test moved to a different directory silently loses (or gains) behaviour. Prefer explicit fixture arguments; when you do use autouse, define it as narrowly as possible and give it a name that states what it does.
pytest ships useful fixtures you never have to define. Three carry most of the weight:
tmp_path — a fresh, empty pathlib.Path directory unique to the test. No manual cleanup; pytest keeps the last few runs for debugging and deletes older ones.monkeypatch — safely patch attributes and environment variables (setattr, setenv, delenv, chdir); every change is automatically undone after the test.capsys — captures stdout and stderr so you can assert on printed output.def test_writes_report(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("REPORT_DIR", str(tmp_path))
generate_report()
assert (tmp_path / "report.csv").exists()
assert "1 report written" in capsys.readouterr().out
Also worth a look when you need them: caplog for asserting on log records, and request for introspecting the requesting test from inside a fixture.
Calling a fixture directly. cart() in test code raises Fixture "cart" called directly. Fixtures are requested, never called; pytest owns their lifecycle. If you need the construction logic as an ordinary function, extract it into a plain helper and have the fixture call the helper.
Scope mismatch. ScopeMismatch: You tried to access the function scoped fixture X with a session scoped request object means a wide-scoped fixture depends on a narrower one. Either widen the dependency or narrow the dependent — usually the latter is safer.
Hidden coupling through autouse. A test passes only because an autouse fixture in a conftest.py two directories up patched something. Move the file, and it fails for reasons the file itself cannot explain. If a test's correctness depends on setup, that setup should be visible in its signature.
Mutable session-scoped state. A session fixture that yields a dict or list is an invitation for one test to mutate it and a later test to inherit the damage. Symptoms are order-dependent flakiness. Keep wide-scoped fixtures effectively immutable, or pair them with a function-scoped fixture that resets state — the transaction-rollback pattern above is exactly this.
Reading about fixtures is the easy half. The graded exercises for this topic — writing yield fixtures, fixing a scope leak, refactoring duplicated setup into conftest.py — are in the interactive lesson at the top of this page, which is part of Pro. If you are new to the track, the first lessons are free and need nothing installed.