Learning Goals
3 min- Explain what line coverage measures (and what it doesn't).
- Run
coverage.pyand read the percentage report. - Find the exact unexecuted lines (
Missingcolumn). - Understand branch coverage vs line coverage.
Warm-Up · Which Lines Ran?
5 mindef grade(score): if score >= 80: return "A" # ← did any test hit this? elif score >= 50: return "B" # ← or this? else: return "F" # ← or this? # a test for grade(90) only runs the first branch. # coverage shows you the others were NEVER executed.
Coverage instruments your code and records which lines ran during the tests. A line that never ran was never tested — a blind spot. Coverage doesn't prove correctness, but it reliably finds the code your tests forgot.
New Concept · Measuring Coverage
14 minInstall & run
pip install coverage coverage run -m pytest # run tests, recording coverage coverage report # print the summary coverage report -m # ALSO show the missing line numbers
Reading the report
Name Stmts Miss Cover Missing ------------------------------------------------- grades.py 7 2 71% 6-7 cart.py 20 0 100% ------------------------------------------------- TOTAL 27 2 93%
Stmts total executable statements Miss statements NOT run by any test Cover percentage executed Missing the exact line numbers never run ← go test these!
Line coverage vs branch coverage
def f(x): if x > 0: log("positive") # line coverage: this line ran return x # but did we test the x <= 0 case (no log)?
line coverage "was this LINE executed?"
branch coverage "was each BRANCH (if-taken AND if-skipped) executed?"
stricter — catches untested "else" / fall-through pathscoverage run --branch -m pytest # enable branch coverage
What coverage does NOT mean
100% coverage ≠ bug-free! A line can run without your test ASSERTING anything about it. Coverage finds UNtested code; it can't prove tested code is CORRECT.
Worked Example · Find & Close a Gap
12 min# grades.py def grade(score): if score < 0 or score > 100: raise ValueError("0-100 only") if score >= 80: return "A" if score >= 50: return "B" return "F"
# test_grades.py — incomplete on purpose from grades import grade def test_a(): assert grade(90) == "A" def test_b(): assert grade(60) == "B"
$ coverage run -m pytest && coverage report -m Name Stmts Miss Cover Missing ------------------------------------------------- grades.py 6 2 67% 2, 6 ------------------------------------------------- # line 2 = the ValueError guard; line 6 = the "F" return # → never tested!
Coverage names the gaps: the error guard and the "F" case. Close them:
import pytest def test_fail(): assert grade(20) == "F" # covers line 6 def test_out_of_range(): # covers line 2 with pytest.raises(ValueError): grade(150)
$ coverage run -m pytest && coverage report grades.py 6 0 100%
Read the diff
Coverage turned "I think my grade function is tested" into "lines 2 and 6 are NOT" — then guided you to exactly the two tests needed. That's the workflow: run coverage, read Missing, write a test for each gap, repeat. Next lesson plugs this into pytest directly with pytest-cov.
Try It Yourself
13 minRun coverage run -m pytest then coverage report -m on one of your modules. What's the percentage? Which lines are missing?
Write a test for each missing line until the module hits 100% line coverage.
Run with --branch. Find a function that is 100% line coverage but NOT 100% branch coverage. Add the test that covers the missing branch.
Mini-Challenge · 100% Honestly
8 minTake a module with branches and error paths. Drive it to 100% branch coverage. Then prove the danger: keep 100% coverage while DELETING all your assertions (just call the functions). Coverage stays 100% but the tests check nothing — demonstrating coverage ≠ correctness.
Show the "coverage lies" demo
# this test has 100% coverage of grade() but asserts NOTHING: def test_calls_everything(): grade(90); grade(60); grade(20) try: grade(150) except ValueError: pass # no assert! coverage = 100%, but a bug in grade() goes undetected
The lesson: coverage measures execution, not verification. Aim for high coverage WITH meaningful assertions — never coverage for its own sake.
Recap
3 mincoverage run -m pytest + coverage report -m shows which lines your tests executed and which they missed. Branch coverage (--branch) is stricter, catching untested if/else paths. Use the Missing column to find gaps and write targeted tests. But remember: coverage finds untested code — it can't prove tested code is correct. Next: integrating this with pytest via pytest-cov.
Vocabulary Card
- line coverage
- Percentage of code lines executed by the test run.
- branch coverage
- Whether each decision branch (taken AND not-taken) was executed.
- Missing
- The report column listing line numbers never run.
- coverage ≠ correctness
- Running a line isn't the same as asserting it behaves correctly.
Homework
4 minPick a module with branches and error paths. Measure coverage, then write tests to reach 100% branch coverage with meaningful assertions. Submit the before/after coverage reports and your test file.
Follow the grades.py example. Use coverage run --branch -m pytest and chase the Missing lines until they're all covered — with real asserts, not the "coverage lies" trick.