mrkeyoor.com_
Thu 06 Aug 01:02 UTC
PyPITestingupdated 05 Aug 2026

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.

Verdict

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.

API stability4/5The --cov flags have looked the same for years, but version 7 removed subprocess measurement outright, which is a silent behavior change rather than a loud break: your command line still works and your number just drops.
Docs3/5readthedocs covers the flags, xdist, contexts, and subprocess migration, but the split with coverage.py's own docs is never made obvious, so people hunt in the wrong manual for omit and exclude_lines.
Maintenance4/5Pushed April 2026 under the pytest-dev org with 160 open issues (168 counting PRs); releases are regular and the 7.0 changelog explains the subprocess removal and its migration path in detail.
Ecosystem5/5The default coverage step in almost every Python CI template, understood by Codecov, Coveralls, and SonarQube through its xml and lcov output, and it composes with pytest-xdist and diff-cover without extra glue.

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
Skip it if

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=xml

Multiple --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=2

The 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-missing

Branch 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-fail

coverage 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-missing

The 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 = true

pytest-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_pkg

The 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

PackageRegistryPick it when
coveragePyPIYou want the engine directly, or you run more than pytest, and you are happy with coverage run -m pytest plus a separate report step.
slipcoverPyPICoverage tracing has made your suite too slow to run locally and you want near-zero-overhead line and branch measurement.
diff-coverPyPIYou want to gate only the lines a pull request touched instead of arguing about a whole-project percentage floor.