Fixtures
conftest.py — Shared Fixtures
The conftest.py file acts as a centralized configuration hub that automatically makes fixtures and hooks available to all tests within its directory tree. By decoupling setup logic from individual test modules, it promotes DRY (Don't Repeat Yourself) principles and cleaner test suites. You reach for this pattern when your project grows beyond a single file and you need to share common infrastructure, such as database connections or authentication state, across multiple test modules.
The Discovery Mechanism
When pytest runs, it looks for a file named conftest.py in the root directory and every subdirectory. This is not just a standard module; it is a specialized plugin configuration file that pytest imports automatically. Because pytest performs a recursive search, fixtures defined in a root-level conftest.py become globally available to every test discovered in your project. This happens because pytest builds a hierarchy of 'conftest' modules and merges their namespaces into the available fixture pool. Understanding this allows you to create test-isolation boundaries: simply place a conftest.py in a subdirectory to limit specific fixtures to only the tests within that specific branch of your file system. This hierarchical structure ensures that configuration remains as local as possible while providing broad accessibility when needed, preventing naming collisions and keeping the global scope clean and manageable.
# File: conftest.py
import pytest
# This fixture is now globally available to all tests in the project
@pytest.fixture
def app_config():
return {"timeout": 30, "environment": "testing"}
# When a test requests 'app_config', pytest finds this function in the scope.Sharing Database Connections
A frequent pain point in large test suites is managing expensive resources like database connections or external API handles. If every test file defines its own setup for these, you end up with redundant, brittle code that is impossible to maintain when connection logic changes. By moving the database initialization into a conftest.py file, you establish a 'Single Source of Truth' for how your application interacts with its stateful dependencies. Pytest leverages its dependency injection system to pass the initialized database handle to any test that requests it. Because the fixture is defined in conftest.py, you can use the scope parameter to manage lifecycle efficiently—for instance, setting scope='session' ensures the database connection is created once per test run, significantly reducing execution time compared to recreating it for every single test function, which would be disastrously slow.
# File: conftest.py
import pytest
@pytest.fixture(scope="session")
def db_connection():
# Setup logic happens once per test session
db = {"connected": True, "pool": []}
yield db
# Teardown logic happens after all tests finish
db["connected"] = FalseUsing Fixture Factories
Sometimes a simple static fixture is insufficient because a test needs to configure the input data or environment parameters dynamically. A 'factory fixture' is a pattern where the fixture returns a function instead of a raw object. By defining this pattern in conftest.py, you provide a reusable utility that any test can call to generate consistent, valid test data on the fly. This prevents hard-coding magic values throughout your test files. Instead of manually constructing complex objects in every test, you simply request the factory fixture and call it with the specific attributes required for that scenario. This centralizes the logic for object creation, ensuring that if the schema of your business entities changes, you only update the factory in one location rather than searching through dozens of individual test files to update inconsistent manual object instantiations.
# File: conftest.py
import pytest
def create_user(name, role):
return {"name": name, "role": role, "active": True}
@pytest.fixture
def user_factory():
# Returns a callable function for tests to use
return create_userScoped Fixture Teardowns
One of the most powerful features of fixtures in conftest.py is their ability to manage complex teardown sequences. Using the yield keyword, you define the setup part of the fixture, then provide a point for the test to execute, and finally define the teardown code that runs regardless of whether the test passed or failed. This is critical for cleaning up side effects, such as deleting temporary files or resetting global caches. By placing these fixtures in conftest.py, you guarantee that even if a test suite crashes, the teardown logic is invoked by the pytest framework. This ensures that the testing environment remains idempotent, meaning that the outcome of one test will not leak into or corrupt the state required by the next test, thereby guaranteeing that your test suite remains deterministic and reliable.
# File: conftest.py
import pytest
import os
@pytest.fixture
def temp_file():
path = "test_data.txt"
with open(path, "w") as f:
f.write("initial state")
yield path # Control returned to test here
# Teardown runs after the test finishes
if os.path.exists(path):
os.remove(path)Hooking into Pytest Execution
Beyond simple fixture definitions, conftest.py serves as the primary location for implementing pytest hooks. Hooks are functions that allow you to customize the behavior of the testing framework itself, such as adding command-line arguments, modifying how tests are collected, or altering the report output upon completion. For example, you might want to add a custom command-line flag that allows developers to skip heavy integration tests during quick local development cycles. By placing this hook implementation in conftest.py, it becomes an integral part of the project's testing configuration. When you run pytest from the command line, the framework scans your conftests for these special hook functions and integrates them into its core execution loop. This makes your test suite highly extensible, allowing you to build complex internal tooling directly into your test harness without modifying the base framework code.
# File: conftest.py
import pytest
# Adding a custom command line option
def pytest_addoption(parser):
parser.addoption("--run-slow", action="store_true", help="run slow tests")
@pytest.fixture
def skip_slow(request):
if not request.config.getoption("--run-slow"):
pytest.skip("need --run-slow option to run")Key points
- The conftest.py file is an automatically discovered configuration module for pytest.
- Fixtures defined in conftest.py are automatically available to all tests in that directory and subdirectories.
- Hierarchical placement of conftest.py files allows you to manage fixture scope and visibility across a project.
- Using the yield statement in fixtures ensures reliable setup and teardown of shared resources.
- Fixture factories allow tests to request reusable functions for creating dynamic test data.
- Defining fixtures with session scope in conftest.py reduces resource overhead for slow initialization tasks.
- The conftest.py file is also the standard location for implementing custom pytest hooks.
- Centralizing shared logic in conftest.py adheres to the DRY principle and simplifies long-term maintenance.
Common mistakes
- Mistake: Manually importing fixtures from conftest.py into test files. Why it's wrong: pytest automatically discovers fixtures in conftest.py files; importing them explicitly can lead to naming conflicts or unexpected behavior. Fix: Simply name the fixture as an argument in your test function without any imports.
- Mistake: Defining fixtures in conftest.py that are only needed by one specific test file. Why it's wrong: This bloats the global scope and slows down collection, violating the principle of keeping local test concerns together. Fix: Move specialized fixtures directly into the test file where they are used.
- Mistake: Assuming fixtures in conftest.py are applied to all tests automatically. Why it's wrong: Unless specified with autouse=True, a fixture must be requested by name to be executed. Fix: Either explicitly request the fixture or set autouse=True if the fixture is required for all tests in that scope.
- Mistake: Placing conftest.py in the root directory and expecting it to override local conftest.py files. Why it's wrong: pytest allows nested conftest.py files, which take precedence based on the proximity to the test file. Fix: Ensure conftest configuration is in the directory closest to the tests you want to influence.
- Mistake: Using global mutable state inside a fixture in conftest.py. Why it's wrong: Fixtures should ideally be stateless or safely teardown state to avoid cross-test contamination. Fix: Use yield fixtures to handle setup and cleanup properly, ensuring that shared state is reset after each test.
Interview questions
What is the primary purpose of the conftest.py file in a pytest project?
The conftest.py file serves as a local configuration file that allows you to define fixtures, hooks, and plugins that are automatically discovered by pytest. Its primary purpose is to provide a mechanism for sharing fixtures across multiple test files without needing to import them explicitly. By placing fixtures here, you ensure they are available to all tests in the same directory and its subdirectories, which significantly improves code maintainability and reduces boilerplate code across your test suite.
How does pytest determine the scope of a fixture defined in conftest.py?
The scope of a fixture in conftest.py is determined by the scope parameter in the @pytest.fixture decorator. Valid options include 'function', 'class', 'module', 'package', or 'session'. When defined in conftest.py, the fixture's availability is tied to the directory structure. Because conftest.py is discovered automatically, any test file in that folder or below it can access the fixture. The scope defines the lifecycle; for instance, 'session' scope ensures the fixture runs once for the entire test run, which is ideal for expensive operations like database connections.
Compare the use of conftest.py fixtures versus fixtures defined directly inside a test module.
Fixtures defined inside a test module are private to that module, which is beneficial for test-specific setup that isn't required elsewhere. In contrast, fixtures defined in conftest.py are global to the test suite or a specific subtree, making them ideal for shared dependencies like API clients or test data setup. Using conftest.py reduces duplication and keeps test files focused on logic rather than configuration. However, over-relying on global fixtures in conftest.py can lead to implicit dependencies that make it harder to trace where a specific fixture is initialized, so it is best practice to use conftest.py for common infrastructure and local files for niche requirements.
Explain the concept of 'fixture dependency injection' using conftest.py.
Fixture dependency injection is the mechanism where pytest automatically resolves and provides the required objects to your test functions. When you define a fixture in conftest.py, you name it, and then include that name as an argument in your test function signature. Pytest inspects the signature, finds the corresponding fixture in conftest.py or the local module, executes the setup code, and injects the returned value directly into your test. This decoupling ensures your tests remain clean, as they only describe the resources they need rather than the logic required to create them.
How would you implement a teardown process for a fixture defined in conftest.py?
To implement teardown in a fixture, you use the 'yield' statement instead of 'return'. Everything before the yield statement acts as the setup phase, while everything after the yield executes during the teardown phase. This is critical for resource cleanup, such as closing a browser session or dropping a temporary database table. For example: @pytest.fixture(scope='session') def db(): setup_db(); yield; teardown_db(). Pytest ensures that the code following the yield runs even if the test fails, guaranteeing that your environment remains consistent for subsequent tests.
If you have a large test suite, why might you use multiple conftest.py files instead of one single root file?
Using multiple conftest.py files in different subdirectories allows for hierarchical fixture scoping, which is essential for managing complexity in large projects. By placing a conftest.py in a specific subdirectory, you can override or define fixtures that are only relevant to tests within that specific feature folder. This keeps the global namespace clean and improves performance, as pytest does not have to load every single fixture for every test. It also enforces clearer boundaries between domains, as developers can define specialized setup logic in the relevant folders, making the test suite much more modular, readable, and easier to debug.
Check yourself
1. How does pytest determine which tests have access to a fixture defined in a conftest.py file?
- A.The fixture must be imported at the top of the test module.
- B.The fixture is available to all tests in the same directory and all subdirectories.
- C.The fixture is only available if it is decorated with @pytest.mark.global.
- D.The fixture must be explicitly registered in a pytest.ini file.
Show answer
B. The fixture is available to all tests in the same directory and all subdirectories.
Pytest uses a directory-based scope for conftest.py files. It is available to the directory containing the file and all subdirectories. Importing is unnecessary and discouraged; global marks and ini registration are not required for fixture discovery.
2. If you have a conftest.py in the root folder and another in a subdirectory, which one takes precedence for a test in that subdirectory?
- A.The root conftest.py takes precedence.
- B.Both are merged, but the root one executes first.
- C.The one in the subdirectory takes precedence.
- D.Pytest will throw a collection error due to naming conflicts.
Show answer
C. The one in the subdirectory takes precedence.
Pytest follows a hierarchical lookup; the conftest.py closest to the test file takes precedence, allowing for overrides or local modifications. Merging is not the mechanism used for same-named fixtures; shadowing is.
3. What is the primary benefit of using a yield fixture in conftest.py compared to a return fixture?
- A.It improves performance by reducing memory usage.
- B.It allows for explicit teardown code to run after the test completes.
- C.It forces the fixture to be executed before any other setup code.
- D.It allows the fixture to be used as a class-level variable.
Show answer
B. It allows for explicit teardown code to run after the test completes.
Yield fixtures allow you to perform setup before 'yield' and cleanup after, which is essential for shared resources. Return fixtures have no built-in mechanism for teardown. Performance and scope are independent of the yield keyword.
4. When is an 'autouse' fixture defined in a conftest.py executed?
- A.Only when it is requested by the test function arguments.
- B.Every time pytest starts a new session.
- C.For every test case within the scope of the conftest.py file.
- D.Only when the test module is explicitly imported.
Show answer
C. For every test case within the scope of the conftest.py file.
An autouse fixture automatically runs for every test in its scope without needing to be named in the test function parameters. Session-wide is an option for scope, but it doesn't describe the triggering mechanism, and the other options are incorrect.
5. You have a fixture in conftest.py that is slow to initialize. You want it to run only once per test file rather than for every test function. How should you define the scope?
- A.Use scope='session'.
- B.Use scope='module'.
- C.Use scope='function'.
- D.Use scope='class'.
Show answer
B. Use scope='module'.
Setting scope='module' ensures the fixture setup runs once for each test file (module). 'Session' runs once per test run, 'function' runs for every test, and 'class' runs per test class, making module the correct choice for file-level reuse.