Pytest dependency injection is less about injecting dependencies and more about requesting them from a shared context.

Let’s see it in action. Imagine you have a database connection and a user object you need for multiple tests.

# conftest.py
import pytest

@pytest.fixture
def db_connection():
    print("\nEstablishing DB connection...")
    conn = {"users": {}}
    yield conn
    print("\nClosing DB connection...")

@pytest.fixture
def user_data():
    return {"username": "alice", "email": "alice@example.com"}

# test_users.py
def test_create_user(db_connection, user_data):
    db_connection["users"][user_data["username"]] = user_data
    assert user_data["username"] in db_connection["users"]
    assert db_connection["users"][user_data["username"]]["email"] == "alice@example.com"

def test_get_user(db_connection, user_data):
    db_connection["users"][user_data["username"]] = user_data
    retrieved_user = db_connection["users"].get(user_data["username"])
    assert retrieved_user["email"] == "alice@example.com"

When you run pytest, you’ll see the output:

Establishing DB connection...
.
Closing DB connection...
Establishing DB connection...
.
Closing DB connection...

Notice how the db_connection fixture is set up and torn down for each test function by default. This is the core mechanism: fixtures are functions that pytest calls when another fixture or test function requests them by name in its signature. Pytest then provides the return value (or yielded value) of that fixture to the requester.

The problem this solves is managing shared state and setup/teardown logic across tests without resorting to global variables or complex inheritance. Fixtures provide a declarative way to define these dependencies. The db_connection and user_data fixtures in the example above are like building blocks. test_create_user requests db_connection and user_data. Pytest sees these names, finds the corresponding fixtures, executes them (if they haven’t been executed already for the current scope), and passes their results to the test function.

The yield keyword in db_connection is crucial for teardown. Code before yield runs as setup, and code after yield runs as teardown. This ensures resources are cleaned up properly after tests complete. The scope of a fixture (function, class, module, session) determines how often it’s set up and torn down. By default, fixtures have "function" scope, meaning they run once per test function. You can explicitly set the scope using @pytest.fixture(scope="module") or @pytest.fixture(scope="session") to share a fixture across multiple tests, reducing redundant setup.

The real magic is how fixtures can depend on each other. You can create a user fixture that uses the db_connection fixture:

# conftest.py (continued)
@pytest.fixture
def user(db_connection, user_data):
    db_connection["users"][user_data["username"]] = user_data
    print(f"\nCreating user {user_data['username']} in DB...")
    return user_data

# test_users.py (modified)
def test_create_user_again(user): # No need to explicitly request db_connection or user_data
    assert user["username"] in db_connection["users"] # Oh wait, db_connection isn't available here directly
    # To access db_connection here, you'd need to request it again,
    # but pytest's caching ensures it's the same instance if scope matches.
    # A better way is to test the *effect* of the 'user' fixture.
    pass # This test would need refinement to actually use db_connection

def test_retrieve_created_user(user):
    # The 'user' fixture has already created the user in the db_connection
    # implicitly via its own fixture dependencies.
    # We can now test that the user is available.
    # To access db_connection here, we'd request it:
    # def test_retrieve_created_user(user, db_connection):
    #     assert user["username"] in db_connection["users"]
    # But often you just test the outcome:
    assert user is not None

If you run pytest with the user fixture, you’ll see the Creating user alice in DB... message. Pytest automatically resolves the dependencies: to run user, it first needs to run db_connection and user_data. This creates a powerful, composable system for managing test infrastructure.

When a fixture is requested by multiple tests within the same scope (e.g., function scope), pytest caches its result. It only executes the setup code once and reuses the same object for all requesting tests. This is key for performance. For db_connection, this means the connection is established once per test function. If you change db_connection to module scope (@pytest.fixture(scope="module")), it would be established only once per test module.

Most people don’t realize that fixture names are case-sensitive and that the order in which you list them in a test function’s signature doesn’t matter for dependency resolution; pytest builds a dependency graph and executes them in the correct order. If two fixtures have the same name in different scopes, the one with the narrower scope (e.g., function scope) will take precedence within that scope.

The next concept you’ll grapple with is parameterization, allowing you to run a single test function with multiple sets of fixture inputs.

Want structured learning?

Take the full Pytest course →