Coverage & Reporting
Coverage measures which lines your tests execute. It reveals untested branches, not test quality - use it to find gaps, not as a quality score alone.
Recipe
uv add --group dev pytest-cov
uv run pytest --cov=src --cov-report=term-missingWhen to reach for this:
- Identifying untested code paths before release
- Enforcing minimum coverage in CI
- Generating HTML reports for review
Working Example
[tool.pytest.ini_options]
addopts = "--cov=src --cov-report=term-missing --cov-fail-under=80"
[tool.coverage.run]
source = ["src"]
branch = true
omit = ["*/tests/*", "*/__main__.py"]
[tool.coverage.report]
show_missing = true
skip_covered = falseuv run pytest
# Name Stmts Miss Branch BrPart Cover
# src/myapp/billing.py 42 3 12 2 91%uv run pytest --cov=src --cov-report=html
open htmlcov/index.htmlWhat this demonstrates:
--cov=srcmeasures the installed package, not testsbranch = truetracks if/else paths--cov-fail-under=80blocks CI below threshold- HTML report highlights uncovered lines in red
Deep Dive
Reading the Report
| Column | Meaning |
|---|---|
| Stmts | Executable statements |
| Miss | Statements never run |
| Branch | Conditional branches |
| BrPart | Partially covered branches |
What Coverage Misses
- Correct assertions (100% coverage with wrong tests)
- Integration failures
- Concurrency bugs
- API contract violations
Gotchas
- Chasing 100% coverage - tests assert nothing meaningful. Fix: 80-90% target; focus on critical paths.
- Coverage on
tests/- inflated numbers. Fix:--cov=srconly. - No branch coverage - missed else branches. Fix:
branch = truein coverage config. - Coverage gate without review - skipped important branches. Fix: pair threshold with PR review of
term-missingoutput. - Generated code in coverage - impossible targets. Fix:
omitmigrations and generated files.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| mutation testing (mutmut) | Test quality audit | Quick gap finding |
| diff-cover | PR-scoped coverage | Full project audit |
| No coverage tool | Prototype | Production services |
FAQs
What is a good coverage target?
80% is a common floor. Critical modules (billing, auth) should be higher.
Does coverage mean good tests?
No. It only shows executed lines. Pair with review and mutation testing.
How do I exclude lines?
# pragma: no cover on defensively unreachable code.
How do I see uncovered branches?
--cov-report=term-missing with branch = true.
Can I enforce coverage in CI?
--cov-fail-under=80 in pytest addopts.
How do I coverage a single module?
pytest --cov=src/myapp/billing.
What is diff-cover?
Reports coverage on changed lines only. Good for PR gates.
HTML or terminal report?
Terminal for CI. HTML for local exploration of gaps.
How do I combine coverage from parallel runs?
coverage combine then coverage report after pytest-xdist runs.
Should I cover __init__.py?
Only if it contains logic. Empty __init__.py files are omitted.
Related
- pytest Setup - addopts config
- Testing Best Practices - coverage policy
- Property-Based Testing - deeper path coverage
- Linting in CI/CD - CI gates
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.