Fixtures
Fixture Scopes — function, class, module, session
Fixture scopes define the lifecycle and persistence duration of your test resources within the pytest execution flow. Choosing the correct scope is critical for optimizing performance by avoiding redundant setup and teardown operations for shared resources. You reach for scoping when you need to balance test isolation against the overhead of initializing heavy objects like database connections or browser drivers.
Function Scope: The Default Isolation
The function scope is the default behavior in pytest, meaning the fixture is set up once per test function and torn down immediately after that function finishes. This is the most conservative choice because it guarantees perfect isolation; every single test starts with a clean slate, ensuring that no state from a previous test can influence the current one. Because of this absolute isolation, it is the safest default, but it can be computationally expensive if your fixture performs a heavy operation, such as initializing a database transaction or creating a file system. If a test suite has five hundred tests, a function-scoped fixture will be instantiated five hundred times. You should prioritize this scope when your tests modify shared state in a way that would be difficult to reset manually between tests, as the automatic teardown ensures the environment remains pristine for every test run.
import pytest
@pytest.fixture(scope='function')
def temp_workspace():
# Executed before EACH test function
print("\nSetting up workspace")
yield "workspace_path"
# Executed after EACH test function
print("\nTearing down workspace")Class Scope: Grouping by Context
The class scope creates a fixture that is set up once when the first test method inside a class is executed and torn down only after all methods within that class have completed. This scope is highly effective when you have a logical grouping of tests that all require a common, slightly expensive object but do not need that object re-initialized for every single call. For example, if you are testing a class that provides CRUD operations, the class-scoped fixture can initialize a mock database connection once for all CRUD tests. While this creates a performance benefit, you must exercise extreme caution regarding state; if one test modifies the object provided by the class-scoped fixture, that modified state will persist for the subsequent tests in the same class. This makes it a powerful tool for performance, but you must ensure that your tests are written to not cross-contaminate each other through this shared instance.
import pytest
@pytest.fixture(scope='class')
def database_connection():
# Setup once for all methods in a class
conn = "connected_db"
yield conn
# Teardown once after the class finishes
print("\nClosing db connection")
class TestDatabase:
def test_read(self, database_connection):
assert database_connection == "connected_db"
def test_write(self, database_connection):
assert database_connection == "connected_db"Module Scope: Shared File Resources
Module-scoped fixtures are initialized once for the entire module (i.e., the python file containing the tests) and are destroyed after all tests in that module finish. This is an excellent middle ground when you have several test classes or standalone test functions that share the same configuration, static data, or heavy utility objects. By pulling these shared items into a module-scoped fixture, you avoid the redundant overhead of re-creating the same resource for every class or function within that file. The key to reasoning about this scope is understanding the boundaries of the file; pytest treats the file as the boundary for the lifecycle. This allows you to centralize setup at the top of your test file, keeping the individual test functions clean and focused on their specific assertions rather than the boilerplate required to initialize the testing environment itself.
import pytest
@pytest.fixture(scope='module')
def config_data():
# Setup once for all tests in this file
return {"api_key": "secret123"}
def test_api(config_data):
assert config_data["api_key"] == "secret123"
def test_auth(config_data):
assert "api_key" in config_dataSession Scope: Global Orchestration
The session scope is the broadest possible scope, where a fixture is initialized when the test session begins and torn down only after the entire test suite has concluded. This is ideal for resources that are extremely expensive to initialize, such as spinning up a local web server, a heavy docker container, or establishing a network connection that remains valid throughout the run. Because the setup happens exactly once, it provides significant performance gains for large test suites. However, it requires a high degree of discipline; because the resource lives for the duration of the entire process, any state corruption caused by a single test will be visible to every other test in the entire suite. You should reserve session scope for immutable resources or services that are designed to handle multiple concurrent requests without needing to be reset between individual test runs.
import pytest
@pytest.fixture(scope='session')
def global_server():
# Setup once for the entire test run
server = "http://localhost:8080"
yield server
# Final teardown after all tests finish
print("\nShutting down global server")Scope Inheritance and Dependencies
A crucial rule in pytest scoping is the hierarchy: a higher-scope fixture cannot depend on a lower-scope fixture. For instance, a session-scoped fixture cannot request a function-scoped fixture because the session fixture lives longer than the function fixture, creating an impossible lifecycle scenario. If a session-scoped fixture tried to use a function-scoped fixture, pytest would not know which function instance to map to the session resource. Understanding this inheritance is vital for building robust test architectures. When designing your fixtures, always plan your dependency graph such that dependencies are 'upwards' or 'lateral' in terms of scope longevity—a function fixture can easily request a session fixture because the session resource is guaranteed to be available, but the reverse is forbidden. By respecting this hierarchy, you ensure your test framework remains predictable, stable, and avoids cryptic runtime errors during the collection phase of the testing process.
import pytest
@pytest.fixture(scope='session')
def global_config():
return "Config"
@pytest.fixture(scope='function')
def local_task(global_config):
# Function fixture depends on session fixture
return f"Task using {global_config}"
def test_task(local_task):
assert local_task == "Task using Config"Key points
- Function scope ensures complete isolation by re-initializing the fixture before every individual test function.
- Class scope allows sharing a fixture instance across all test methods within a specific test class.
- Module scope centralizes setup for all tests defined within a single Python file.
- Session scope provides global persistence for the duration of the entire test execution process.
- Higher-scope fixtures cannot depend on lower-scope fixtures due to incompatible lifecycles.
- Choosing a wider scope significantly reduces performance overhead for heavy object initialization.
- Narrower scopes are preferable when tests modify shared state and require clean environments.
- The default pytest fixture scope is function, providing the highest level of safety by default.
Common mistakes
- Mistake: Using 'session' scope for every fixture to maximize speed. Why it's wrong: It creates hidden state dependencies where tests might pass or fail based on the order of execution because fixtures aren't reset. Fix: Use the smallest scope necessary for the test requirement.
- Mistake: Assuming a 'class' scope fixture will automatically re-run for every method inside the class. Why it's wrong: Class scope fixtures run once per class, not per test method. Fix: Use 'function' scope if you need a fresh state for every test method.
- Mistake: Trying to use a higher-scoped fixture (like 'session') as a dependency for a lower-scoped one (like 'function') that relies on dynamic data. Why it's wrong: Higher-scoped fixtures are initialized before lower-scoped ones and cannot access the state of the latter. Fix: Pass the data into the function-scoped fixture or use request.getfixturevalue for dynamic dependencies.
- Mistake: Forgetting that 'module' scoped fixtures persist throughout the entire file. Why it's wrong: Side effects (like modifying a database table) persist across every test function in that module, leading to 'leaky' tests. Fix: Use 'function' scope if the test modifies the fixture's state.
- Mistake: Using class-scoped fixtures for setup that involves external resources like network connections or files without proper teardown. Why it's wrong: If the class fails or the scope persists unexpectedly, the resource stays locked. Fix: Always use yield-style fixtures to ensure teardown code executes regardless of test success.
Interview questions
What is the default scope for a pytest fixture, and what does that mean in practice?
The default scope for a pytest fixture is 'function'. This means that the fixture is executed and its return value is generated once for every single test function that requests it as an argument. If you have ten test functions in a file, all requesting the same fixture, that fixture setup code will run ten separate times. This ensures test isolation, as each test receives a fresh instance of the resource, preventing side effects from one test bleeding into another, although it can lead to slower execution times if the setup logic is resource-intensive.
Explain the purpose of the 'class' scope in pytest fixtures.
The 'class' scope is designed for situations where you group test methods inside a test class. When a fixture is defined with this scope, it is invoked once for the entire class, regardless of how many test methods are within that class. The result of the fixture is shared across all methods of that class. This is particularly useful for database connections or object initializations that are expensive to set up but are only required by tests living within the same logical unit, reducing the total overhead compared to the default 'function' scope.
How does the 'module' scope differ from 'session' scope in terms of fixture lifetime?
The 'module' scope and 'session' scope differ fundamentally in their breadth of persistence. A 'module' scoped fixture runs once per test file, meaning it is set up at the start of the module and torn down once all tests in that specific file are complete. In contrast, 'session' scoped fixtures run once per entire test suite execution. This means a 'session' fixture is initialized when pytest begins and is only torn down when every single test in the run has finished. 'Session' scope is ideal for global resources like starting a single browser driver or connecting to a shared test database that must persist across the entire test run.
Compare using 'function' scope versus 'session' scope when dealing with heavy resource initialization.
When dealing with heavy resources, 'function' scope is the safest choice for isolation but the most expensive in terms of performance because it repeats the high-cost setup for every test. 'Session' scope provides significant performance gains by initializing the resource just once for the entire suite. However, 'session' scope requires careful design because it forces every test to share the exact same state. If your tests modify the state of that resource, you risk cascading failures where one test's modifications corrupt the input for subsequent tests. Therefore, use 'session' only for read-only resources or when you can effectively reset the state between tests.
What happens if a test with a wider scope tries to request a fixture with a narrower scope?
In pytest, a fixture cannot depend on a fixture that has a narrower scope than itself. For example, a 'session' scoped fixture cannot request a 'function' scoped fixture. This is because a 'session' fixture is initialized only once, while the 'function' fixture is intended to be initialized repeatedly. Pytest enforces this restriction because there is no logical way to inject a 'function' scoped dependency into a context that is only generated once. Attempting this will cause a 'ScopeMismatch' error during test collection, prompting you to either broaden the dependency or narrow the requesting fixture.
Describe how to determine the optimal scope for a fixture in a complex test suite.
Determining the optimal scope involves balancing test isolation against execution speed. Start by scoping as narrowly as possible—typically 'function'—to ensure that no test pollutes the environment for another. As your suite grows, monitor execution times. If a specific fixture is identified as a bottleneck, consider moving it to 'class' or 'module' scope if the tests within those contexts do not mutate the fixture's state. Finally, only promote a fixture to 'session' scope if it is truly a global dependency, like a configuration reader or a remote service client, ensuring you have logic in place to handle state cleanup if tests interfere with one another.
Check yourself
1. You have a suite where multiple test classes require a database connection. If you want the connection to be opened once per module to save time, which scope should you use?
- A.function
- B.class
- C.module
- D.session
Show answer
C. module
Module scope ensures the fixture runs once for the entire file (module), which is the intended goal. Function scope would be too slow, class scope wouldn't share across classes in the same module, and session scope is overkill and risks state leakage if only one module needs it.
2. What happens if a test function requires a 'function' scoped fixture that depends on a 'session' scoped fixture?
- A.Pytest raises an error because scopes are incompatible.
- B.The session fixture is re-initialized for every function.
- C.The session fixture is initialized once and reused by the function fixture.
- D.The function fixture will override the session fixture configuration.
Show answer
C. The session fixture is initialized once and reused by the function fixture.
Lower-scoped fixtures can depend on higher-scoped ones without issue. The session fixture remains alive for the duration of the test run, while the function fixture is created fresh for each test. Option 1 is false, Option 2 defeats the purpose of session, and Option 4 is syntactically impossible.
3. Why would you choose 'function' scope over 'session' scope for a fixture that clears a temporary directory?
- A.To ensure each test starts with a completely empty directory.
- B.Because 'session' scope cannot be used for file operations.
- C.To allow tests to run in parallel more efficiently.
- D.Because 'session' scope only works for class-based tests.
Show answer
A. To ensure each test starts with a completely empty directory.
Function scope ensures isolation; if one test leaves files in the directory, the next test won't see them because the fixture re-runs. Session scope would mean all tests share the same directory, leading to file collisions. The other options are factually incorrect regarding pytest capabilities.
4. If you have a base test class with a 'class' scoped fixture, and a subclass that inherits from it, how many times does the fixture run?
- A.Once for the base class and once for each subclass.
- B.Once for the entire class hierarchy.
- C.Every time a method is called within either class.
- D.It depends on the number of test methods in the base class.
Show answer
A. Once for the base class and once for each subclass.
Pytest scopes are applied to the specific class they are defined in. If the fixture is defined in the base class, subclasses will trigger the fixture again upon their own execution. Option 1 is the correct behavior; others misunderstand the scope's lifecycle.
5. Which of the following scenarios best justifies the use of a 'session' scoped fixture?
- A.Initializing a complex UI component for a specific test function.
- B.Setting up a local mock server that needs to be active for the entire test suite.
- C.Creating a fresh user database entry for a single test case.
- D.Preparing a local file to be read by tests in a single file.
Show answer
B. Setting up a local mock server that needs to be active for the entire test suite.
Session scope is ideal for expensive resources that are needed globally, such as a mock server. Function scope (Option 1 and 3) is better for specific test requirements, and module scope (Option 4) is better for single-file dependencies to avoid unnecessary global overhead.