Measure Test Coverage with pytest-cov
How to measure and enforce Python test coverage thresholds with pytest-cov, including branch coverage, HTML reports, exclusions, and CI integration.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
pytest-cov is a pytest plugin that integrates the coverage.py library. It measures which lines of your Python code are executed during tests and reports the percentage. You can enforce minimum thresholds, generate HTML reports, and track branch coverage (if/else paths) — not just line coverage.
When to Use
- Measuring how much of your codebase is covered by tests
- Enforcing a minimum coverage threshold in CI (e.g., fail builds below 80%)
- Identifying dead code that is never executed by any test
- Generating HTML coverage reports for visual inspection of gaps
- Tracking branch coverage to ensure both sides of if/else are tested
When NOT to Use
- Chasing 100% coverage as a goal — 80-90% with good test quality is better than 100% with trivial tests
- Measuring coverage for scripts or one-off utilities — focus on tested production code
- Using coverage as the only quality metric — high coverage with bad assertions gives false confidence
Solution
Setup
pip install pytest pytest-cov
Basic coverage run
pytest --cov=myapp tests/
This prints a summary to the terminal:
---------- coverage: platform linux, python 3.12 ----------
Name Stmts Miss Cover
-------------------------------------------
myapp/__init__.py 2 0 100%
myapp/models.py 45 5 89%
myapp/services.py 80 12 85%
myapp/api.py 60 8 87%
-------------------------------------------
TOTAL 187 25 87%
Enforce minimum coverage
pytest --cov=myapp --cov-fail-under=80 tests/
If coverage falls below 80%, pytest exits with a non-zero code, failing CI.
HTML report
pytest --cov=myapp --cov-report=html tests/
Opens htmlcov/index.html in a browser. Green lines are covered, red lines are missed, with line-by-line highlighting.
Branch coverage
pytest --cov=myapp --cov-branch --cov-report=term-missing tests/
Branch coverage checks that both the if and else paths of each conditional are executed. Line coverage alone can miss branches where the condition is always true or always false in tests.
Configuration in pyproject.toml
[tool.pytest.ini_options]
addopts = "--cov=myapp --cov-report=term-missing --cov-report=html --cov-branch"
[tool.coverage.run]
source = ["myapp"]
branch = true
omit = [
"myapp/__init__.py",
"myapp/migrations/*",
"*/tests/*",
]
[tool.coverage.report]
show_missing = true
skip_covered = false
fail_under = 80
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"if __name__ == .__main__.:",
]
Excluding specific lines
def get_config(key: str, default=None):
if key in os.environ:
return os.environ[key]
return default # pragma: no cover
The # pragma: no cover comment tells coverage to ignore that line.
Excluding entire blocks
if TYPE_CHECKING:
from myapp.models import User # pragma: no cover
With the exclude_lines config above, any line matching if TYPE_CHECKING: and everything under it is excluded.
Coverage for multiprocessing
# pyproject.toml
[tool.coverage.run]
concurrency = ["multiprocessing", "thread"]
Coverage with parallel test runs
pytest -n auto --cov=myapp --cov-report=term-missing
With pytest-xdist, each worker writes its own .coverage file. Use coverage combine before generating the report:
coverage combine
coverage report
coverage html
Variants
Using coverage.py directly (without pytest)
coverage run -m pytest tests/
coverage report -m
coverage html
Coverage diff with diff-cover
pip install diff-cover
coverage xml
diff-cover coverage.xml --compare-branch=origin/main --html-report=coverage-diff.html
This shows coverage only for lines changed in the current branch — useful for PR reviews.
Coverage trends with coverage-badge
pip install coverage-badge
coverage-badge -o coverage-badge.svg
Generates an SVG badge with the current coverage percentage for your README.
Best Practices
-
For a deeper guide, see Pytest in Production Guide.
-
Set a realistic threshold (80-85%) — too high encourages trivial tests just to hit the number
-
Use branch coverage alongside line coverage — it catches untested else paths
-
Exclude migration files,
__init__.py, and test files from coverage measurement -
Use
# pragma: no coverfor debug-only code,__repr__, andif __name__blocks -
Review coverage reports regularly — find gaps in critical paths, not just overall percentage
-
Generate XML coverage for CI tools (SonarQube, Codecov, Coveralls)
Common Mistakes
- Chasing 100% coverage: writing trivial tests (
assert True) to cover lines without verifying behavior. - Not using branch coverage: line coverage of 100% can still miss
elsebranches. - Including test files in coverage:
tests/should be excluded — you’re measuring production code. - Not combining parallel coverage files: with
pytest-xdist, each worker writes a separate file. Runcoverage combinebefore reporting. - Excluding too much: if you exclude every hard-to-test line, the number becomes meaningless.
FAQ
What is the difference between line coverage and branch coverage?
Line coverage measures whether a line was executed. Branch coverage measures whether both paths of a conditional (if/else) were taken. A line with if x: can be 100% line-covered but only 50% branch-covered if x is always True in tests.
How do I exclude a whole file from coverage?
In pyproject.toml:
[tool.coverage.run]
omit = ["myapp/legacy/*", "myapp/migrations/*"]
How do I get coverage for a single test file?
pytest tests/test_models.py --cov=myapp.models --cov-report=term-missing
Can I use pytest-cov with Django or Flask?
Yes. Point --cov to your project package:
pytest --cov=myproject --cov-report=html
For Django, ensure DJANGO_SETTINGS_MODULE is set in your test configuration.
How do I fail CI only on decreased coverage?
Use diff-cover with --fail-under=100 to require 100% coverage on changed lines:
coverage xml
diff-cover coverage.xml --compare-branch=origin/main --fail-under=100
How do I exclude lines from coverage?
Add # pragma: no cover to exclude single lines. Use # pragma: no cover <reason> for documentation. Configure exclusions in .coveragerc:
[report]
exclude_lines =
pragma: no cover
def __repr__
raise NotImplementedError
if __name__ == .__main__.:
Exclude debug-only code, repr methods, and abstract method stubs. Do not exclude error handling paths — those are critical to test.
How do I measure branch coverage instead of line coverage?
Pass --cov-branch to pytest-cov or set branch = True in .coveragerc:
pytest --cov=myapp --cov-branch --cov-report=term-missing
Branch coverage reports whether both the true and false paths of each conditional were executed. It catches missing else branches and short-circuit evaluation paths that line coverage misses.
How do I generate coverage badges for my README?
Use coverage-badge to generate SVG badges from your coverage report:
pip install coverage-badge
coverage-badge -o coverage.svg
Add the badge to your README: . In CI, generate the badge as an artifact and commit it to a badges branch or upload to a badge service like shields.io.
How do I handle coverage with multiprocessing?
Use coverage with --concurrency=multiprocessing in .coveragerc:
[run]
concurrency = multiprocessing
parallel = True
This spawns separate coverage data files per process. Run coverage combine after the test suite to merge them. Without this, coverage from child processes is lost.
How do I integrate coverage with GitHub Actions?
Add a step in your workflow to run pytest with coverage and upload the report:
- run: pytest --cov=myapp --cov-report=xml
- uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
Codecov posts a comment on PRs with coverage diff and visualizes uncovered lines. Use fail_under in .coveragerc to fail the CI job if coverage drops below a threshold.
Related Resources
Pytest Fixtures and Parametrize
How to use pytest fixtures and @pytest.mark.parametrize to write data-driven tests with reusable setup logic across Python projects.
RecipeMeasure Test Coverage
How to measure, report, and enforce code coverage with branch and condition coverage using pytest-cov, nyc, and JaCoCo for meaningful quality gates.
RecipeMock External APIs with responses Library
How to mock HTTP API calls in Python tests using the responses library, including status codes, headers, JSON bodies, and error simulation.
RecipeProperty-Based Testing with Hypothesis
How to use Hypothesis for property-based testing in Python, generating hundreds of test cases automatically from strategies instead of writing them by hand.