← Python Code Setup & Execution
Browse Python Concepts

pytest Setup and Fixture Scope

Mental Model

Think of a function-scoped fixture as a fresh, clean setup created uniquely for each test function, guaranteeing isolation. A session-scoped fixture, however, is a single shared resource that's set up once and potentially modified by all tests in the session, like a shared whiteboard that everyone writes on without erasing.

Rule: Always default your pytest fixtures to scope="function" to prevent accidental state mutation leaks between individual tests.

The Setup

You are writing unit tests for an API. You define a pytest fixture to yield a database connection, but notice that mutations made in one test leak into downstream tests.

What Does This Print?

Broken code
Python
import pytest

# Simulating a mock database with session scope to "save execution time"
@pytest.fixture(scope="session")
def db_conn():
    return {"users": ["alice", "bob"]}

def test_delete_user(db_conn):
    db_conn["users"].remove("alice")
    assert "alice" not in db_conn["users"]

def test_list_users(db_conn):
    # This test relies on the original initial state of the database
    assert "alice" in db_conn["users"]
What happens when you run pytest on this file? Will both tests pass?

The Output

What actually happens
def test_list_users(db_conn): > assert 'alice' in db_conn['users'] E AssertionError: assert 'alice' in ['bob']

The second test fails because the shared state in the db_conn fixture was mutated by the first test. Since the fixture scope is set to session, pytest initializes the fixture dictionary exactly once and passes a reference to that same dictionary object to every test that requests it. Mutations from individual tests accumulate, causing tests to depend on execution order and leak state across the test suite. This makes tests highly fragile and guarantees failures when running parallel test execution suites. To prevent state leaks, you must manage fixture scopes appropriately, ensuring that mutable data structures are regenerated for each unit test.

Why Python Does This

Pytest manages fixture lifetimes using a caching registry. When a test function references a fixture, pytest looks up its scope configuration. If a fixture has a broad scope, the framework runs the generator once, saves the output value in memory, and returns that exact object reference for all matching test cases. Because Python dictionaries, lists, and custom database connections are mutable objects, any in-place mutation alters the single cached instance in memory. CPython passes these objects by reference, meaning no automatic isolation or deep copying occurs between test cases unless explicitly coded inside the fixture itself.

The Fix

Corrected pattern
Python
import pytest

# function scope: fresh state created before each test, torn down after
@pytest.fixture(scope="function")
def db_conn():
    conn = {"users": ["alice", "bob"]}
    yield conn
    # Teardown: runs after every test, even if the test fails
    conn["users"].clear()  # reset state — prevents leaking into other fixtures

def test_delete_user(db_conn):
    db_conn["users"].remove("alice")
    assert "alice" not in db_conn["users"]

def test_list_users(db_conn):
    # gets a fresh {"users": ["alice", "bob"]} — unaffected by previous test
    assert "alice" in db_conn["users"]

Setting scope="function" ensures that the fixture's setup code is run before each test function that requests it, and its teardown (if any) runs after. This creates a completely isolated and pristine instance of the fixture for every test, preventing state from one test from affecting another.

How This Fails in Real Systems

A SaaS startup's CI pipeline began randomly failing tests in pull requests. Local developers couldn't reproduce the failures because they ran tests individually. The issue was a session-scoped mock Redis client mutating auth tokens. It took a week to discover that parallel execution using pytest-xdist caused random, flaky test failures because of shared global state in broad-scope fixtures.

Key Takeaway

Always default your pytest fixtures to scope="function" to prevent accidental state mutation leaks between individual tests.
Common mistake: Developers mistakenly use broad fixture scopes like session or module to save setup/teardown time, failing to account for mutable shared state leaking between tests and causing unexpected failures.