Cutting Test Time with Pytest Session and Module Fixtures
Re‑use expensive resources in your test suite with pytest’s session and module fixture scopes. Learn how to set up a persistent HTTP client and a per‑file database fixture, verify lifetimes, and avoid shared‑state pitfalls.
16 Aug 2026, 14:39 UTC

The Problem
When a test suite starts to grow, the cost of repeatedly creating expensive resources—like a database container, a mock server, or a HTTP client—becomes a bottleneck. Each test function normally runs in isolation, but that isolation forces the fixture to run for every test, even when the underlying resource could be reused safely.
Understanding Fixture Scopes
Pytest fixtures can be declared with five scopes: function, class, module, package, and session. The scope determines how long the fixture instance lives:
| Scope | Lifetime | Typical use |
|---|---|---|
| function | Each test function | Fast, immutable objects |
| class | All tests in a test class | Shared setup for a class |
| module | All tests in a file | Heavy per‑file setup (e.g., migrations) |
| package | All tests in a package | Package‑wide resources |
| session | Entire test run | Long‑lived services (Docker, HTTP client) |
When a fixture uses yield instead of return, the code before yield runs during setup and the code after it runs during teardown. The teardown executes only once when the fixture’s scope ends.
Practical Example: A Session‑Scoped HTTP Client
Suppose you need a persistent HTTP client that connects to a test server. Re‑creating the client for each test adds latency. Define a session‑scoped fixture in conftest.py:
# conftest.py
import pytest
import httpx
@pytest.fixture(scope="session")
def http_client():
"""Create a single HTTP client for the entire test session."""
client = httpx.Client(base_url="http://localhost:8000")
yield client
client.close()
Now any test that accepts http_client will receive the same instance. Pytest guarantees that the client is created once, used by all tests, and closed after the last test in the session finishes.
To confirm the behaviour, run:
pytest --setup-show -k "http_client" tests/test_api.py
The output will show the http_client fixture being set up only once, regardless of how many test functions use it.
Adding a Module‑Scoped Database Fixture
For tests that need a database, you might want to run migrations only once per file. A module‑scoped fixture is ideal:
# conftest.py
import pytest
from myapp.db import init_db, teardown_db
@pytest.fixture(scope="module")
def db_session():
"""Run migrations once per test module."""
init_db()
yield
teardown_db()
Tests in tests/test_users.py can depend on both http_client and db_session without incurring the cost of repeated migrations.
Trade‑offs & Best Practices
- Shared State Risks: If a test mutates a shared fixture (e.g., appending to a list inside
http_client), later tests may see the mutated state. Keep shared fixtures immutable or provide explicit reset logic. - Isolation vs. Speed: Session‑scoped fixtures sacrifice the isolation that function‑scoped fixtures provide. Use them only when the resource is stateless or you can guarantee cleanup.
- Visibility Control: Place
conftest.pyat the root of the test tree for session‑scoped fixtures to be automatically available. Useautouse=Truesparingly; explicit injection improves readability. - Teardown Order: Pytest tears down fixtures in reverse order of their creation. If a module‑scoped fixture depends on a session‑scoped one, the session fixture is torn down last.
- Verify with
--fixtures: Runpytest --fixturesto see where each fixture is defined and its scope.
Takeaway
Using session and module scopes can dramatically reduce test runtime by reusing expensive resources. The key is to ensure the shared objects are either immutable or reset between tests. Verify fixture lifetimes with --setup-show and keep an eye on the teardown order to avoid hidden coupling. With these practices, you’ll keep your suite fast without compromising reliability.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.