Pytest Fixture Scope: When to Share State and When to Reset
Pytest fixture scope is an engineering trade-off between test isolation and runtime. Learn how to choose function, module, or session scope without creating order-dependent tests.
13 Oct 2026, 20:53 UTC

Every test suite starts the same way: a few tests, a little setup, and a helper that grows until it is a small application. When setup is expensive—parsing a large fixture file, initializing a client, warming a cache—the temptation is to share it across every test to save seconds. Pytest makes sharing easy with fixture scopes, but the scope you pick is an engineering decision about state isolation, not just a performance flag.
Fixtures are dependency injection with a lifecycle
A pytest fixture is a function decorated with @pytest.fixture. When a test declares a parameter with the same name as the fixture, pytest runs the fixture and passes the result into the test. This is declarative dependency injection: the test states what it needs, and the framework handles construction and teardown.
The scope keyword controls how long the fixture lives. The default is function, meaning a fresh instance is created for every test. Broader scopes—class, module, and session—reuse the same instance across multiple tests. That reuse is the entire trade-off: broader scopes run setup fewer times, but they also let one test observe side effects from another.
A worked example: when sharing helps and when it hurts
Imagine a test module that needs a parsed configuration object. Building it is cheap, so function scope is fine:
import pytest
@pytest.fixture
def config():
return {"timeout": 30, "retries": 3}
def test_timeout(config):
assert config["timeout"] == 30
def test_retries(config):
assert config["retries"] == 3
Now suppose the config comes from a slow parser or a network call. Repeating that work for every test wastes time, so you widen the scope:
@pytest.fixture(scope="session")
def shared_config():
# Stand-in for an expensive one-time parse or fetch
data = {"timeout": 30, "retries": 3}
yield data
# Teardown runs once at the end of the session
If shared_config is read-only, session scope is a legitimate optimization. But if a test mutates the dictionary—say, setting config["timeout"] = 60—the next test inherits that change. The suite becomes order-dependent, and a failure in one test can cascade into unrelated ones.
Pytest also prevents a longer-lived fixture from depending on a shorter-lived one. If a session-scoped fixture requests a function-scoped fixture, collection fails with a ScopeMismatch error. That guardrail exists because the shorter-lived object would be destroyed before the longer-lived one finished with it.
Inspecting scope and choosing deliberately
You do not have to guess which fixtures exist or how they are scoped. Run the following command in your project root or test directory:
pytest --fixtures
This lists every available fixture, its scope, and the file where it is defined. To watch setup and teardown happen relative to test execution, run:
pytest -vv
The verbose output shows fixture setup lines interleaved with test results, making it obvious when a session-scoped resource is created once or a function-scoped one is rebuilt each time.
Use those observations to apply a few rules. Default to function scope for anything mutable. Use module scope for read-only data that is expensive to build and only needed by one test file. Reserve session scope for genuinely immutable, expensive resources such as a read-only connection pool or a compiled schema. If you must share mutable state, wrap it in a factory fixture that returns a new copy, or add an explicit reset fixture that runs before each test.
Fixture scope is static; you cannot change it per test invocation without parameterization or a plugin. Circular imports between conftest.py files can also cause collection errors that look unrelated to fixtures. If pytest --fixtures fails to list a fixture you expect, check for import cycles in your fixture modules first.
To verify your choice, run pytest -vv and confirm that session-scoped setup appears once while function-scoped setup repeats. Then run the suite in a random order—using a plugin such as pytest-randomly if available—to catch hidden state leakage. If tests pass individually but fail in a different order, your scope is too broad.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.