Fixture Lifecycle
Purpose
This subtopic addresses the complete lifecycle management of fixtures within the pytest framework. Unlike the broader fixture system that encompasses scoping, parametrization, and injection, the fixture lifecycle focuses specifically on how fixture instances are created, cached, reused, and properly torn down (finalized) during test execution. It ensures that fixtures are instantiated only once per their defined scope and that any associated teardown code is executed correctly after tests complete or when fixture instances are no longer valid.
Functionality
The lifecycle management of fixtures revolves around several key workflows:
Fixture Execution and Caching
When a fixture is requested (typically during test setup), pytest resolves the appropriate
FixtureDefand determines whether the fixture's value is already cached for the current scope and parameters.If a cached value exists and matches the current parameters, pytest returns the cached value, avoiding redundant fixture setup.
If not cached or the parameters differ (e.g., due to parametrization), pytest executes the fixture function to produce a new fixture instance.
Fixture functions can be normal functions returning a value or generator functions using
yieldto separate setup and teardown code.
Setup and Teardown
For generator-based fixtures (yield fixtures), pytest runs the fixture function up to the
yieldstatement to perform setup, captures the yielded value as the fixture instance, and registers a finalizer that runs the teardown code after the test or scope ends.For regular (returning) fixtures, pytest simply calls the fixture function and caches the returned value.
Finalizers are stored per fixture and called in reverse order during teardown, ensuring cleanup happens reliably even if exceptions occur.
The system supports chaining finalizers, including those added dynamically via
request.addfinalizer.
Scope and Dependency Awareness
Fixtures have defined scopes (
function,class,module,package,session) that control their lifetime.When executing a fixture, pytest ensures that all dependent fixtures (those requested by the fixture function) are evaluated first and their lifecycles are properly managed.
Fixtures with wider scopes cache their values to be reused across multiple tests, minimizing setup overhead.
If a fixture instance becomes invalid (e.g., due to parameter changes), pytest triggers teardown of the old instance before creating a new one.
Error Handling
If fixture setup raises an exception or skips, pytest caches that outcome and propagates it accordingly.
Fixture lifecycle management carefully handles multiple exceptions during teardown by aggregating them into an
BaseExceptionGroup.
Key Classes and Methods
FixtureDef.execute(request: SubRequest): Core method that manages caching, executes the fixture function if needed, handles dependencies, and registers finalizers.FixtureDef.finish(request: SubRequest): Runs all registered finalizers for the fixture, clears cached results, and handles teardown errors.call_fixture_func(fixturefunc, request, kwargs): Calls the fixture function, handling generator fixtures by splitting setup and teardown phases.SubRequestandTopRequest: Represents the context of a fixture execution request, managing scope checking and finalizers.FixtureRequest.getfixturevalue(argname): Dynamically triggers fixture execution and returns its value, respecting lifecycle and caching.
Example Code Snippet
def execute(self, request: SubRequest) -> FixtureValue:
# Check cache and dependencies
if self.cached_result is not None:
# Return cached value if parameters match
...
# Setup dependent fixtures
...
# Execute fixture function and cache the result
result = request.node.ihook.pytest_fixture_setup(fixturedef=self, request=request)
# Register finalizer for teardown
request.node.addfinalizer(functools.partial(self.finish, request=request))
return result
This method coordinates the entire lifecycle for a single fixture invocation.
Relationship to Parent Topic
The parent topic, *Fixture Management*, provides the overall framework for defining, requesting, and injecting fixtures with features like parametrization and dependency resolution. *Fixture Lifecycle* is a critical subcomponent focused on the temporal aspect — how fixtures are instantiated, reused, and cleaned up during test runs.
It implements the caching strategy that ensures fixtures are only recreated when necessary.
It manages the setup and teardown phases that guarantee resource allocation and cleanup correspond correctly to the fixture's scope.
Lifecycle complements Fixture Parametrization by handling the nuances of caching and re-creation of fixture instances as parameters change.
It integrates tightly with the
FixtureRequestobjects that provide fixture context and with theFixtureManagerthat holds fixture definitions.
Together, these subtopics form a cohesive fixture system that is powerful, efficient, and reliable.
Diagram
The following flowchart visualizes the core process of fixture lifecycle management during test execution:
flowchart TD
A[Test Requests Fixture] --> B[Resolve FixtureDef & Scope]
B --> C{Is Fixture Cached for Scope & Params?}
C -->|Yes| D[Return Cached Fixture Value]
C -->|No| E[Setup Dependent Fixtures]
E --> F[Execute Fixture Function]
F --> G{Is Generator Fixture?}
G -->|Yes| H[Run Setup Part (before yield)]
G -->|No| I[Run Fixture Function Normally]
H --> J[Cache Yielded Value]
I --> J[Cache Return Value]
J --> K[Register Finalizer for Teardown]
K --> L[Return Fixture Value]
subgraph Teardown Phase
M[After Tests or Scope Ends] --> N[Call Registered Finalizers]
N --> O[Clear Cached Result]
end
L --> M
This flowchart highlights the decision points and major steps in setting up, caching, and finalizing fixtures, encapsulating the lifecycle that supports pytest's flexible fixture system.