Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›pytest & Testing›Fixture Dependencies and Factories

Fixtures

Fixture Dependencies and Factories

Fixture dependencies allow for modular test setups by injecting existing fixtures into new ones, building complex environmental states from simple building blocks. Fixture factories take this a step further by returning a callable instead of a static value, enabling dynamic test data generation within a single test execution. Mastering these patterns is essential for maintaining clean, scalable test suites that avoid redundant setup code and hardcoded dependencies.

Basic Fixture Dependencies

In pytest, fixture dependency is managed through parameter injection. When a fixture requests another fixture as an argument, pytest resolves the dependency graph before executing the test. This mechanism is powerful because it allows you to compose complex states from discrete, reusable components. For instance, you might have a low-level 'db_connection' fixture and a higher-level 'user_repository' fixture. By passing 'db_connection' into 'user_repository', you guarantee that the repository is always initialized with a valid, active database session. This approach promotes the 'Don't Repeat Yourself' principle, as shared setup logic is isolated in a single, testable fixture. Because pytest manages the lifecycle, it ensures that requested fixtures are torn down in the reverse order of their creation, preventing memory leaks or dangling connections. Understanding this dependency injection is the cornerstone of writing robust, maintainable test suites where setup logic remains decoupled from the test scenarios themselves.

# A low-level fixture representing a database handle
@pytest.fixture
def db_connection():
    db = {"status": "connected"}
    yield db
    db["status"] = "closed"

# A higher-level fixture that depends on the database
@pytest.fixture
def user_repository(db_connection):
    return {"db": db_connection, "table": "users"}

def test_repository(user_repository):
    assert user_repository["db"]["status"] == "connected"

Understanding Dependency Resolution

When multiple fixtures depend on the same underlying resource, pytest's dependency resolution becomes critical. It performs a topological sort on the fixture graph to ensure every requirement is met in the correct sequence. If you have a test that requires two separate fixtures, both of which rely on the same 'config' fixture, pytest ensures that 'config' is instantiated exactly once for that test execution, unless scopes dictate otherwise. This singleton-like behavior during a single test session prevents redundant heavy lifting, such as repeated file system writes or network connections. By understanding this resolution path, you can reason about performance impacts; if a base fixture is slow, all downstream dependencies will inherit that latency. You can strategically place fixtures in conftest files to share them across the entire project, ensuring that your dependency graph remains consistent, predictable, and fully observable through the command-line introspection tools.

# Shared config fixture
@pytest.fixture(scope="session")
def app_config():
    return {"timeout": 30}

# Both fixtures depend on app_config
@pytest.fixture
def logger(app_config):
    return f"Logging with {app_config['timeout']}s timeout"

@pytest.fixture
def client(app_config):
    return f"Client using {app_config['timeout']}s timeout"

def test_app(logger, client):
    assert "30s" in logger
    assert "30s" in client

The Fixture Factory Pattern

A fixture factory occurs when a fixture function returns a function instead of a raw object or value. This pattern is indispensable when you need multiple variants of a resource during a single test or need to pass parameters at runtime that fixtures normally cannot accept directly. By returning a callable, you defer the creation of the object until the moment it is actually needed in the test body. This allows you to generate dozens of similar objects—such as unique user accounts or specific message payloads—without writing dozens of individual fixtures. The factory maintains access to the fixture's internal logic, like dependency injection or teardown tracking, ensuring that every object created by the factory is still managed by the pytest lifecycle. This shift from static instantiation to dynamic production makes tests highly flexible while retaining the benefits of automatic resource cleanup provided by the pytest framework.

# Factory fixture returning a function
@pytest.fixture
def user_factory():
    def create_user(username, role="guest"):
        return {"username": username, "role": role}
    return create_user

def test_user_creation(user_factory):
    # Generate multiple unique objects in one test
    admin = user_factory("admin_user", role="admin")
    guest = user_factory("guest_user")
    assert admin["role"] == "admin"
    assert guest["role"] == "guest"

Integrating Dependencies with Factories

Factories become exponentially more useful when they combine the factory pattern with standard dependency injection. By creating a factory that relies on other fixtures, you can build complex, stateful objects that automatically include required environmental components like database sessions, mock services, or configuration objects. For example, a 'data_factory' could require a 'db_session' fixture, then use that session to persist items created by the test. This means your tests don't have to worry about manual database interaction or wiring; they simply ask for the factory and call it. This separation of concerns means the test defines the 'what'—what data is needed—while the factory handles the 'how'—how to construct it, persist it, and relate it to the global environment. This pattern is the gold standard for large, complex test suites where manual object instantiation would lead to massive, unreadable setup blocks inside individual test functions.

# Factory dependent on a database fixture
@pytest.fixture
def item_factory(db_connection):
    def create_item(name):
        item = {"name": name, "db": db_connection}
        return item
    return create_item

def test_item_persistence(item_factory):
    item = item_factory("test_widget")
    assert item["db"]["status"] == "connected"
    assert item["name"] == "test_widget"

Advanced Factory Teardown

One subtle challenge with factory patterns is ensuring that objects created during test execution are properly torn down. While pytest cleans up the factory itself, it does not automatically track objects returned by a factory unless you explicitly implement a tracking mechanism. A common strategy is to keep an internal list of created resources within the factory fixture and perform cleanup inside the 'yield' block. When the test finishes, the factory fixture’s teardown code iterates through this list and destroys every object generated during the test. This pattern is particularly vital when using factories to manage external resources like temporary files, remote API mocks, or volatile database records. By encapsulating this tracking logic within the factory, you keep your test code clean and declarative, while ensuring that the system remains free of side effects, providing a clean slate for every subsequent test case.

@pytest.fixture
def managed_factory():
    created = []
    def factory(name):
        obj = {"name": name}
        created.append(obj)
        return obj
    yield factory
    # Cleanup all items created by this factory
    for item in created:
        item.clear()

def test_cleanup(managed_factory):
    item = managed_factory("temp_data")
    assert item["name"] == "temp_data"

Key points

  • Fixtures communicate dependencies through function argument injection.
  • Pytest automatically resolves dependency graphs to determine the order of execution.
  • Dependencies are shared across tests to optimize performance and prevent redundant setups.
  • A fixture factory returns a callable function that generates objects on demand.
  • Factories enable dynamic data creation without cluttering the test suite with boilerplate.
  • Dependencies can be injected into factories to grant them access to environmental state.
  • Teardown logic in factories ensures that dynamically generated resources do not persist after the test.
  • Encapsulating construction logic in factories keeps test functions readable and focused on the verification of behavior.

Common mistakes

  • Mistake: Manually instantiating complex objects inside test functions. Why it's wrong: This couples tests to the internal structure of objects, making maintenance difficult. Fix: Use a factory fixture to encapsulate object creation logic.
  • Mistake: Overusing session-scoped fixtures for everything. Why it's wrong: Session scope can lead to cross-test pollution where one test modifies state that affects others. Fix: Use function or module scope for fixtures that involve state.
  • Mistake: Putting heavy database setup in a fixture without transactional cleanup. Why it's wrong: Data persistence carries over between tests, causing flaky results. Fix: Use yield fixtures to perform cleanup code after the test finishes.
  • Mistake: Passing factories as fixtures rather than using them to return objects. Why it's wrong: The test becomes responsible for calling the factory logic, defeating the purpose of abstraction. Fix: Return a callable object from the fixture so tests can request specific parameters.
  • Mistake: Ignoring fixture dependencies by trying to define them in arbitrary orders. Why it's wrong: Pytest relies on explicit dependency injection; implicit assumptions about order break easily. Fix: Explicitly request dependencies as arguments in the fixture's signature.

Interview questions

What is a fixture dependency in pytest, and how do you implement one?

A fixture dependency occurs when one fixture requires the output of another fixture to function correctly. You implement this by simply passing the name of the required fixture as an argument to your fixture function. For example, if you have a 'db_connection' fixture, another fixture like 'user_data' can request 'db_connection' in its definition. Pytest will automatically resolve the dependency graph, executing 'db_connection' first, then passing its result into 'user_data', ensuring that the state is properly prepared before your test logic ever executes.

How does using a factory pattern with fixtures improve test flexibility compared to standard static fixtures?

Standard static fixtures provide a fixed, pre-defined state, which limits your ability to test different variations of the same data structure. A factory pattern, where the fixture returns a callable function instead of a raw object, allows the test to dynamically generate data on demand. By calling the factory function within the test—for example, 'create_user(is_admin=True)'—you retain the benefit of automatic teardown and setup while gaining the control needed to inject custom parameters, making your tests significantly more modular and reusable.

Compare the use of autouse fixtures versus explicit fixture dependencies. When should you choose one over the other?

Autouse fixtures run automatically for every test in a given scope, which is useful for global setups like clearing a database or initializing logs. However, explicit dependencies—where you inject the fixture into the test signature—are generally preferred. Explicit dependencies are easier to trace, improve test readability by showing exactly what resources a test requires, and prevent unnecessary setup costs for tests that don't actually need that specific resource, leading to faster and more predictable test suite performance.

How can you leverage the 'yield' keyword to handle fixture teardown in pytest?

The 'yield' keyword is essential for defining fixtures that need cleanup after a test finishes. By using 'yield' instead of 'return', the fixture splits its execution: code before the 'yield' runs during the setup phase, and code after the 'yield' runs during the teardown phase. This is critical for resetting external resources, such as closing file handles, rolling back database transactions, or stopping mock servers, ensuring that one test's side effects do not leak into and pollute subsequent tests.

Explain the concept of fixture scope and how it impacts dependency management.

Fixture scope, defined as 'function', 'class', 'module', or 'session', dictates how often a fixture is created and destroyed. Managing scope is crucial because a fixture can only depend on other fixtures with the same or wider scope; for example, a 'session' scoped fixture cannot depend on a 'function' scoped one. Choosing the correct scope allows you to cache expensive resources, like database connections, while ensuring that the lifecycle of your test dependencies remains consistent and efficient throughout the entire suite execution.

How would you handle complex dependency injection for a testing environment where many fixtures require different combinations of session-scoped and function-scoped resources?

Handling complex dependency chains requires a modular approach using 'conftest.py' files. You should decompose your fixtures into smaller, single-responsibility units that are then composed using standard dependency injection. By utilizing the 'indirect' parameterization feature, you can pass test data into a fixture, allowing the fixture to adapt its behavior dynamically. This enables you to combine session-level resources, like a database engine, with function-specific data without coupling the setup logic, ensuring that your architecture remains maintainable even as the complexity of your integration tests grows.

All pytest & Testing interview questions →

Check yourself

1. What is the primary advantage of using a factory pattern fixture over a static object fixture?

  • A.It speeds up execution by skipping the setup phase
  • B.It allows tests to customize the generated data on a per-test basis
  • C.It automatically detects and deletes unused database tables
  • D.It reduces the total number of lines of code in the project
Show answer

B. It allows tests to customize the generated data on a per-test basis
The factory pattern provides a callable fixture that allows tests to override specific fields, providing flexibility. Static fixtures don't do this (Option 2). Speed isn't the primary goal (Option 1). It doesn't handle database cleanup (Option 3). Code length is not the main benefit (Option 4).

2. If two fixtures depend on each other, what happens when a test requests them?

  • A.Pytest raises an error about a circular dependency
  • B.Pytest runs both fixtures in parallel
  • C.Pytest executes them in the order they are defined in the file
  • D.Pytest ignores the dependency and initializes them randomly
Show answer

A. Pytest raises an error about a circular dependency
Pytest detects circular dependencies during the dependency resolution phase and raises a RecursionError. It does not run them in parallel (Option 2), order of definition is irrelevant (Option 3), and it does not ignore the dependency (Option 4).

3. What is the purpose of the 'yield' keyword in a pytest fixture?

  • A.To return multiple values to the test
  • B.To pause the test execution until the database is ready
  • C.To separate the setup code from the teardown/cleanup code
  • D.To bypass the fixture scope restriction
Show answer

C. To separate the setup code from the teardown/cleanup code
Yield is the standard way to provide a teardown phase; code before yield runs before the test, and code after runs after the test. It does not return multiple values (Option 1), pause tests for the database (Option 2), or bypass scope (Option 4).

4. Which scope should you choose for a fixture that creates a temporary database connection that must be fresh for every test?

  • A.session
  • B.module
  • C.class
  • D.function
Show answer

D. function
Function scope ensures the fixture is recreated for every individual test function, providing isolation. Session, module, and class scopes persist the fixture longer than a single test, which risks cross-test contamination.

5. Why is it recommended to use 'autouse=True' sparingly?

  • A.Because it makes it difficult to track which fixtures affect a specific test
  • B.Because it causes tests to run significantly slower than normal
  • C.Because it breaks the fixture dependency injection mechanism entirely
  • D.Because it prevents fixtures from using the yield keyword
Show answer

A. Because it makes it difficult to track which fixtures affect a specific test
Autouse fixtures run implicitly, making it hard to see dependencies in the test signature. It doesn't inherently slow things down (Option 2), break DI (Option 3), or prevent yield (Option 4).

Take the full pytest & Testing quiz →

← Previousconftest.py — Shared FixturesNext →pytest.mark.parametrize — Multiple Inputs

pytest & Testing

25 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app