pytest-cov
pytest-cov wires coverage.py into pytest so that pytest --cov=mypkg measures which lines your tests execute and prints a table at the end of the run. It is a thin plugin, not a coverage engine: everything about what gets measured and how reports look comes from coverage.py and its config file. What the plugin adds is the pytest integration people actually want: it erases the old data file at the start, starts measurement before your code is imported, combines results from pytest-xdist workers automatically, can tag each measured line with the test that hit it, and gives you --cov-fail-under so CI turns red on a coverage drop.
The standard way to get coverage out of a pytest suite, and worth it over plain coverage run mostly for xdist combining, per-test contexts, and consistent sys.path. Treat it as a thin wrapper: budget your configuration time for coverage.py, and keep --no-cov in muscle memory for debugging sessions.
Use it if
- You run pytest and want coverage numbers without changing how you invoke tests: coverage run -m pytest changes sys.path (it adds the current directory), which quietly changes what your imports resolve to
- You use pytest-xdist and need coverage across workers: the plugin combines each worker's data file for you, which raw coverage does not do
- You want a CI gate: --cov-fail-under=85 plus --cov-report=xml gives you a failing exit code and a file Codecov or SonarQube can read
- You want per-test attribution: --cov-context=test records the full test id (including parametrization) as the coverage context, so the HTML report shows which test covered which line
- You are debugging with a breakpoint or a profiler: coverage's tracer fights pdb and other sys.settrace users, which is exactly why the plugin ships --no-cov, and forgetting it costs you an afternoon
- You care about test suite speed: line tracing slows a run noticeably on large suites, and on Python 3.12+ you may be better off with coverage's sys.monitoring core or a faster tool such as slipcover for local loops
- You measure subprocesses and are upgrading from 6.x: pytest-cov 7 deleted the .pth file that used to make subprocess measurement automatic, so coverage silently drops to whatever the parent process ran until you add patch = subprocess to your coverage config
- You want the coverage features themselves rather than the pytest glue: everything about include, omit, exclude_lines, branch mode, and report formats lives in coverage.py, so reading the plugin's docs to configure them wastes your time
- You are treating the percentage as a quality metric: it measures lines executed, not assertions made, and a suite that imports every module and asserts nothing scores well
Setup reality
pip install pytest-cov then pytest --cov=mypkg is genuinely all it takes, and coverage[toml] comes along so pyproject.toml config works out of the box. The friction shows up later. First, --cov=mypkg names a package or path, and pointing it at your tests directory or at a source layout it cannot import gives you an empty or nonsense report. Second, the plugin's own options are a small set; include, omit, exclude_lines, and dynamic contexts all belong to coverage.py, so half your configuration lives in [tool.coverage.run] and [tool.coverage.report] and only the rest in pytest addopts. Third, pytest-cov 7 requires coverage 7.10.6 or newer and dropped the .pth trick for subprocesses, so upgrading from 6.x can drop your coverage number without any error message. Fourth, on xdist the workers each need pytest-cov installed, which matters when workers are remote interpreters.
Patterns
Measure one packagebasic-run
pytest --cov=myproj tests/
# multiple sources:
pytest --cov=myproj --cov=myplugin tests/
# measure everything that gets imported:
pytest --cov= tests/Point --cov at the package you are testing, not at tests/. A bare --cov= (empty value) disables source filtering and records every module that gets imported, which is slower but catches modules you never import in tests.
Pick report outputsreport-formats
pytest --cov=myproj \
--cov-report=term-missing:skip-covered \
--cov-report=html:build/htmlcov \
--cov-report=xmlMultiple --cov-report flags stack. Only term and term-missing accept :skip-covered; the file formats (html, xml, json, lcov, markdown, annotate) accept :DEST. Use --cov-report= with an empty value to suppress output entirely.
Fail CI below a thresholdfail-under-gate
pytest --cov=myproj --cov-fail-under=85
# override reported precision as well:
pytest --cov=myproj --cov-fail-under=85.5 --cov-precision=2The check uses the precision from your coverage config, so 84.6% passes an 85 threshold at precision 0 because it rounds to 85. Values above 100 are rejected by the argument parser.
Turn on branch coveragebranch-coverage
pytest --cov=myproj --cov-branch --cov-report=term-missingBranch mode counts both outcomes of every conditional, so switching it on always lowers your percentage. Enable it once at the start of a project rather than mid-way, when the drop looks like a regression.
Keep the configuration in pyproject.tomlconfig-in-pyproject
[tool.pytest.ini_options]
addopts = "--cov=myproj --cov-report=term-missing"
[tool.coverage.run]
branch = true
source = ["myproj"]
omit = ["*/migrations/*", "*/__main__.py"]
[tool.coverage.report]
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError"]omit, exclude_lines, and source are coverage.py settings, not plugin settings, so they never appear in --help. Putting --cov in addopts also means coverage runs during local debugging, which is why --no-cov exists.
Turn coverage off to use a debuggerdisable-for-debugging
pytest --no-cov -k test_the_broken_one
# or skip the report when the run already failed:
pytest --cov=myproj --no-cov-on-failcoverage installs a trace function, which breaks pdb stepping and some profilers. If --cov is in your addopts, --no-cov is the only way to get a clean debugging session; the plugin warns when you pass both.
Skip coverage for one testexclude-a-test
import pytest
@pytest.mark.no_cover
def test_spawns_a_debugger():
...
# or take the fixture, same effect:
def test_also_uncovered(no_cover):
...The no_cover marker and the no_cover fixture both suspend measurement for that single test. Useful for tests that themselves manipulate sys.settrace, which would otherwise corrupt the collected data.
Coverage with parallel workersxdist-parallel
pip install pytest-xdist
pytest -n auto --cov=myproj --cov-report=term-missingThe plugin collects each worker's data and combines it before reporting, so no manual coverage combine step is needed. Every worker environment must have pytest-cov installed, which bites when the workers are remote interpreters.
Measure code that runs in subprocessessubprocess-measurement
# pyproject.toml
[tool.coverage.run]
patch = ["subprocess"]
concurrency = ["multiprocessing"]
sigterm = truepytest-cov 7 removed the .pth file that used to do this automatically, so this config is now required and its absence is silent: the run succeeds and the number just falls. Needs coverage 7.10.6 or newer.
See which test covered which lineper-test-contexts
pytest --cov=myproj --cov-context=test --cov-report=html
# then open htmlcov/index.html and use the context filter"test" is the only accepted value. Contexts make the .coverage database considerably larger and the run slower, so use it when hunting dead code rather than on every CI run.
Combine several test runs into one numberappend-across-runs
pytest --cov=myproj tests/unit --cov-report=
pytest --cov=myproj tests/integration --cov-append --cov-report=term
# reset accumulated --cov values from a config file:
pytest --cov-reset --cov=other_pkgThe data file is erased at the start of each run unless --cov-append is passed, so the first command must come without it and every later one with it. --cov-report= on the early runs stops them printing half-finished tables.
Reach the coverage object from a testaccess-coverage-object
def test_inspect_coverage(cov):
if cov is None:
pytest.skip("running without --cov")
data = cov.get_data()
assert data.measured_files()The cov fixture yields the underlying coverage.Coverage instance, or None when coverage is disabled, so guard for None or the test breaks for anyone running plain pytest.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| coverage | PyPI | You want the engine directly, or you run more than pytest, and you are happy with coverage run -m pytest plus a separate report step. |
| slipcover | PyPI | Coverage tracing has made your suite too slow to run locally and you want near-zero-overhead line and branch measurement. |
| diff-cover | PyPI | You want to gate only the lines a pull request touched instead of arguing about a whole-project percentage floor. |