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:
pip install coverageThe 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:
coverage run -m pytestThe 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:
coverage run -m unittest discoverEither 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:
coverage run --source=my_package -m pytestWithout 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:
coverage reportThe 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:
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:
coverage report -mYou 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:
coverage htmlOpen 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:
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:
coverage run --branch -m pytestBranch 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
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.
Frequently Asked Questions
What is a good test coverage percentage to aim for?
Does coverage.py slow down my tests?
How do I exclude test files and setup code from coverage reports?
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.
More in this topic
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
Organize and Run Python Tests
Learn how to structure test directories, run tests with unittest and pytest, configure test discovery, and integrate tests into your workflow.
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.