Overview
pytest replaces unittest's class-based, boilerplate-heavy structure with plain functions, plain assert statements, and a fixture system for reusable setup, tests read like ordinary Python instead of a specialized testing DSL. Fixtures are the core mechanic: small, composable functions that provide setup (and teardown, via yield) to any test that requests them by name, with a scope setting controlling how often each one is recreated. Most confusing pytest test failures, state leaking between tests, tests passing only in a certain order, trace back to a fixture scoped broader than the state it holds can safely support.
Quick Reference
| Concept | Purpose |
|---|---|
assert x == y | Plain assertion; pytest rewrites it for detailed failure output |
@pytest.fixture | Reusable, injectable test setup |
| Fixture scope | function (default), class, module, session |
yield in a fixture | Everything after yield runs as teardown |
@pytest.mark.parametrize | Run one test function against multiple input sets |
Syntax
import pytest
@pytest.fixture
def client():
app = create_app(testing=True)
yield app.test_client()
# teardown code (if any) goes here, after yield
def test_health_check(client):
response = client.get("/health")
assert response.status_code == 200Examples
@pytest.mark.parametrize("input,expected", [
(2, 4),
(3, 9),
(-2, 4),
])
def test_square(input, expected):
assert square(input) == expectedBroad fixture scope + mutable state = flaky tests
A session-scoped fixture holding mutable state (a database connection with rows inserted by one test) leaks that state into every other test sharing it. Default to function scope, and widen it only for genuinely expensive, stateless setup.
Visual Diagram
Common Mistakes
- Scoping a fixture broader (
session,module) than the state it holds can safely support, causing order-dependent test failures. - Putting teardown logic after a
returninstead of ayield, code afterreturnin a fixture never runs, since the function has already exited. - Writing tests that depend on execution order instead of being fully independent, which pytest doesn't guarantee unless explicitly configured.
- Reaching for
unittest.mock.patchbroadly instead of designing code with dependency injection in mind, making tests fragile to internal refactors that shouldn't affect test outcomes.
Performance
- Fixture scope directly trades test isolation for speed,
session-scoped fixtures for genuinely expensive, read-only setup (like starting a test database container once) meaningfully speed up a large suite compared to recreating it per test. pytest-xdistparallelizes test execution across multiple processes, which is usually the highest-leverage speed improvement for a large, mostly-independent test suite.- Overusing broad-scope fixtures for convenience rather than genuine performance need reintroduces the exact state-leakage risk that made
functionscope the safer default.
Best Practices
- Default every fixture to
functionscope, and only widen it when the setup is genuinely expensive and provably safe to share (read-only, no per-test mutation). - Use
yieldfixtures for anything needing teardown, keeping setup and teardown co-located in one readable function. - Use
@pytest.mark.parametrizeinstead of copy-pasting near-identical test functions for different inputs. - Keep tests independent of execution order, if a test only passes after another specific test runs first, that's a fixture-scope or shared-state bug to fix, not a test-ordering constraint to document.