Managing Setup and Teardown in Pytest Fixtures
In Python testing, pytest fixtures provide a modular,
declarative framework to prepare test environments and cleanly dispose
of resources. This article explains how fixtures utilize the
yield statement to execute setup and teardown phases, how
pytest resolves complex fixture dependencies through
dependency injection, and how scoping rules dictate the lifecycle of
resources across your test suite.
The
yield Keyword: Defining Setup and Teardown
pytest separates setup from teardown within a single
fixture using Python’s yield statement. When a test
requests a fixture, pytest executes all code prior to the
yield statement during the setup phase. The value provided
to yield is then passed directly into the test function.
Once the test completes—regardless of whether it passed, failed, or
raised an exception—pytest resumes the fixture to execute
the code following yield as the teardown phase.
import pytest
@pytest.fixture
def database_connection():
# Setup: initialize resource
conn = create_db_connection()
yield conn
# Teardown: clean up resource
conn.close()This structure ensures resource disposal without requiring nested
try...finally blocks inside individual test cases.
Resolving Dependencies via Dependency Injection
Fixtures are designed to be composable. A fixture can request other
fixtures simply by listing them as arguments in its function definition.
When a test runs, pytest constructs a Directed Acyclic
Graph (DAG) of all requested fixtures to determine the exact order of
execution.
Setup operations run from the outermost dependency inward:
- Base dependencies execute their setup code first.
- Dependent fixtures execute their setup using the resolved base dependencies.
- The test function runs.
- Teardown executes in the exact reverse order: dependent fixtures tear down first, followed by base dependencies.
@pytest.fixture
def app_config():
return {"host": "localhost", "port": 5432}
@pytest.fixture
def db_client(app_config):
client = connect(app_config["host"], app_config["port"])
yield client
client.disconnect()
def test_query(db_client):
assert db_client.is_connected()In this scenario, app_config is evaluated first, passed
to db_client, and teardown occurs strictly after
test_query finishes.
Managing Lifecycles with Fixture Scopes
To avoid unnecessary setup and teardown overhead, fixtures support
multiple scopes via the @pytest.fixture(scope=...)
decorator parameter. The available scopes are:
function(default): Runs setup and teardown for each test function.class: Runs once per test class, sharing setup across all methods in that class.module: Runs once per module (Python file).package: Runs once per package directory.session: Runs once for the entire test session.
Higher-scoped fixtures (such as session) cannot depend
on lower-scoped fixtures (such as function), preserving
test isolation and deterministic teardown sequences.
Automatic Execution with
autouse
When setup or teardown must occur globally without passing fixture
arguments directly to test signatures, fixtures can be defined with
autouse=True.
@pytest.fixture(autouse=True)
def clear_cache():
cache.clear()
yield
cache.clear()Every test within the fixture's scope automatically triggers this setup and teardown behavior, guaranteeing that environment states remain isolated between tests.