Coverage shows which lines and paths a test suite executed. It helps find forgotten behavior, but does not measure assertion quality or prove that code is defect-free.

Measure lines and branches

python -m pip install coverage pytest
coverage run --branch -m pytest
coverage report -m
coverage html

Inspect htmlcov/index.html. Branch coverage reveals decisions where only one destination ran, even when every line appears covered.

[run]
branch = True
source = src

[report]
show_missing = True
fail_under = 85

Use a threshold to stop accidental decline, not to reward empty tests. Exclude code only with a reviewable reason. The pytest guide covers test design, while fixtures and mocks help isolate dependencies.

Prioritize business rules, error handling, and external boundaries. A trivial covered line and a financial decision count equally in the percentage but carry different risk. Check for tests that execute code without asserting its result.

The official coverage.py documentation, accessed July 22, 2026, explains statement and branch measurement and report formats. Treat coverage as an investigation map, not a standalone goal.

Understand measurement, combination, and reporting

coverage.py separates executing code from presenting results. coverage run collects data in a .coverage file, coverage report prints a terminal summary, coverage html builds a browsable report, and XML or JSON output serves automated integrations. You can therefore produce several reports without rerunning the suite.

Start measurement from a predictable directory. When suites run in separate processes or jobs, produce distinct data files and combine them before reporting:

coverage erase
coverage run --parallel-mode -m pytest tests/unit
coverage run --parallel-mode -m pytest tests/integration
coverage combine
coverage report -m

coverage erase prevents old data from contaminating the current result. Parallel mode creates separate files, and coverage combine merges them. In distributed CI, download every job's coverage artifacts into a common directory first. Compare percentages only when they use the same configuration and source set.

Prefer branch coverage for decisions

Line coverage records whether a line ran, but it can hide a missing route through a decision:

def shipping_cost(total: float, express: bool) -> float:
    if total >= 200 or express:
        return 0
    return 18

A test with total=250 reaches the free result but says nothing about express=True with a smaller order. Another case must exercise the paid result. Branch coverage tracks transitions out of decisions and marks partial branches in HTML.

This does not require a test for every mechanical combination. Select cases that represent behavior: below the threshold, exactly at it, express shipping, and invalid input if the function owns that validation. Assertions must inspect the returned value and meaningful effects, not merely call the function.

Configure the measured source

The source setting lets coverage.py identify modules that could have run, including files never imported by the suite. Match it to the real layout:

[run]
branch = True
source =
    src
omit =
    */tests/*
    */migrations/*

[report]
show_missing = True
skip_covered = False
precision = 1
fail_under = 85

[html]
directory = htmlcov

Do not copy exclusions without reviewing the repository. Generated migrations might be reasonable to omit, while a hand-written data transformation can deserve direct tests. Test modules are usually outside the production metric, but complex test helpers may still need their own verification.

Working directory matters as well. Running subsets from different locations can produce paths that do not merge cleanly. Standardize local and CI commands, and version one configuration in .coveragerc, pyproject.toml, or setup.cfg.

Use exclusions sparingly

Some lines are platform-specific or defensive. coverage.py understands markers such as # pragma: no cover, but each exception should survive review:

if TYPE_CHECKING:
    from external_package import Client

if __name__ == "__main__":  # pragma: no cover
    main()

Before excluding code, ask whether its behavior can be tested through a public interface. Do not mark difficult blocks merely to improve a dashboard. Broad exclusions reduce the report's ability to reveal future risk. Regular-expression exclusion rules also need narrow patterns and an explanation.

Set a useful threshold policy

There is no universal target. A new package may begin with a high threshold; an established system can record its baseline and block decline while improving important modules. A global fail_under is easy to enforce, but it allows untested new code to be offset by thoroughly tested old code.

During review, inspect changed lines and ask which new paths were added. An authentication module at 90% may carry more risk than a formatting helper at 60%. Combine the automated gate with scenario review, regression tests for discovered defects, and examination of missing lines.

Be deliberate about rounding. precision controls display, while the threshold is evaluated against measured data. Keep local and CI commands identical so developers see the same outcome before pushing.

Integrate pytest and subprocesses

Running coverage run -m pytest makes the measurement layer explicit. The pytest-cov plugin offers pytest-native options but is not required. Choose one supported command and document it; mixing invocations can lead to different source filters, branch settings, or erased data.

Subprocess code requires additional setup because a new Python interpreter does not automatically inherit measurement. Follow the subprocess guidance for the installed coverage.py version and confirm that parallel data files are written and combined. Never assume the parent process represents workers, queue consumers, or external commands.

Read reports as diagnostic evidence

Start with missing and partial lines in high-risk modules. For error handling, test network failure, malformed data, permissions, and resource cleanup. For loops, include empty collections and early exits. For compound conditions, look for short-circuit behavior that prevents an operand from running.

A genuine coverage improvement includes an assertion that would fail if behavior changed. Ask whether replacing the returned value or removing a side effect would break the test. Mutation testing can deepen that investigation, but it does not replace thoughtful scenario design.

In CI, publish the minimum report needed for review and control access to HTML when the source is private. Do not upload configuration containing secrets or indiscriminately archive the workspace. The useful artifact is the report, not the entire execution environment.

Review coverage as part of test design

When a change modifies a conditional, inspect missing lines in that area before chasing unrelated uncovered code. Ask what observable behavior each branch represents and test through the public interface when possible. These tests survive refactoring better than checks coupled to private details.

Coverage can also expose dead code. Before adding a test solely to execute an unreachable fallback, confirm whether that branch still belongs in the product. Removing obsolete logic may improve clarity more than manufacturing a test for it. Keep exclusions narrow and explain why defensive code cannot run in a supported environment.

Review the HTML report occasionally even when CI enforces a threshold. Its per-file view reveals clusters of missed behavior that one percentage hides. Combine that evidence with bug history and module criticality to choose the next testing investment.