What this usually means
Pytest fixture scope errors occur when the declared scope of a fixture (function, class, module, session) conflicts with the scopes of fixtures it depends on or the tests it serves. The most common cause is a lower-scoped fixture (e.g., function) depending on a higher-scoped fixture (e.g., session) that is not designed for shared state. Another typical pattern is when a session-scoped fixture returns a mutable object (like a list or dict) that gets mutated by a test, causing state leakage. Pytest enforces scope hierarchy: you cannot request a fixture with a broader scope from a fixture with a narrower scope. The error message 'ScopeMismatch' is explicit, but sometimes the symptom is silent data corruption.
The first ten minutes — establish facts before touching code.
- 1Run pytest with `-v` and `--setup-show` to print fixture setup and teardown order. Look for fixtures being set up/teardown at unexpected times.
- 2Check the error message: if it's 'ScopeMismatch', identify which fixture has the conflict. Use `pytest --fixtures` to list all fixture scopes.
- 3Isolate the failing test: run it alone with `pytest tests/test_file.py::test_name -v`. If it passes, it's likely a scope/state issue.
- 4Add a `print` or `logging` inside the fixture to see when it's called. Use `pytest -s` to capture stdout.
- 5If using `pytest-xdist`, try running with `-n 0` (no parallel) to see if concurrency is the issue.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchconftest.py files: check all fixture definitions and their `scope` parameter.
- searchTest files: look for fixtures that override conftest fixtures or request other fixtures.
- search`pytest --fixtures` output: shows the scope and location of every fixture.
- search`pytest --setup-plan` output: lists the order of setup and teardown for each test.
- searchAny fixture that returns a mutable object (list, dict, set, custom class) – especially session-scoped ones.
- searchTest that modifies a fixture's return value directly (e.g., `my_fixture.append(1)`).
Practical causes, not theory. These are the things you will actually find.
- warningA session-scoped fixture returns a mutable object that tests modify, causing cross-test contamination.
- warningA function-scoped fixture depends on a module-scoped fixture that has a different teardown order expectation.
- warningMisplaced `scope` parameter: e.g., `@pytest.fixture(scope='class')` inside a class that doesn't use class-based test structure.
- warningOverriding a fixture in a child conftest with a different scope that conflicts with parent fixture usage.
- warningUsing `pytest.mark.usefixtures` with a fixture that has a narrower scope than the test's scope.
- warningDynamic fixture requests via `request.getfixturevalue()` that inadvertently mix scopes.
Concrete fix directions. Pick the one that matches your root cause.
- buildIf a session-scoped fixture must be mutable, make it return a factory function or use a copy: `return list(original)`.
- buildChange the scope of the dependent fixture to match or be broader than the dependency. E.g., if function depends on session, make it session too.
- buildRestructure fixtures to avoid dependency chain mismatches: extract shared setup into a separate session fixture.
- buildUse `@pytest.fixture(scope='module')` for state that should persist across a module but reset between modules.
- buildFor parallel execution with xdist, ensure session fixtures are idempotent or use `scope='session'` with `autouse=False`.
- buildRefactor tests to not modify fixture objects: prefer immutable data or use `.copy()` before mutation.
A fix you cannot prove is a guess. Close the loop.
- verifiedRun the full test suite with `pytest -v --setup-show` and confirm fixture setup/teardown occurs at expected scopes.
- verifiedRun the previously failing tests in multiple orders: `pytest --random-order` or `pytest --ff` (failed first).
- verifiedInsert assertions inside fixtures to verify state is clean before each test.
- verifiedAdd a pytest marker `@pytest.mark.xfail(strict=True)` to a known failing test and ensure it fails consistently, then verify fix removes the mark.
- verifiedUse `pytest --co` (cache clear) and run from scratch to ensure no cached state from previous runs.
Things that make this bug worse or harder to find.
- warningDon't blindly change all fixtures to `scope='session'` – it can hide race conditions and make tests interdependent.
- warningAvoid modifying fixture return values inside tests – create a copy or use a factory.
- warningDon't ignore 'ScopeMismatch' errors by suppressing them; always fix the root cause.
- warningNever use `scope='class'` if your tests are not organized in classes; it may cause unexpected behavior.
- warningDon't rely on fixture teardown order for cleanup that should happen unconditionally – use `yield` with try/finally.
- warningAvoid nesting fixtures with conflicting scopes in conftest files without clear documentation.
The Case of the Disappearing Database Records
Timeline
- 09:15CI pipeline fails on test_db_integration.py: 'test_user_creation' passes alone but fails when run after 'test_user_deletion'.
- 09:20I run `pytest test_db_integration.py -v --setup-show` and see session-scoped fixture 'db_session' is set up once and torn down after all tests.
- 09:25Check 'db_session' fixture: returns a SQLAlchemy session connected to a transaction that rolls back after each test. But the fixture is session-scoped, so rollback happens at session end.
- 09:30Test 'test_user_deletion' deletes a user and commits. Next test 'test_user_creation' expects the user to exist but it's gone.
- 09:35I realize the fixture should be function-scoped to rollback per test, but it's session-scoped for performance.
- 09:40Fix: change `db_session` scope to 'function' and wrap the session in a transaction that rolls back on teardown.
- 09:45Run the full suite: `pytest -v --setup-show` confirms each test gets its own session. All tests pass.
- 09:50Commit fix and push. CI passes.
The incident started with a failing CI pipeline. The test suite had been running fine for weeks, but a new test `test_user_deletion` was added that deleted a user record. The existing `test_user_creation` expected that user to be present. Both tests used a session-scoped database fixture that was supposed to roll back after each test, but because the fixture was session-scoped, the rollback only happened once at the end of the session.
I first confirmed the behavior by running `pytest --setup-show`. It showed that the `db_session` fixture was set up only once for the entire module. I then inspected the fixture code in conftest.py: it created a SQLAlchemy session and bound it to a transaction. The `yield` returned the session, and after the yield, it called `session.rollback()`. Because it was session-scoped, the rollback only executed after all tests in the session completed.
The root cause was a scope mismatch: the fixture was designed to provide a clean state per test (function scope) but was declared as session scope for performance. The fix was straightforward: change `scope='session'` to `scope='function'`. This ensured each test got a fresh transaction that rolled back on teardown. After the fix, all tests passed consistently. The lesson: always match fixture scope to the intended isolation level, and don't optimize prematurely for performance at the cost of correctness.
Root cause
Session-scoped database fixture that returned a mutable SQLAlchemy session; tests modified the database state and the rollback only happened at session teardown, causing cross-test contamination.
The fix
Changed the fixture scope from 'session' to 'function' so each test gets its own transaction that rolls back after the test.
The lesson
Fixture scope must match the required isolation level. Performance optimizations like session-scoped fixtures should only be used when the fixture state is immutable or properly reset between tests.
Pytest defines four scopes for fixtures: function (default), class, module, and session. The scope determines how often the fixture is set up and torn down. Function scope runs for each test, class scope runs once per test class, module scope once per module, and session scope once per entire test run.
The key constraint is that a fixture can only request fixtures of the same or broader scope. For example, a function-scoped fixture can request a session-scoped fixture, but not vice versa. Violating this causes a 'ScopeMismatch' error with a clear message: 'ScopeMismatch: You tried to access the fixture ... with a lower scope'.
To inspect the current scopes, run `pytest --fixtures`. This lists all fixtures with their scope, location, and docstring. Use it to audit your conftest files.
The `--setup-show` flag prints the setup and teardown of fixtures for each test. It shows when fixtures are created and destroyed. This is invaluable for debugging scope issues.
Example output: `SETUP S db_session`, `teardown S db_session`. If you see 'S' (session) only once for multiple tests, that confirms session scope. To see per-test teardown, look for 'F' (function) scope fixtures.
Combine with `-v` to see test names. If a fixture is torn down after the last test in a module, it's module or session scope.
Frequently asked questions
What does 'ScopeMismatch' mean exactly?
It means a fixture with a narrower scope (e.g., function) is trying to depend on a fixture with a broader scope (e.g., session) in a way that pytest cannot resolve. For example, a session-scoped fixture cannot depend on a function-scoped fixture because the function fixture would need to be created multiple times but the session fixture only once. The error message includes the names of the conflicting fixtures.
Can I have a session-scoped fixture that returns a mutable object safely?
Yes, but you must ensure that tests do not mutate the object, or you must reset it between tests. Common patterns: return a copy (e.g., `return original_list.copy()`), use a factory that creates a new object each time, or design the fixture to provide an immutable view. For database sessions, use a transaction that rolls back.
Why does my test pass alone but fail in a suite?
This is a classic symptom of scope-related shared state. When run alone, the fixture is set up fresh for that test. In a suite, earlier tests may have modified the fixture's state. Check for session or module-scoped fixtures that return mutable objects. Use `--setup-show` to see fixture lifetimes.
How do I fix a fixture that has conflicting scope dependencies?
Identify the fixture that is causing the mismatch. Options: change the dependent fixture's scope to match or be broader, or refactor the dependency chain. For example, if a function fixture needs session data, make the function fixture session-scoped as well, or extract the shared data into a session fixture and request it from the function fixture.
What is the best practice for fixture scopes in a large test suite?
Start with function scope for most fixtures to ensure isolation. Only promote to module or session scope when you have measured a performance need and can guarantee immutability or proper reset. Document the scope and any assumptions in the fixture docstring. Use conftest.py to organize shared fixtures and keep scope consistent.