Measure Test Coverage in Python

Learn how to use coverage.py to measure which lines of your Python code are tested and which are not, so you can identify gaps in your test suite.

7 min read

Test coverage measures which lines of your code actually run during your test suite. A line that never runs during any test is a line whose behavior is unverified. It might work, or it might contain a bug waiting for the right input.

Coverage measurement tells you where untested lines live so you can decide whether they need a test.

The standard tool for measuring Python test coverage is coverage.py, a third-party package that tracks every line your tests execute and produces reports showing covered and missed code. This article builds on the test suite you set up in organizing and running Python tests and shows you how to install coverage.py, run your tests under it, and read the reports.

Installing coverage.py

coverage.py is not part of the standard library, so you install it with pip like any other third-party package before you can measure anything:

bashbash
pip install coverage

The package provides the coverage command with three subcommands you will use regularly: run executes your tests under measurement, report prints a summary of results, and html generates a browsable line-by-line report.

Running tests under coverage

The coverage run command wraps your test runner and records which lines execute. Replace the python in your normal test command with coverage run. The same pattern works for both pytest and unittest:

bashbash
coverage run -m pytest

The command works the same way for unittest; only the module you run under coverage run -m changes, since coverage itself does not care which test runner produced the calls it is tracking:

bashbash
coverage run -m unittest discover

Either command runs your tests exactly as usual while coverage tracks every line behind the scenes, saving the data to a .coverage file in your project directory. You do not need to change any test code or configuration to start measuring.

If your source code lives in a specific package, add the --source option to limit measurement to your code and exclude library files:

bashbash
coverage run --source=my_package -m pytest

Without this option, coverage measures every Python file that runs during tests, including standard library modules and third-party packages. Limiting the source keeps the report focused on your code.

Reading the coverage report

After running your tests, generate a text report with coverage report:

bashbash
coverage report

The output shows one row per source file with columns for total statements, missed statements, and coverage percentage, plus a TOTAL row summarizing the whole project:

texttext
Name                      Stmts   Miss  Cover
---------------------------------------------
my_package/__init__.py        4      0   100%
my_package/models.py         32      3    91%
my_package/utils.py          18      7    61%
my_package/views.py          45      0   100%
---------------------------------------------
TOTAL                        99     10    90%

The Cover column is the percentage of statements that ran at least once during tests. The Miss column counts statements that never executed.

Focus on files with low coverage percentages. Those are the modules most likely to contain untested bugs.

Add the -m flag to see which specific lines are missed. This adds a Missing column listing the line numbers of unexecuted statements:

bashbash
coverage report -m

You can cross-reference these numbers with your source code to find the exact branches and functions that need tests.

Generating an HTML report

The text report is a good summary, but the HTML report is where coverage measurement becomes actionable. Generate it with coverage html:

bashbash
coverage html

Open the generated htmlcov/index.html file in your browser. Each file is displayed with line-by-line highlighting: green lines ran during tests, red lines never ran.

You can click into any file to see exactly which branches executed and which code paths were never touched.

This view is especially useful for finding partially tested functions. A function might show as mostly green, but a red line inside an if block reveals that the condition was never true during any test. That is the line you need to write a new test for.

Excluding code from coverage

Not every line of code deserves a test. Debug-only logging, unreachable error handlers, and main-guard blocks are often impractical to test and safe to exclude. Add a # pragma: no cover comment to exclude a specific line:

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

For larger blocks, most developers exclude the entire block rather than annotating each line. Coverage also supports configuration-based exclusion in .coveragerc or pyproject.toml for patterns like assert statements or abstract method bodies.

Interpreting coverage results

High coverage does not guarantee good tests. A test that calls a function without checking its return value still counts as covering that code. Coverage tells you which code ran, not whether the assertions were correct.

Use coverage as a discovery tool, not a scorecard. When you see a red line, ask whether the behavior on that line matters.

If it does, write a test that exercises it with meaningful assertions. If it does not, consider whether the code itself is necessary.

Branch coverage goes beyond line coverage by checking whether both paths of every conditional executed. Enable it with the --branch flag:

bashbash
coverage run --branch -m pytest

Branch coverage catches a common gap: a line of code may have run, but the else branch of an if statement on the same line may not have. Without branch coverage, both paths on a one-line conditional count as covered if either path executes.

Integrating coverage into your workflow

Run coverage before merging changes or as part of your CI pipeline. Compare the current coverage against the previous run to catch regressions. A drop in coverage signals new code without tests.

The next article covers debugging Python code with pdb, which is what you use when a test fails and you need to step through the code to understand why.

Rune AI

Rune AI

Key Insights

  • coverage run executes your tests under measurement; coverage report prints a summary of line coverage per file.
  • coverage html generates an HTML report with line-by-line highlighting of covered and missed code.
  • Set the source option to limit measurement to your package and exclude test files and library code.
  • Use # pragma: no cover to exclude lines that should not be counted, like debug-only blocks.
  • Focus coverage efforts on branch coverage in critical code paths, not on hitting an arbitrary percentage.
RunePowered by Rune AI

Frequently Asked Questions

What is a good test coverage percentage to aim for?

There is no universal number. 100% coverage means every line runs during tests, but it does not mean every behavior is tested correctly. Most projects aim for 80-90% as a practical target, with critical paths like payment processing or authentication aiming higher. Focus on covering branches and edge cases in important code rather than hitting an arbitrary percentage across the entire project.

Does coverage.py slow down my tests?

Yes, coverage measurement adds overhead because it tracks every line executed. Expect tests to run about 20-50% slower under coverage. This is fine for a coverage run that you do before committing or in CI, but you may want to run tests without coverage during active development for faster feedback.

How do I exclude test files and setup code from coverage reports?

Create a .coveragerc file or add a [tool.coverage.run] section to pyproject.toml and set source to your package directory. For example, source = ["my_package"] tells coverage to only measure code inside that directory. You can also add # pragma: no cover comments to exclude specific lines like debug-only code or unreachable error handlers.

Conclusion

Coverage measurement turns a subjective feeling about test quality into objective data. You can see exactly which lines run and which do not, then decide where to focus your next test. Use coverage run to collect data, coverage report for a quick summary, and coverage html for line-by-line detail.