diff-cover review
diff-cover 10.5.1 compares the lines in a Git diff with Cobertura, Clover, JaCoCo XML, or LCOV coverage data. That produces a coverage percentage for changed code while leaving old untouched lines outside the calculation. The same distribution includes `diff-quality`, which limits supported lint results to changed lines. The current release indexes Clover paths instead of scanning the report again for each source file and turns a missing Git executable into a readable error. Our clean Python 3.12 installation was small and free of known vulnerabilities, but the tool still depends on a correctly fetched base branch and matching file paths.
diff-cover is a practical ratchet for repositories carrying old coverage debt, provided CI makes the base ref and source paths deterministic. It should sit beside a whole-project report because changed-line coverage cannot expose untouched gaps.
We installed it
| Install | ✓ · 0.3s | 6 packages on disk · 10 MB |
| Import | ✓ | import diff_cover in 0.22s · pure Python · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does diff-cover install cleanly?
Yes. In a fresh container with an empty cache, pip install diff-cover finished in 0.3s, leaving 6 packages and 10 MB on disk. pip-audit reported no known vulnerabilities.
What does diff-cover need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import diff_cover succeeded in 0.22s.
diff-cover or coverage: which should you use?
coverage: Use it to collect Python coverage, combine data files, and enforce a whole-project threshold without Git diff analysis. diff-cover is a practical ratchet for repositories carrying old coverage debt, provided CI makes the base ref and source paths deterministic.
When should you not use diff-cover?
Your repository does not use Git. The README lists Git as a requirement, and 10.5.1 only improves the error shown when the executable is missing.
Use it if
- A legacy repository cannot pass a sensible whole-project coverage floor yet, but every new change should carry tests.
- CI already produces Cobertura, Clover, JaCoCo XML, or LCOV and can fetch the branch used for comparison.
- Reviewers need a local HTML, JSON, or Markdown artifact listing uncovered changed lines.
- The same changed-line policy should apply to a supported checker such as Ruff, Flake8, Pylint, Checkstyle, or Clang.
- Your repository does not use Git. The README lists Git as a requirement, and 10.5.1 only improves the error shown when the executable is missing.
- Your test system cannot emit Cobertura, Clover, JaCoCo XML, or LCOV. Console summaries and proprietary report formats are not accepted inputs.
- You need the overall test health of the repository. A perfect diff score can coexist with large untouched areas that have no coverage.
- Coverage paths differ from repository paths and cannot be normalized. The troubleshooting section ties the empty-result message to source names that fail to match `git diff` paths.
- Every physical line in a multiline statement must be counted exactly. Coverage reports often name only executable statement lines; `--expand-coverage-report` is an XML-only approximation that copies the previous reported hit count.
Setup reality
We installed diff-cover 10.5.1 in a fresh Python 3.12 Bookworm container. pip finished in 0.3 seconds, leaving 6 packages and 10 MB on disk. The package has 5 direct dependencies, requires Python 3.10 or newer, and is pure Python without py.typed. import diff_cover worked in 0.22 seconds. pip-audit reported zero known vulnerabilities.
diff-cover does not execute tests or create coverage files. Generate a supported report first, enter the Git worktree, and run the command from a directory where report paths match repository paths. Its default base is origin/main. Shallow CI clones often lack that ref, while repositories with another default branch measure the wrong range unless the workflow fetches the base and passes --compare-branch. Version 10.5.1 reports missing Git cleanly instead of leaking a FileNotFoundError.
A low percentage fails the job only when --fail-under is set. With several coverage files, a line counts as covered if any input covers it. JaCoCo users may need --src-roots to connect package paths to Java files. Multiline expansion works only for XML and assumes an omitted line has the preceding reported line's hit count. Review that assumption before turning it into a gate.
Optional settings can live in a .toml file after installing diff-cover[toml]; positional report files remain on the command line. The default three-dot Git range means changes since the branches diverged, while --diff-range-notation=.. compares the two tips. A saved patch can replace branch lookup through --diff-file. diff-quality needs its selected linter installed separately and may need --report-root-path when a prebuilt report came from another directory.
Patterns
Generate XML and inspect changed lines measure-pytest-changes
pytest --cov=src --cov-report=xml:coverage.xml
diff-cover coverage.xml --compare-branch=origin/main --show-uncoveredRun both steps from the repository root, and fetch `origin/main` in CI before asking Git to compare against it.
Fail below the review policy enforce-diff-threshold
diff-cover coverage.xml \
--compare-branch=origin/main \
--fail-under=90 \
--show-uncoveredReporting alone does not fail on a poor percentage. `--fail-under` supplies the exit-code policy.
Count partial branches check-branch-coverage
diff-cover coverage.xml \
--compare-branch=origin/main \
--branch-coverage \
--fail-under=90Branch-aware handling arrived in 10.4.0. The input report must contain branch coverage data for this option to add useful information.
Merge coverage evidence from several jobs combine-test-suites
diff-cover unit.xml integration.xml browser.lcov \
--compare-branch=origin/main \
--fail-under=85Inputs use OR semantics. A changed line is covered when at least one supplied report marks it covered.
Write HTML and Markdown reports publish-local-formats
diff-cover coverage.xml \
--format html:artifacts/diff.html,markdown:artifacts/diff.mdCreate the destination directory first. Uploading or commenting with these files remains the CI provider's job.
Use an explicit Git patch analyze-saved-patch
git diff origin/main...HEAD > change.diff
diff-cover coverage.xml --diff-file=change.diff --fail-under=90This is useful when a CI system supplies a patch or keeps little branch history. Ensure the patch and report refer to the same checkout.
Tell JaCoCo where Java lives map-jacoco-sources
diff-cover target/site/jacoco/jacoco.xml \
--src-roots src/main/java generated/src/main/java \
--compare-branch=origin/mainThe documented defaults are `src/main/java` and `src/test/java`. Add source roots when JaCoCo package paths do not resolve to repository files.
Remove generated files from the gate exclude-generated-paths
diff-cover coverage.xml \
--exclude 'src/generated/*' '*/migrations/*' \
--fail-under=90Exclusions use fnmatch against paths relative to the Git directory. Quote patterns to prevent shell expansion.
Choose local change states control-worktree-inputs
diff-cover coverage.xml --ignore-unstaged --include-untrackedStaged and unstaged changes are included by default. Untracked files require `--include-untracked`.
Approximate omitted statement lines expand-multiline-xml
diff-cover coverage.xml --expand-coverage-report --show-uncoveredThis works only with XML. It assigns an absent line the hit count of the preceding reported line, which may not match every producer's semantics.
Move optional flags into TOML load-toml-policy
# install
pip install 'diff-cover[toml]==10.5.1'
# diff-cover.toml
[tool.diff_cover]
compare_branch = "origin/main"
fail_under = 90
show_uncovered = true
# run
diff-cover coverage.xml -c diff-cover.tomlThe filename must end in `.toml`. Coverage report paths are mandatory command arguments, and command-line options win over file values.
Score Ruff violations on changed lines gate-ruff-on-diff
diff-quality \
--violations=ruff.check \
--compare-branch=origin/main \
--fail-under=100Install Ruff in the job first. diff-quality invokes the selected checker but does not add that checker as a dependency.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| coverage | PyPI | Use it to collect Python coverage, combine data files, and enforce a whole-project threshold without Git diff analysis. |
| pytest-cov | PyPI | Use it when pytest should collect coverage and write the XML file that a later diff-cover step consumes. |
| pycobertura | PyPI | Use it to inspect and compare Cobertura reports locally when a changed-line Git policy is unnecessary. |
More testing guides
pytest · chai · jsdom · vitest · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

