Learning Goals
3 min- Explain why 100% coverage is usually the wrong goal.
- Pick a target based on risk, not vanity.
- Focus coverage on the code that matters most.
- Recognise "coverage theatre" — tests with no assertions.
Warm-Up · The Last 10% Costs the Most
5 min0% → 70% fast, high value — covers the main logic
70% → 90% moderate effort — edge cases, error paths
90% → 100% expensive — defensive code, "impossible" branches,
__repr__, logging lines, platform-specific bitsCoverage has diminishing returns. The first 70-80% catches most real bugs cheaply; the last few percent often means testing trivial or near-impossible code. Aim for "high coverage of the code that matters", not 100% of everything.
New Concept · Choosing a Target
14 minWhy not 100%?
- The last few percent is usually trivial code (
__repr__, a logging line) or genuinely unreachable defensive branches. - Chasing it wastes time you could spend on better tests for risky code.
- It tempts "coverage theatre" — assertion-free tests that hit lines just to bump the number (Lesson 27's warning).
Why not 0% (or 20%)?
Untested code is unverified code. Below ~50-60%, large parts of your program have never been checked — you're flying blind on most of it.
Sensible targets by context
learning / hobby projects 60-80% (cover the core, relax on edges) typical production code 80-90% (a common professional bar) critical: payments, medical, 95%+ + branch coverage + reviews safety, security (and even then, coverage isn't enough alone)
Coverage by importance, not uniformly
# pyproject.toml — exclude code that's pointless to test [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "raise NotImplementedError", "if __name__ == .__main__.:", ]
Mark genuinely untestable lines with # pragma: no cover so they don't drag down (or inflate) your real number. Focus the percentage on logic that matters.
The metric to actually watch
Better than "% coverage": is the BUSINESS-CRITICAL code well tested? A 75% suite with great tests on the payment logic beats a 95% suite that mostly covers getters and __repr__.
Worked Example · Triage by Risk
12 minModule Current Risk if wrong Target Priority payments.py 60% money lost 95% 🔴 do first auth.py 70% security breach 95% 🔴 do first order_logic.py 80% wrong orders 90% 🟡 next ui_helpers.py 40% cosmetic glitch 60% 🟢 later __repr__ / logging 0% nothing skip ⚪ ignore
# focus the gate where it matters, not a blanket number: # tests/conftest.py or CI step could enforce per-module bars. # pragmatically: a global floor + manual focus on the red rows. # mark the truly-untestable: def __repr__(self): # pragma: no cover return f"<Order {self.id}>"
Read the diff
Instead of "get everything to 90%", you triage: payments and auth get the most tests because a bug there is catastrophic; UI helpers can wait; __repr__ is skipped entirely. The headline number might be 82%, but the right 82% — the dangerous code is at 95%. That judgement is what separates a tester from a number-chaser.
Try It Yourself
13 minFor a project, list each module and rate "risk if it's wrong" (low/med/high). Assign each a sensible coverage target.
Add exclude_lines for __repr__, __main__, and any NotImplementedError. Re-measure — does your "real" coverage number change?
Audit your own suite: is any test running lines without asserting anything? Add assertions so the coverage is honest.
Mini-Challenge · A Coverage Policy
8 minWrite a one-page "coverage policy" for a team: the global floor, higher bars for critical modules, what's excluded and why, and the rule that "coverage must come with assertions". This is a real document teams maintain.
Show an example policy
COVERAGE POLICY - Global floor: 80% branch coverage; CI fails below it. - Critical modules (payments, auth, security): 95%+ required, reviewed. - Excluded: __repr__, __main__ blocks, defensive NotImplementedError. - Every covered line must have a meaningful assertion somewhere (no assertion-free "coverage theatre"). - New code may not lower the global number (coverage diff in PRs).
Non-negotiables: a floor, a higher critical-code bar, an exclusion list, and the no-theatre rule. Numbers serve quality, not the reverse.
Recap
3 minCoverage has diminishing returns: 100% is usually wasteful and tempts assertion-free "theatre"; 0% is negligent. Pick a target by risk — 80-90% typical, 95%+ for critical code, low or skipped for trivial code. Exclude genuinely-untestable lines. The real goal: the dangerous code is well tested, with real assertions. Next: TDD — writing the test first.
Vocabulary Card
- diminishing returns
- Each extra coverage percent costs more for less benefit.
- coverage theatre
- Tests that execute lines without asserting anything — fake coverage.
- pragma: no cover
- Marks a line as intentionally excluded from coverage.
- risk-based testing
- Focusing test effort where a bug would do the most harm.
Homework
4 minWrite a coverage policy for a project of yours: risk-rank the modules, set per-area targets, list exclusions, and audit for coverage theatre. Apply the exclusions and report your "honest" coverage number before and after.
Follow the example policy + triage table. The insight to demonstrate: a slightly lower but honest, well-targeted number beats a high number padded with theatre.