Q&A library Python

How do I share test fixtures across several test files with pytest?

Python Advanced conftestfixturespytest

Short answer

Put shared fixtures in a conftest.py and pytest finds them automatically, with no import needed. Use the scope argument to control how often an expensive fixture is rebuilt.

The short version

Put shared fixtures in a file called conftest.py in your tests folder. Pytest discovers it automatically, so any test in that folder or below can use those fixtures by name, with no import. Use scope to control how often an expensive fixture is created.

The conftest file

# tests/conftest.py
import pytest
import pandas as pd

@pytest.fixture
def sample_orders():
    return pd.DataFrame({
        "city": ["Leeds", "Leeds", "Bath"],
        "amount": [10, 20, 5],
    })

Any test file in tests/ can now simply ask for it.

# tests/test_summary.py
def test_total_by_city(sample_orders):
    result = summarise(sample_orders)
    assert result.loc["Leeds", "amount"] == 30

No import, no boilerplate. Pytest matches the parameter name to the fixture and passes it in.

Why conftest rather than imports

Importing fixtures from a helper module technically works but is fragile, because pytest's fixture machinery expects to discover them. conftest.py is the sanctioned mechanism, and it also scopes fixtures to a directory: a conftest.py inside tests/integration/ provides fixtures only to the tests below it, which keeps things tidy in a large suite.

Controlling how often it runs

By default, a fixture runs once per test, which is right for a small frame you want fresh each time. For something expensive, that repetition is wasteful.

@pytest.fixture(scope="session")
def database_connection():
    con = connect()
    yield con
    con.close()

scope="session" creates it once for the entire run. Other scopes are function (the default), class and module.

The danger of a wide scope

A shared, mutable fixture is a trap. If one test modifies a session-scoped DataFrame, later tests see the change, and you get failures that depend on the order the tests ran in, which is maddening to debug.

So: use a wide scope only for things that are expensive and read-only, or that manage their own cleanup. Keep mutable test data at function scope, where every test gets a fresh copy.

Cleaning up with yield

A fixture that yields runs its teardown after the test finishes, whatever the outcome.

@pytest.fixture
def temp_file(tmp_path):
    path = tmp_path / "data.csv"
    path.write_text("city,amount\nLeeds,10\n")
    yield path
    # nothing to clean up, tmp_path is removed automatically

tmp_path is a built-in fixture giving a fresh temporary directory per test, which is the right way to test file reading and writing without leaving debris behind.

Fixtures can use other fixtures

A fixture may request another, which lets you compose test setup in layers rather than duplicating it.

@pytest.fixture
def summarised(sample_orders):
    return summarise(sample_orders)

This keeps each fixture small and focused, and it is the mechanism that makes pytest's approach scale to a large suite without turning into a tangle of setup code.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project