How Coverage.py Measures Statement and Branch Coverage

coverage.py is the standard tool for measuring code coverage in Python, determining precisely which parts of a codebase run during automated tests. To calculate statement and branch coverage, the tool combines static source-code analysis with dynamic execution tracing. It parses the Python Abstract Syntax Tree (AST) to identify every executable line and potential control-flow branch, and then hooks into Python's execution runtime to record which lines and transitional paths actually execute.

Tracing Execution at Runtime

At the core of coverage.py is its execution tracer. When running a test suite under coverage monitoring, the tool hooks into the Python interpreter using one of three mechanisms:

  1. The C Tracer Extension: By default, coverage.py uses an optimized C-extension that hooks into PyEval_SetTrace. This approach minimizes performance overhead while tracking execution events.
  2. sys.settrace: If the C extension is unavailable, coverage.py falls back to the pure-Python sys.settrace function. This callback intercepts interpreter events such as function calls, line executions, and returns.
  3. PEP 669 Monitoring: In modern Python versions (3.12+), coverage.py can use the low-overhead monitoring API defined by PEP 669, which generates events specifically for line execution and branching without the heavy overhead of traditional tracing.

Measuring Statement Coverage

Statement coverage measures whether each individual executable statement in a file was executed at least once. coverage.py computes this in two phases:

1. Static Analysis (Finding Executable Lines)

Before execution completes, coverage.py compiles the target Python source files into ASTs and bytecode. This analysis filters out non-executable code, including:

The result is a set of line numbers representing all possible executable statements for each file.

2. Dynamic Tracking

Whenever the tracer detects that the interpreter has advanced to a new line, it records that line number into an in-memory set.

At report time, statement coverage is calculated using a straightforward formula:

\[\text{Statement Coverage} = \frac{\text{Executed Statements}}{\text{Total Executable Statements}} \times 100\]

Measuring Branch Coverage

Branch coverage (enabled via the --branch flag) determines whether every boolean decision point in the code evaluated to both True and False, exploring all possible execution paths.

Rather than simply tracking whether a line was reached, branch coverage tracks arcs—the transitions from one line to another.

1. Generating the Control Flow Graph (CFG)

coverage.py analyzes the AST of each file to map out the possible control flow jumps. For every decision point, it identifies the source line and all valid target destination lines:

Each possible pathway is represented as a directed pair of line numbers: (from_line, to_line).

2. Tracking Arcs

When branch coverage is enabled, the runtime tracer records line transitions rather than solitary line numbers. It keeps track of the previously executed line and records the pair (last_line, current_line) as an executed arc.

3. Calculating the Branch Ratio

At the end of execution, coverage.py compares the executed arcs against the list of expected arcs compiled from the AST. A branch is marked as:

Branch coverage provides a more thorough assessment than statement coverage alone, exposing edge cases and untested fallback logic within conditional blocks.