Pytest is a Python testing framework that discovers and runs test functions by name convention (any function prefixed test_), rewrites their assert statements at import time via an AST transform to produce rich failure diffs, and provides a fixture system that manages test dependencies and shared state across parametrized, isolated, and parallelized test suites—all without subclassing unittest.TestCase.
<svg viewBox="0 0 760 470" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace,monospace">
<rect width="760" height="470" fill="#0d1117"/>
<text x="380" y="32" text-anchor="middle" fill="#e6edf3" font-size="21" font-weight="700">pytest Fixture Scope & Execution Model</text>
<text x="380" y="52" text-anchor="middle" fill="#8b98a5" font-size="13">Fixture lifetimes, conftest.py resolution, and parallelism via pytest-xdist</text>
<!-- Fixture scope ladder -->
<text x="30" y="82" fill="#8b98a5" font-size="12">Fixture scope — wider = fewer setups, wider sharing</text>
<!-- session scope -->
<rect x="30" y="92" width="700" height="36" rx="4" fill="#1f6feb" fill-opacity="0.15" stroke="#1f6feb" stroke-width="1.5"/>
<text x="46" y="112" fill="#58a6ff" font-size="12" font-weight="700">session</text>
<text x="140" y="112" fill="#8b98a5" font-size="11">setup once per pytest run · DB connection, auth token, compiled binary · ~0.1 s amortized to ~0 µs/test</text>
<!-- module scope -->
<rect x="30" y="134" width="700" height="36" rx="4" fill="#238636" fill-opacity="0.12" stroke="#238636" stroke-width="1"/>
<text x="46" y="154" fill="#3fb950" font-size="12" font-weight="700">module</text>
<text x="140" y="154" fill="#8b98a5" font-size="11">setup once per .py file · temporary schema, loaded fixture data · shared across all tests in file</text>
<!-- class scope -->
<rect x="30" y="176" width="700" height="36" rx="4" fill="#8957e5" fill-opacity="0.12" stroke="#8957e5" stroke-width="1"/>
<text x="46" y="196" fill="#a371f7" font-size="12" font-weight="700">class</text>
<text x="140" y="196" fill="#8b98a5" font-size="11">setup once per TestClass · instance-level shared state · rarely needed in function-style tests</text>
<!-- function scope -->
<rect x="30" y="218" width="700" height="36" rx="4" fill="#f0883e" fill-opacity="0.12" stroke="#f0883e" stroke-width="1"/>
<text x="46" y="238" fill="#f0883e" font-size="12" font-weight="700">function</text>
<text x="140" y="238" fill="#8b98a5" font-size="11">default — setup + teardown per test · tmp_path, monkeypatch, capsys · ~2 µs overhead</text>
<!-- conftest.py resolution -->
<text x="30" y="278" fill="#8b98a5" font-size="12">conftest.py resolution — traverses upward from test file to root</text>
<text x="46" y="298" fill="#6e7681" font-size="11">tests/unit/test_auth.py</text>
<text x="46" y="316" fill="#6e7681" font-size="11">tests/unit/conftest.py ← unit-level fixtures (mock DB)</text>
<text x="46" y="334" fill="#6e7681" font-size="11">tests/conftest.py ← suite-level fixtures (session DB, auth token)</text>
<text x="46" y="352" fill="#6e7681" font-size="11">conftest.py ← project-level (env setup, plugins)</text>
<line x1="36" y1="290" x2="36" y2="360" stroke="#30363d" stroke-width="1"/>
<!-- xdist parallelism -->
<text x="420" y="278" fill="#8b98a5" font-size="12">pytest-xdist -n 8 workers</text>
<!-- worker bars -->
<rect x="420" y="290" width="300" height="14" rx="2" fill="#1f6feb" fill-opacity="0.6"/>
<text x="728" y="301" fill="#58a6ff" font-size="10">w0</text>
<rect x="420" y="308" width="300" height="14" rx="2" fill="#238636" fill-opacity="0.6"/>
<text x="728" y="319" fill="#3fb950" font-size="10">w1</text>
<rect x="420" y="326" width="300" height="14" rx="2" fill="#8957e5" fill-opacity="0.6"/>
<text x="728" y="337" fill="#a371f7" font-size="10">w2</text>
<rect x="420" y="344" width="300" height="14" rx="2" fill="#f0883e" fill-opacity="0.6"/>
<text x="728" y="355" fill="#f0883e" font-size="10">w3–7</text>
<text x="420" y="374" fill="#6e7681" font-size="10">I/O-bound: ~7.5× speedup · CPU-bound: ~6.4×</text>
<!-- assertion rewriting box -->
<rect x="30" y="382" width="360" height="72" rx="4" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<text x="46" y="400" fill="#8b98a5" font-size="11">Assertion rewriting (AST transform at import)</text>
<text x="46" y="418" fill="#f85149" font-size="10">assert result == expected → FAILED</text>
<text x="46" y="434" fill="#6e7681" font-size="10"> Left: {'a': 1, 'b': 2, 'c': 3}</text>
<text x="46" y="448" fill="#6e7681" font-size="10"> Right: {'a': 1, 'b': 9, 'c': 3} diff shown inline</text>
<text x="380" y="462" text-anchor="middle" fill="#6e7681" font-size="11">1,400+ pytest plugins on PyPI — xdist, cov, mock, hypothesis, benchmark, asyncio</text>
</svg>
Pytest's assertion rewriting is the mechanism that makes bare assert statements produce informative failure messages—at collection time, pytest's import hook intercepts each test module and rewrites every assert node in the AST to capture the left- and right-hand subexpressions before evaluation, so a failing assert result == expected prints a structured diff of both values rather than the generic AssertionError Python would raise. This rewriting is purely syntactic: no monkey-patching of assert, no assertEqual/assertIn wrappers required. Dicts produce key-by-key comparison, lists show the first differing index, multiline strings render as unified diffs, and dataclasses display field-by-field. The AST transform adds approximately 2 ms to module import time for a 500-line test file—negligible against test execution cost.
Fixture scope is the primary tool for controlling test isolation versus setup cost, and choosing the wrong scope is the most common source of both slow test suites and flaky tests. A function-scoped fixture (the default) is set up and torn down around every individual test at ~2 µs overhead—appropriate for monkeypatch, tmp_path, and capsys where per-test isolation is mandatory. A session-scoped fixture is created once per pytest invocation and shared across every test that requests it: starting a Docker container, compiling a Cython extension, or establishing a database connection incurs the cost once (~0.1 s) rather than thousands of times. Using function scope for an expensive resource and session scope for mutable state are mirror-image bugs that produce slow suites and cross-test contamination respectively.
Parametrize expands a single test function into N independent test cases—each with its own ID in the report, its own pass/fail status, and its own re-run target under --lf. Decorating with @pytest.mark.parametrize("x,y", [(1,2),(3,4),(5,6)]) registers three entries in the collection phase; @pytest.mark.parametrize stacking two decorators with M and N values each registers M × N combinations. The --lf (last-failed) flag re-runs only the test IDs that failed in the previous session: if 2 out of 1,000 parametrized cases failed, the next invocation executes 2 tests rather than 1,000—a 500× reduction in feedback latency during a debugging cycle.
Conftest.py files are discovered by traversing from the test file upward to the filesystem root, allowing fixture definitions to be scoped to a subtree without any explicit import. A conftest.py at tests/unit/ defines fixtures available only to tests/unit/**; one at tests/ defines fixtures shared across the entire suite; one at the project root configures plugins and marks. This traversal mirrors how pyenv resolves .python-version files. Fixtures defined in inner conftest.py files shadow outer ones of the same name, making it possible to override a production database fixture with a mock for unit tests without changing any test file.
Property-based testing via hypothesis integrates directly with pytest and generates up to 100 random examples per test case, finding edge cases that hand-written parametrize lists miss. A @given(st.integers()) decorator drives pytest to call the test function repeatedly with values covering boundary integers (0, 1, −1, sys.maxsize, sys.minint), then shrinks any failing input to the smallest reproducing example automatically. Branch coverage from pytest-cov --cov-branch finds approximately 30% more code paths than line coverage alone, because it tracks both the taken and not-taken branch of every conditional rather than simply whether the line executed.
Pytest-xdist distributes test execution across 8 worker processes and delivers approximately 7.5× speedup for I/O-bound suites and ~6.4× for CPU-bound ones. Each worker is a separate Python interpreter; session-scoped fixtures run once per worker (not once globally), so truly global resources—a shared read-only database, a compiled binary—must be managed via a tmp_path_factory lock file or an external service. Running pytest -n auto sets worker count to the machine's CPU count; combining with --dist loadfile groups tests by source file so that module-scoped fixtures are set up only once per worker rather than once per test-file boundary.
| Feature | pytest | unittest |
|---|---|---|
| Test discovery | test_.py / _test.py by name | TestCase subclass required |
| Fixture injection | Function argument by name | setUp/tearDown methods |
| Assertion diff | AST-rewritten assert | assertEqual, assertIn... |
| Parametrize | @pytest.mark.parametrize | subTest or manual loop |
| Collection time (10k tests) | ~2 s | ~4 s |
| Plugin ecosystem | 1,400+ on PyPI | stdlib only |
PYTEST EXECUTION FLOWCHART
pytest invoked
│
▼
┌─────────────────────┐
│ Collection phase │ discover test_*.py, rewrite assert AST
│ ~2 s / 10k tests │ build fixture dependency graph
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ Session fixtures │ set up once (DB, auth token, binary)
│ (scope=session) │ ~0.1 s amortized to ~0 µs/test
└────────┬────────────┘
│
xdist? │
┌───────┴───────┐
YES NO
▼ ▼
┌──────────┐ ┌──────────────────────┐
│ N workers│ │ Sequential execution │
│ 7.5× I/O│ │ function fixtures: │
│ 6.4× CPU│ │ setup → test → tear │
└────┬─────┘ └──────────┬───────────┘
└────────┬───────────┘
▼
┌─────────────────────┐
│ Report + exit code │ --lf saves failures for next run
│ 0=pass, 1=fail │ 500× faster re-run on 2/1000 fails
└─────────────────────┘
Read pytest through a fixture dependency graph lens rather than a test runner lens. The framework's real job is not executing functions called test_—it is resolving a directed acyclic graph of named dependencies (fixtures) at each scope level, ensuring each node is set up exactly once per its declared scope and torn down in reverse order. Every feature—parametrize, conftest traversal, xdist worker isolation, the --lf cache—is a consequence of operating on that graph, and every pytest performance or flakiness problem reduces to a fixture whose scope is misaligned with its actual sharing requirements.
Explore 500+ Semiconductor & AI Topics
From EUV lithography to CUDA optimization — search the full knowledge base or chat with our AI assistant.