Back to Blog
Python

Python Pytest Coverage with pytest-cov

python pytest coverage with pytest cov: Learn how to measure code coverage with pytest using pytest-cov, configure thresholds, generate reports, and integrate coverage...

pytestcode coveragepytest-covtestingcoverage reporting
A visual representation of code coverage measurement with pytest-cov showing a progress bar and coverage percentage.

When you run pytest, you often want to know how much of your code is actually exercised by your tests. The pytest-cov plugin integrates coverage.py with pytest, giving you a simple way to measure python pytest coverage with pytest cov and turn that measurement into actionable reports.

Installing pytest-cov and Running Your First Coverage Report

Start by installing the plugin into your environment:

pip install pytest-cov

Once installed, run pytest with the --cov flag and point it at the package or module you want to measure:

pytest --cov=myproject

This executes your test suite and prints a coverage summary to the terminal. The summary shows the percentage of statements executed in each file, along with missing line numbers. For example:

Name Stmts Miss Cover ---------------------------------------- myproject/__init__.py 10 2 80% myproject/core.py 50 5 90% ---------------------------------------- TOTAL 60 7 88%

The --cov option accepts a path to a package, module, or even a directory. You can also pass multiple paths by repeating the flag:

pytest --cov=myproject --cov=myproject.utils

If you omit the value, pytest-cov measures coverage for all imported code, which is rarely what you want because it includes third-party libraries. Always specify the code you own.

Understanding Coverage Options and Report Formats

By default, pytest-cov prints a terminal report. You can control the output format with --cov-report. Common options are term, html, xml, and json.

To generate an HTML report that you can open in a browser:

pytest --cov=myproject --cov-report=html

This creates an htmlcov/ directory with an index.html file. The HTML report shows line-by-line highlighting of covered and missed lines, which helps when you need to inspect specific branches or statements.

For CI systems, an XML report is often more useful because tools like SonarQube or Codecov can ingest it:

pytest --cov=myproject --cov-report=xml

The XML file is written to coverage.xml by default. You can combine multiple report formats in a single run:

pytest --cov=myproject --cov-report=term --cov-report=html

This prints the terminal summary and writes the HTML report at the same time.

Using a Configuration File for Consistent Coverage Settings

Typing the same --cov flags on every command is error-prone. You can store them in a pytest configuration file so that a plain pytest invocation picks them up automatically.

In pytest.ini:

[pytest] addopts = --cov=myproject --cov-report=term --cov-report=html

If you use pyproject.toml, add a [tool.pytest.ini_options] section:

[tool.pytest.ini_options] addopts = "--cov=myproject --cov-report=term --cov-report=html"

With this configuration, every test run produces both the terminal summary and an HTML report. You can still override these options on the command line, but the defaults are now consistent across your team and CI.

Setting Coverage Thresholds to Enforce Quality

A coverage percentage alone does not guarantee quality, but a threshold gives you a concrete gate for regressions. The --cov-fail-under option makes pytest exit with a non-zero status if the total coverage falls below a given percentage.

pytest --cov=myproject --cov-fail-under=80

If the total coverage is 79% or lower, pytest exits with status 1, which causes a CI build to fail. This is useful for preventing coverage from silently dropping over time.

You can also set the threshold in the configuration file:

[pytest] addopts = --cov=myproject --cov-fail-under=80

Be careful with this setting in large legacy codebases. A high threshold may force you to write tests for code that is not worth testing, while a low threshold gives a false sense of security. Choose a value that reflects the risk profile of your project.

Excluding Code from Coverage

Not every line of code needs to be covered. For example, debug-only branches, version-compatibility shims, or code that runs only under a specific interpreter version may be impractical to test. coverage.py provides two ways to exclude code.

First, you can add a # pragma: no cover comment to a line or block:

def debug_only_function(): if __debug__: print("Debugging output") # pragma: no cover

For a whole block, place the comment on the if or def line:

def platform_specific_behavior(): # pragma: no cover if sys.platform == "win32": return "Windows" else: return "Linux"

Second, you can configure exclusions in a .coveragerc file or in the [tool.coverage] section of pyproject.toml. For example, to exclude all except clauses that only re-raise an exception:

[tool.coverage.report] exclude_lines = ["if __debug__:", "raise AssertionError"]

Excluding code should be a deliberate decision. If you exclude too much, your coverage number becomes meaningless. Use pragmas sparingly and document why a particular block is excluded.

Combining Coverage Across Multiple Test Runs

In a large project, you might split your test suite into several invocations—for example, unit tests and integration tests run separately. By default, each pytest run starts with a clean coverage state. To accumulate coverage across runs, use the --cov-append flag.

pytest --cov=myproject tests/unit pytest --cov=myproject --cov-append tests/integration

The second run reads the data from the first and combines it. This is useful when different test directories require different fixtures or database setups.

However, be aware that --cov-append only works if the same coverage data file is used. By default, pytest-cov writes to .coverage in the current directory. If you run tests from different directories, you need to set the COVERAGE_FILE environment variable to a shared path.

Operational Considerations: Performance and CI Integration

Coverage measurement adds overhead to every test run because the interpreter must track which lines are executed. The overhead is usually a few percent, but it can be significant for test suites that are already slow. If you need a quick smoke test, run pytest without --cov; reserve coverage measurement for full CI runs.

Branch coverage, enabled with --cov-branch, provides a more detailed view of which branches were taken, but it increases the overhead further. Use it when you need to understand edge cases in complex conditionals.

In CI, you typically want a single command that produces a report and enforces a threshold. A common pattern is:

pytest --cov=myproject --cov-report=xml --cov-report=term --cov-fail-under=80

This generates an XML artifact for external tools and fails the build if coverage drops below 80%. Keep the threshold in the configuration file so that local runs match CI behavior.

Another operational detail is that coverage data files (.coverage, htmlcov/, coverage.xml) should be added to .gitignore. They are generated artifacts and should not be committed.

Finally, remember that coverage measures which lines were executed, not whether assertions were meaningful. A high coverage percentage does not guarantee that your tests catch bugs. Use coverage as one signal among many, and combine it with mutation testing or property-based testing when you need stronger guarantees.

python pytest coverage with pytest cov: Practical Usage and | RYUSLOG DEV