Python Pytest Fixtures Scope and conftest
python pytest fixtures scope and conftest: Learn how pytest fixture scope controls setup frequency and how conftest.py shares fixtures across test files, with guidance...
Understanding python pytest fixtures scope and conftest comes down to two questions: how often a fixture's setup code runs, and where that fixture is visible. The scope parameter answers the first question. The conftest.py file answers the second. Getting both right keeps test suites fast without making tests depend on each other's state.
How Fixture Scope Controls Setup and Teardown
A pytest fixture is a function that provides a value to any test that requests it. The scope parameter controls how often the fixture's setup code runs and when its teardown runs. The default is function, meaning the fixture is created fresh for every test that requests it.
import pytest @pytest.fixture def temp_file(tmp_path): path = tmp_path / "data.txt" path.write_text("initial") yield path path.unlink(missing_ok=True)
With the default scope, each test gets its own temp_file. Setup runs before the test, teardown runs after. This is the safest default because tests cannot leak state into each other through the fixture.
The other scopes are class, module, and session. A session-scoped fixture runs once for the entire test run. A module-scoped fixture runs once per test module.
@pytest.fixture(scope="session") def database(): db = create_database() yield db db.drop()
The fixture above creates the database once and tears it down after the last test finishes. Any test that requests database receives the same object for the whole session.
Sharing Fixtures Across Test Files with conftest.py
Fixtures defined inside a test module are only visible to that module. To share a fixture across multiple test files, define it in a conftest.py file in the same directory or a parent directory.
# conftest.py import pytest @pytest.fixture(scope="session") def api_client(): client = create_api_client() yield client client.close()
Any test file under that directory can request api_client without importing it. pytest discovers conftest files automatically during collection. This is the main reason conftest exists: it is a discovery point for fixtures, hooks, and command-line options that apply to a directory tree.
conftest.py files are themselves collected as part of the test session, so fixtures defined there are available to every test in that subtree. A fixture defined in a conftest at the project root is visible to the entire suite, while a conftest inside a subdirectory limits visibility to that subtree.
Choosing Between Function, Class, Module, and Session Scope
The choice of scope is a tradeoff between setup cost and test isolation.
Function scope is correct when the fixture holds mutable state that tests modify. If two tests mutate the same object, the second test sees the first test's changes, which makes failures hard to attribute.
Class scope is useful when a group of tests in one class can share an expensive object without mutating it. Module scope extends that idea to an entire test file.
Session scope is appropriate for resources that are expensive to create and safe to share, such as a database connection, a compiled binary, or an HTTP client. The risk is that tests become order-dependent: if one test corrupts the shared resource, later tests fail for reasons unrelated to their own logic.
Use this rule: start with function scope, and widen the scope only when you can prove the fixture is read-only or when the setup cost justifies sharing.
Overriding Fixtures in Nested conftest Directories
pytest resolves fixtures by directory depth. A fixture defined in a more specific conftest overrides one with the same name in a parent directory.
# tests/conftest.py @pytest.fixture def api_client(): return RealClient() # tests/unit/conftest.py @pytest.fixture def api_client(): return MockClient()
Tests under tests/unit/ receive the mock client. Tests elsewhere in tests/ receive the real client. This is the standard pattern for swapping infrastructure dependencies in a subset of the test suite.
The override applies to the entire subtree, so a fixture defined in tests/unit/conftest.py overrides the parent for all tests under tests/unit/, even those in nested subdirectories. The same mechanism lets you replace a session-scoped fixture with a function-scoped one in a specific directory, which is useful when a subset of tests needs fresh state.
Autouse Fixtures and How Scope Changes Their Behavior
An autouse fixture runs without being explicitly requested. It is useful for setup that every test needs, such as environment variables or a temporary directory.
@pytest.fixture(autouse=True) def set_environment(): os.environ["APP_ENV"] = "test" yield os.environ.pop("APP_ENV", None)
Autouse fixtures respect the same scope rules. An autouse session-scoped fixture runs once for the whole session, while an autouse function-scoped fixture runs before every test. The combination of autouse=True and scope="session" is common for one-time global setup that does not depend on per-test state.
One caveat: an autouse fixture that yields and performs teardown runs its teardown at the end of its scope, not after each test. If you need per-test cleanup, keep the autouse fixture at function scope. A session-scoped autouse fixture that mutates global state can silently affect every test in the run, so it should only set up values that tests treat as read-only.
Scope, Performance, and Test Isolation Tradeoffs
Widening scope reduces setup time but increases coupling between tests. A session-scoped database fixture can make the suite much faster, but a single test that leaves the database in an unexpected state breaks every later test that depends on it.
The practical compromise is to scope expensive fixtures at session or module level and design tests so they do not mutate shared state. If tests must mutate state, use function scope and accept the setup cost.
Another consideration is parallel execution. Tools like pytest-xdist run tests in separate workers. A session-scoped fixture runs once per worker, not once per overall run. If the fixture creates a resource that cannot be shared across processes, each worker needs its own instance, and the fixture must be written to handle that.
When a fixture is expensive and read-only, session scope is the right choice. When it is expensive and mutable, prefer module scope and isolate mutations inside individual tests. When it is cheap or stateful, function scope is the safest default. The conftest file gives you a single place to adjust scope later without touching every test that uses the fixture.