Pytest Fixtures: The Complete Guide for 2026
Pytest fixtures are the backbone of clean, maintainable test suites in Python. They replace repetitive setup and teardown code with reusable, composable functions that inject dependencies directly into your tests. Whether you need a database connection, a temporary directory, a mocked API client, or complex test data, fixtures give you a declarative way to provide it.
This guide covers everything from basic fixture usage to advanced patterns like fixture factories, parametrized fixtures, and session-scoped resource management. You will learn how to use built-in fixtures like tmp_path, monkeypatch, and capsys, organize shared fixtures with conftest.py, and apply the patterns that keep large test suites fast and readable.
Table of Contents
- What Are Pytest Fixtures
- Basic Fixture Usage
- Fixture Scope: function, class, module, session
- Autouse Fixtures
- Parametrize with Fixtures
- Fixture Factories
- Yield Fixtures: Setup and Teardown
- conftest.py: Sharing Fixtures
- Built-in Fixtures
- The request Fixture
- Fixture Finalization and Cleanup
- Best Practices
- Frequently Asked Questions
1. What Are Pytest Fixtures
A pytest fixture is a function decorated with @pytest.fixture that provides a fixed baseline for tests to run on. When a test function includes a fixture name as a parameter, pytest calls the fixture function and passes the return value into the test. This is dependency injection: your test declares what it needs, and the framework provides it.
Fixtures solve several problems that plague test suites built with raw setUp/tearDown methods:
- Reusability — Define setup logic once, use it across hundreds of tests
- Composability — Fixtures can depend on other fixtures, building complex state from simple pieces
- Explicit dependencies — Each test declares exactly what it needs in its parameter list
- Automatic teardown — Yield fixtures clean up resources even when tests fail
- Flexible scope — Run setup once per test, per class, per module, or per session
2. Basic Fixture Usage
The simplest fixture returns a value that your test needs. Declare it with @pytest.fixture and request it by adding the fixture name as a test parameter:
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "email": "alice@example.com", "role": "admin"}
def test_user_has_name(sample_user):
assert sample_user["name"] == "Alice"
def test_user_is_admin(sample_user):
assert sample_user["role"] == "admin"
Each test gets its own fresh copy of the fixture. If test_user_has_name modifies the dictionary, test_user_is_admin still receives the original data. This isolation is the default behavior with function-scoped fixtures.
Fixtures can depend on other fixtures. Pytest resolves the dependency graph automatically:
@pytest.fixture
def db_connection():
conn = create_connection("test.db")
return conn
@pytest.fixture
def user_repo(db_connection):
return UserRepository(db_connection)
def test_create_user(user_repo):
user = user_repo.create(name="Bob")
assert user.id is not None
When test_create_user runs, pytest first creates db_connection, passes it into user_repo, and then passes the repository into the test. The entire chain is resolved from the parameter names.
3. Fixture Scope
Fixture scope controls how often pytest creates and destroys the fixture instance. The scope parameter accepts four values:
Function Scope (Default)
A new fixture instance is created for every test function that requests it. This provides maximum isolation but can be slow for expensive resources:
@pytest.fixture(scope="function")
def fresh_database():
db = create_test_database()
return db
# Created and destroyed for EVERY test
Class Scope
One instance is shared across all test methods in a class. Useful when tests in a class read from shared state without modifying it:
@pytest.fixture(scope="class")
def api_client():
client = APIClient(base_url="https://test.api.com")
client.authenticate(token="test-token")
return client
class TestUserEndpoints:
def test_list_users(self, api_client):
response = api_client.get("/users")
assert response.status_code == 200
def test_get_user(self, api_client):
response = api_client.get("/users/1")
assert response.status_code == 200
Module Scope
One instance per test file. Ideal for expensive resources that are safe to share across all tests in a module:
@pytest.fixture(scope="module")
def elasticsearch_index():
index = create_test_index("test_products")
index.bulk_insert(load_test_data())
return index
Session Scope
A single instance shared across the entire test run. Use this for truly expensive, read-only resources like Docker containers or database servers:
@pytest.fixture(scope="session")
def postgres_container():
container = start_postgres_container()
wait_for_ready(container, timeout=30)
return container
Rule of thumb: Use the narrowest scope that makes sense. Function scope for anything that might be modified. Session scope only for expensive, immutable infrastructure.
4. Autouse Fixtures
Setting autouse=True makes a fixture apply to every test in its scope automatically, without tests needing to request it by name. This is useful for environment setup, logging, or resetting global state:
@pytest.fixture(autouse=True)
def reset_environment():
"""Clear environment variables before each test."""
original = os.environ.copy()
yield
os.environ.clear()
os.environ.update(original)
@pytest.fixture(autouse=True, scope="session")
def configure_logging():
"""Set up test logging once for the entire suite."""
logging.basicConfig(level=logging.WARNING)
yield
Use autouse sparingly. When every test silently depends on hidden fixtures, debugging failures becomes harder. Prefer explicit fixture parameters unless the setup truly applies universally.
Autouse fixtures in conftest.py apply to all tests in that directory. An autouse fixture defined inside a test class applies only to methods in that class:
class TestPayments:
@pytest.fixture(autouse=True)
def setup_stripe_mock(self, monkeypatch):
monkeypatch.setattr("payments.stripe.charge", mock_charge)
def test_successful_payment(self):
result = process_payment(amount=100)
assert result.success is True
5. Parametrize with Fixtures
You can parametrize fixtures to run every dependent test multiple times with different data. This is more powerful than @pytest.mark.parametrize when the parametrized values require setup logic:
@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database(request):
db = create_database(engine=request.param)
yield db
db.drop_all()
def test_insert_record(database):
database.insert({"key": "value"})
assert database.count() == 1
# Runs 3 times: once for each database engine
The request.param attribute holds the current parameter value. Pytest generates a separate test ID for each parameter, so your output shows test_insert_record[sqlite], test_insert_record[postgres], and test_insert_record[mysql].
You can customize test IDs with the ids parameter:
@pytest.fixture(
params=[
{"host": "localhost", "port": 5432},
{"host": "remote.db.com", "port": 5432},
],
ids=["local-db", "remote-db"]
)
def db_config(request):
return request.param
Combine parametrized fixtures with @pytest.mark.parametrize for matrix testing. If you have a parametrized fixture with 3 database engines and a parametrize decorator with 4 input values, pytest generates 12 test cases.
6. Fixture Factories
When a test needs to create multiple instances with different configurations, return a factory function instead of a value. This pattern is called a fixture factory:
@pytest.fixture
def make_user():
created_users = []
def _make_user(name="Test User", role="viewer", active=True):
user = User(name=name, role=role, active=active)
user.save()
created_users.append(user)
return user
yield _make_user
# Cleanup: delete all created users
for user in created_users:
user.delete()
def test_admin_can_delete_users(make_user):
admin = make_user(name="Admin", role="admin")
target = make_user(name="Target", role="viewer")
admin.delete_user(target.id)
assert User.get(target.id) is None
def test_viewer_cannot_delete_users(make_user):
viewer = make_user(name="Viewer", role="viewer")
target = make_user(name="Target", role="viewer")
with pytest.raises(PermissionError):
viewer.delete_user(target.id)
The factory pattern gives tests full control over the created objects while keeping cleanup centralized in the fixture. This is cleaner than creating multiple separate fixtures for each variation.
7. Yield Fixtures: Setup and Teardown
Yield fixtures split a fixture into setup (before yield) and teardown (after yield). The yielded value is what the test receives. Teardown code runs after the test finishes, regardless of whether it passed or failed:
@pytest.fixture
def temp_database():
# Setup
db = Database.create("test_db")
db.migrate()
db.seed(test_data)
yield db # Test receives this value
# Teardown (always runs)
db.drop()
db.disconnect()
This replaces the classic try/finally pattern and is more readable than request.addfinalizer. Use yield fixtures whenever your resource needs cleanup:
@pytest.fixture
def mock_server():
server = MockHTTPServer(port=9999)
server.start()
yield server
server.shutdown()
@pytest.fixture
def temp_config_file(tmp_path):
config = tmp_path / "config.yaml"
config.write_text("debug: true\nlog_level: INFO\n")
yield config
# tmp_path handles cleanup automatically,
# but you could add extra teardown here
def test_app_reads_config(mock_server, temp_config_file):
app = Application(config=temp_config_file, api_url=mock_server.url)
assert app.config["debug"] is True
Important: If setup code raises an exception before yield, the teardown code does not run because the fixture was never fully created. Use request.addfinalizer if you need cleanup even after partial setup.
8. conftest.py: Sharing Fixtures
conftest.py is a special file that pytest discovers automatically. Fixtures defined in it are available to all test files in the same directory and all subdirectories, with no import needed:
# tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def app():
"""Create the Flask/Django test application."""
app = create_app(testing=True)
return app
@pytest.fixture
def client(app):
"""Create a test client for HTTP requests."""
return app.test_client()
@pytest.fixture
def auth_headers():
"""Provide authentication headers for API tests."""
token = generate_test_token(user_id=1, role="admin")
return {"Authorization": f"Bearer {token}"}
You can have multiple conftest.py files at different levels of your test directory hierarchy:
tests/
conftest.py # Shared by all tests
test_models.py
api/
conftest.py # Shared by api tests only
test_users.py
test_products.py
integration/
conftest.py # Shared by integration tests only
test_workflows.py
Fixtures in a deeper conftest.py can override fixtures from a parent directory. A fixture named client in tests/api/conftest.py takes precedence over one with the same name in tests/conftest.py for all tests under tests/api/.
Common patterns for conftest.py:
- Root conftest.py — Database connections, application instances, session fixtures
- Subdirectory conftest.py — Specialized fixtures for API tests, integration tests, or specific features
- Custom markers and hooks — Register custom markers, configure test collection
9. Built-in Fixtures
Pytest ships with several fixtures you can use without defining them yourself. These cover the most common testing needs.
tmp_path and tmp_path_factory
tmp_path provides a unique temporary directory as a pathlib.Path object. It is scoped to each test function and cleaned up after the session:
def test_write_config(tmp_path):
config_file = tmp_path / "settings.json"
config_file.write_text('{"debug": true}')
assert config_file.exists()
assert "debug" in config_file.read_text()
def test_process_csv(tmp_path):
input_file = tmp_path / "data.csv"
output_file = tmp_path / "result.csv"
input_file.write_text("name,age\nAlice,30\nBob,25\n")
process_csv(input_file, output_file)
assert output_file.exists()
tmp_path_factory is session-scoped and lets you create multiple named temporary directories, useful in session or module fixtures:
@pytest.fixture(scope="session")
def shared_data_dir(tmp_path_factory):
return tmp_path_factory.mktemp("shared_data")
monkeypatch
monkeypatch modifies objects, dictionaries, and environment variables for the duration of a test. All changes are reverted automatically after the test:
def test_api_call_with_custom_url(monkeypatch):
monkeypatch.setenv("API_URL", "https://test.api.com")
client = APIClient.from_env()
assert client.base_url == "https://test.api.com"
def test_datetime_mock(monkeypatch):
import datetime
class FakeDate(datetime.date):
@classmethod
def today(cls):
return cls(2026, 1, 1)
monkeypatch.setattr(datetime, "date", FakeDate)
assert datetime.date.today().year == 2026
def test_disable_network(monkeypatch):
def block_requests(*args, **kwargs):
raise ConnectionError("Network disabled in tests")
monkeypatch.setattr("urllib.request.urlopen", block_requests)
Key monkeypatch methods:
setattr(target, name, value)— Replace an attribute on an object or moduledelattr(target, name)— Remove an attributesetenv(name, value)— Set an environment variabledelenv(name, raising=True)— Remove an environment variablesetitem(dict, key, value)— Set a dictionary keydelitem(dict, key)— Remove a dictionary keysyspath_prepend(path)— Prepend tosys.pathchdir(path)— Change the current working directory
capsys and capfd
capsys captures output written to sys.stdout and sys.stderr. capfd captures at the file descriptor level, catching output from C extensions and subprocesses too:
def test_greeting_output(capsys):
print_greeting("Alice")
captured = capsys.readouterr()
assert "Hello, Alice" in captured.out
assert captured.err == ""
def test_error_logging(capsys):
log_error("Something went wrong")
captured = capsys.readouterr()
assert "ERROR" in captured.err
def test_subprocess_output(capfd):
subprocess.run(["echo", "hello"], check=True)
captured = capfd.readouterr()
assert "hello" in captured.out
Call readouterr() multiple times to capture output in segments. Each call resets the capture buffer.
Other Built-in Fixtures
caplog— Capture log records from theloggingmodulerecwarn— Record warnings issued during a testpytestconfig— Access to the pytest configuration objectcache— Store and retrieve values across test runsdoctest_namespace— Inject names into doctest execution
10. The request Fixture
The special request fixture provides information about the requesting test. It is primarily used in parametrized fixtures and for conditional fixture behavior:
@pytest.fixture(params=["json", "xml", "csv"])
def export_format(request):
"""Provide different export formats."""
return request.param
@pytest.fixture
def configured_client(request):
"""Create a client configured based on test markers."""
marker = request.node.get_closest_marker("slow")
timeout = 30 if marker else 5
return HTTPClient(timeout=timeout)
@pytest.fixture
def resource(request):
"""Access information about the requesting test."""
print(f"Setting up for: {request.node.name}")
print(f"Test module: {request.module.__name__}")
print(f"Fixture scope: {request.scope}")
return SomeResource()
Useful request attributes:
request.param— Current parameter value in parametrized fixturesrequest.node— The test item or collector requesting the fixturerequest.scope— The fixture scope (function, class, module, session)request.module— The Python module where the test is definedrequest.config— The pytest configuration objectrequest.addfinalizer(func)— Register a teardown callback
11. Fixture Finalization and Cleanup
Beyond yield fixtures, you can register explicit finalizers using request.addfinalizer. This is useful when you need multiple cleanup steps or when cleanup must run even if setup partially fails:
@pytest.fixture
def database(request):
db = Database.connect("test_db")
request.addfinalizer(db.disconnect)
db.create_tables()
request.addfinalizer(db.drop_tables)
db.seed_data()
request.addfinalizer(db.clear_data)
return db
Finalizers run in reverse registration order (LIFO), so in this example: clear_data, then drop_tables, then disconnect. If create_tables raises an exception, only the disconnect finalizer runs because the later finalizers were never registered.
Compare the two approaches:
# Yield approach: simpler, but all-or-nothing teardown
@pytest.fixture
def server():
srv = Server()
srv.start()
yield srv
srv.shutdown() # Only runs if start() succeeded
# Finalizer approach: granular cleanup
@pytest.fixture
def server(request):
srv = Server()
request.addfinalizer(srv.release_port) # Always runs
srv.start()
request.addfinalizer(srv.shutdown) # Only if start() succeeded
return srv
For most cases, yield fixtures are preferred for their readability. Use addfinalizer when you need the extra control over partial setup cleanup.
12. Best Practices
Keep fixtures focused
Each fixture should do one thing. Instead of a massive fixture that creates a database, seeds data, and configures settings, compose multiple small fixtures:
# Bad: monolithic fixture
@pytest.fixture
def everything():
db = create_db()
db.seed()
app = create_app(db=db)
client = app.test_client()
return client
# Good: composed fixtures
@pytest.fixture(scope="session")
def db():
return create_db()
@pytest.fixture
def seeded_db(db):
db.seed()
yield db
db.clear()
@pytest.fixture
def app(seeded_db):
return create_app(db=seeded_db)
@pytest.fixture
def client(app):
return app.test_client()
Name fixtures clearly
Use descriptive names that indicate what the fixture provides, not what it does internally. authenticated_client is better than setup_auth. sample_order is better than create_order_fixture.
Use the narrowest scope possible
Function scope is the safest default. Only widen the scope when you have measured the performance impact and confirmed that shared state does not cause test interference. A test suite where order matters is a broken test suite.
Avoid fixture side effects
Fixtures should not modify global state unless they also restore it. If a fixture changes an environment variable, a class attribute, or a module-level setting, use monkeypatch or a yield fixture to revert the change.
Prefer fixtures over setup/teardown methods
The setUp/tearDown methods from unittest still work in pytest, but fixtures are more flexible. You can share them across classes, parametrize them, compose them, and control their scope independently:
# unittest style (less flexible)
class TestOrders(unittest.TestCase):
def setUp(self):
self.db = create_db()
self.db.seed()
def tearDown(self):
self.db.clear()
# pytest style (composable, shareable, parametrizable)
@pytest.fixture
def seeded_db():
db = create_db()
db.seed()
yield db
db.clear()
def test_order_total(seeded_db):
order = seeded_db.get_order(1)
assert order.total == 59.99
Use conftest.py strategically
Do not put every fixture in the root conftest.py. Place fixtures at the directory level where they are used. If only API tests need an api_client fixture, put it in tests/api/conftest.py, not the root.
Document complex fixtures
Add docstrings to fixtures. When a test fails, pytest --fixtures shows fixture docstrings, making it easier for teammates to understand what each fixture provides and how to use it.
For a broader introduction to pytest and its testing philosophy, see our Python Testing with Pytest guide. If you are setting up a Python project from scratch and need to configure virtual environments for your test dependencies, check out the Python Virtual Environments guide.
Frequently Asked Questions
What is a pytest fixture and why should I use one?
A pytest fixture is a function decorated with @pytest.fixture that provides reusable setup logic for your tests. Instead of duplicating database connections, test data, or mock objects in every test function, you define a fixture once and request it by name as a parameter. Fixtures handle both setup and teardown, support dependency injection, and can be scoped to run once per function, class, module, or session. They replace the setUp/tearDown pattern from unittest with a more flexible, composable approach.
What is the difference between function, class, module, and session fixture scope?
Fixture scope controls how often the fixture is created and destroyed. Function scope (the default) creates a new instance for every test function. Class scope creates one instance shared across all tests in a class. Module scope creates one instance per test file. Session scope creates a single instance shared across the entire test run. Use narrower scopes for test isolation and wider scopes for expensive resources like database connections or API clients that are safe to share.
How does conftest.py work and where should I put it?
conftest.py is a special file that pytest automatically discovers and loads. Fixtures defined in it are available to all test files in the same directory and subdirectories without needing an import. You can have multiple conftest.py files at different directory levels. Place shared fixtures at the appropriate directory level. Root-level conftest.py fixtures are available everywhere, while a conftest.py inside tests/api/ only applies to tests in that folder.
What is a yield fixture and when should I use it?
A yield fixture uses the yield keyword instead of return to provide a value to the test. Code before yield runs as setup, and code after yield runs as teardown, guaranteed to execute even if the test fails. Use yield fixtures when you need cleanup logic such as closing database connections, removing temporary files, or restoring original state. This pattern replaces try/finally blocks and ensures deterministic resource cleanup.
How do I use monkeypatch to mock dependencies in pytest?
monkeypatch is a built-in pytest fixture that lets you modify objects, dictionaries, and environment variables during a test, with all changes automatically reverted afterward. Use monkeypatch.setattr() to replace functions or class attributes, monkeypatch.setenv() to set environment variables, monkeypatch.delenv() to remove them, and monkeypatch.setitem() to modify dictionaries. Unlike unittest.mock.patch, monkeypatch integrates naturally with pytest fixtures and requires no decorators or context managers.