Back to Blog
Python

Python Coverage: Branch Coverage and HTML Reports

python coverage branch coverage and html reports: Learn how to measure branch coverage with Python's coverage.py, generate detailed HTML reports, and interpret the res...

coverage.pybranch coverageHTML reportstestingcode quality
Illustration of Python code with branch coverage report showing covered and missed branches in an HTML view.

python coverage branch coverage and html reports requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you run coverage run without extra options, Python's coverage.py measures line coverage: it records which lines of code executed during the test run. Line coverage is useful, but it can give a false sense of security. A line containing an if statement is marked as covered even if only one branch of that condition was ever taken. Branch coverage closes that gap by tracking each possible outcome of a conditional statement. This article explains how to enable branch coverage in coverage.py, generate HTML reports, and read the results to find untested logical paths.

Why Line Coverage Misses Logical Paths

Consider a simple function with a conditional:

def classify(value): if value > 10: return "large" else: return "small"

If your test suite only calls classify(20), line coverage reports 100% because both the if line and the return "large" line execute. The else branch never runs, but line coverage does not care. Branch coverage, on the other hand, sees that the else branch was not taken and marks the branch as partial. This distinction matters because untested branches often hide bugs. A function may behave correctly for the tested path but fail on the alternative path.

Branch coverage is not a replacement for line coverage; it is a stricter metric. When you enable it, coverage.py still tracks line coverage but also records which branch of each conditional was taken. The reported percentage reflects both line and branch coverage combined, so it is always lower than or equal to line coverage for the same test run.

Enabling Branch Coverage in coverage.py

The coverage command-line tool accepts a --branch flag that activates branch measurement. The most common invocation runs tests with pytest:

coverage run --branch -m pytest

This runs your test suite and records both line and branch coverage. You can also enable branch coverage persistently by adding a configuration file. Coverage.py reads .coveragerc, pyproject.toml, or setup.cfg. In .coveragerc, add:

[run] branch = True

In pyproject.toml, under the [tool.coverage.run] section:

[tool.coverage.run] branch = true

Using a configuration file is preferable for projects where multiple developers or CI systems run coverage, because it ensures consistent behavior without remembering the flag.

Generating the HTML Report

Once you have collected coverage data, generating an HTML report is straightforward:

coverage html

This creates a directory named htmlcov containing an index.html file and per-module HTML pages. Open htmlcov/index.html in a browser to see an overview of all measured modules. The report shows the coverage percentage for each file, and you can click through to see line-by-line highlighting. Lines that executed are shown in green, lines that did not execute in red, and lines with partial branch coverage in yellow.

The HTML report is self-contained; it uses relative paths and embedded CSS, so you can share it with team members or host it on a static server. If you need to deploy it to a CI artifact, you can change the output directory with coverage html -d path/to/dir.

Reading the Branch Coverage Report

The HTML report includes a column labeled "Covered" that shows the combined line and branch coverage percentage. For each file, you can expand the view to see which branches were missed. Coverage.py marks a line as partial when a conditional statement has both taken and untaken branches. For example, an if statement with an else branch that never executes will show as partial, with the else portion highlighted in red.

To understand exactly which branch is missing, click on the yellow line. Coverage.py inserts a small annotation showing the branch transitions. For instance, if the condition value > 10 is always true, the report will indicate that the false branch was never taken. This level of detail helps you write a test that exercises the missing path.

Branch coverage also applies to other constructs beyond if statements. Boolean expressions with and and or have multiple sub-branches. Coverage.py treats each operand as a separate branch. For example, in if a and b:, there are two branches: one where a is false (short-circuit) and one where a is true and b is evaluated. If your tests never hit the case where a is false, that branch is marked as missed.

Interpreting Partial Branches

A partial branch does not always mean your tests are inadequate. Sometimes a branch is impossible to hit given the current code. For example, a function may have an if condition that is always true because of an earlier validation. Coverage.py cannot know that; it only sees that the false branch was never executed. In such cases, you can exclude the branch from the report using the exclude_lines option in the configuration, but this should be done sparingly. More often, a partial branch indicates a missing test case.

Consider this function:

def process(data): if data is None: return [] return data.upper()

If every test passes a non-None value, the if branch is never taken. The HTML report will show the if line as partial. To achieve full branch coverage, you need a test that calls process(None). This is exactly the kind of edge case that branch coverage surfaces.

When you see a partial branch, ask whether the branch is logically reachable. If it is, add a test. If it is not, consider simplifying the code or adding an explicit assertion that documents the invariant. Avoid blindly suppressing branches with # pragma: no cover because that can hide real gaps.

Branch Coverage in a Test Workflow

Branch coverage is most useful when integrated into your regular test workflow. A common pattern is to run coverage as part of a CI pipeline and fail the build if the branch coverage drops below a threshold. You can set a threshold in the configuration file:

[report] fail_under = 80

This makes coverage report exit with a non-zero status if the total coverage is below 80%. The coverage report command prints a text summary to the terminal, which is useful for quick checks. For a more detailed view, the HTML report remains the primary tool.

You can also combine branch coverage with test isolation tools like pytest-cov, which wraps coverage.py and adds a --cov flag. However, using coverage.py directly gives you finer control over the HTML report and configuration. The choice depends on your project's existing setup.

Performance Cost of Branch Measurement

Enabling branch coverage adds overhead to your test run. Coverage.py must instrument every conditional expression and record which path was taken. This increases execution time and memory usage, though the exact impact depends on the size of the codebase and the number of tests. For a typical project, the slowdown is noticeable but acceptable during local development. In CI, you can mitigate the cost by running coverage only on the main test suite and not on every pull request, or by using parallel coverage collection to speed up large suites.

Memory usage also increases because coverage.py stores more data per branch. This is rarely a problem for ordinary projects, but if you are measuring a very large codebase, you may want to monitor memory consumption. If the overhead becomes prohibitive, consider running branch coverage only on critical modules or during nightly builds rather than on every commit.

The HTML report generation itself is fast; the main cost is during the test run. You can reduce the data collected by using the --include and --omit options to restrict coverage to specific packages, which also speeds up execution.

Branch coverage is a valuable addition to your testing toolkit. It reveals untested logical paths that line coverage misses, and the HTML report makes those gaps visible and actionable. By enabling --branch and generating HTML reports, you can systematically improve the robustness of your test suite.

python coverage branch coverage and html reports: Practical | RYUSLOG DEV