Python Part II · Module 29 · Coverage Automation
Coverage Deltas Run Over Run
Every coverage dashboard shows an arrow beside the number, and that arrow is produced by subtracting last night's percentage from tonight's. It is the most widely deployed wrong calculation in verification automation. A percentage is a ratio whose numerator and denominator both move between runs, so its difference blends two completely different events — work the team did, and code somebody merged — into one signed number that cannot be acted on. Worse, the single cheapest way to make that number go up involves no testing whatsoever.
Advanced21 min readPythonPart IIAutomationCoverageTrends
Part II · Module 29 · Chapter 29.4 · Coverage Deltas Run Over Run
1. What a Delta Is Asked to Answer
The arrow beside a coverage number is read as one thing and is asked to carry four:
READINGS = [
("the team covered more bins", "progress — the only one anybody wants"),
("somebody merged new coverable code", "the denominator grew"),
("somebody excluded uncovered bins", "the denominator shrank — §6"),
("the covergroups were restructured", "bins renamed, not added — §5"),
]
print(f"{'what happened':<40} what it does to the number")
for what, effect in READINGS:
print(f"{what:<40} {effect}")
print()
print("events a signed percentage delta can distinguish:", 1)
print("events that produce one:", len(READINGS))what happened what it does to the number
the team covered more bins progress — the only one anybody wants
somebody merged new coverable code the denominator grew
somebody excluded uncovered bins the denominator shrank — §6
the covergroups were restructured bins renamed, not added — §5
events a signed percentage delta can distinguish: 1
events that produce one: 4Four causes, one symptom. A number that four unrelated events can produce is not a measurement of any of them, and the rest of this chapter is about getting back to the four.
2. The Annotation
delta = today["functional"] - previous["functional"]It runs, it produces a plausible number, and it is the line almost every coverage dashboard contains:
previous = {"functional": 90.00, "branch": 88.20}
today = {"functional": 88.00, "branch": 89.10}
delta = today["functional"] - previous["functional"]
print(f"previous: {previous['functional']:.2f}%")
print(f"today : {today['functional']:.2f}%")
print(f"delta : {delta:+.2f} points")
print()
print("the dashboard will render:", "a red down-arrow")
print("the conclusion a reader draws:", "coverage got worse")previous: 90.00%
today : 88.00%
delta : -2.00 points
the dashboard will render: a red down-arrow
the conclusion a reader draws: coverage got worseHold on to that conclusion. The next section supplies the two numbers the annotation threw away, and they say the opposite.
3. The Percentage Fell and the Work Advanced
RUNS = [
("Monday", 900, 1000),
("Friday", 1056, 1200),
]
for day, hits, total in RUNS:
print(f"{day:<8} {hits:>5} / {total:<5} = {100.0*hits/total:>6.2f}%")
(_, h0, t0), (_, h1, t1) = RUNS
print()
print(f"percentage : {100.0*h1/t1 - 100.0*h0/t0:+.2f} points")
print(f"bins covered : {h1 - h0:+d}")
print(f"bins coverable : {t1 - t0:+d}")
print(f"bins still uncovered, Monday: {t0 - h0}")
print(f"bins still uncovered, Friday: {t1 - h1}")Monday 900 / 1000 = 90.00%
Friday 1056 / 1200 = 88.00%
percentage : -2.00 points
bins covered : +156
bins coverable : +200
bins still uncovered, Monday: 100
bins still uncovered, Friday: 144The team covered 156 bins in a week. The headline fell two points, and the uncovered count rose from 100 to 144 — so in the sense the dashboard means, coverage really did get worse, and in the sense the team means, a great deal of work got done. Both are true, which is precisely why one signed number cannot represent the week.
This is Chapter 29.1 Section 6's argument arriving at its destination. A flow that stored only percentages has lost the ability to make the distinction permanently; no cleverness in the comparison layer can reconstruct 156 from 90.00 and 88.00.
4. Decomposing the Delta Exactly
The two causes can be separated, and not approximately — the split is an algebraic identity:
h0, t0 = 900, 1000
h1, t1 = 1056, 1200
p0 = 100.0 * h0 / t0
p1 = 100.0 * h1 / t1
numerator_effect = 100.0 * (h1 - h0) / t1
denominator_effect = 100.0 * h0 * (1.0 / t1 - 1.0 / t0)
print(f"before {h0}/{t0} = {p0:.2f}%")
print(f"after {h1}/{t1} = {p1:.2f}%")
print()
print(f"effect of covering {h1-h0} bins : {numerator_effect:+.2f} points")
print(f"effect of adding {t1-t0} coverable bins: {denominator_effect:+.2f} points")
print(f"sum : {numerator_effect + denominator_effect:+.2f} points")
print(f"actual delta : {p1 - p0:+.2f} points")
print(f"exact, not approximate : {abs((numerator_effect + denominator_effect) - (p1 - p0)) < 1e-9}")before 900/1000 = 90.00%
after 1056/1200 = 88.00%
effect of covering 156 bins : +13.00 points
effect of adding 200 coverable bins: -15.00 points
sum : -2.00 points
actual delta : -2.00 points
exact, not approximate : True+13.00 from work, −15.00 from new code, −2.00 net. That is the week, reported in a form a reader can act on: the verification effort was worth thirteen points and the design grew by fifteen.
The identity is worth a line of algebra, because knowing it is exact is what makes it safe to publish:
100·(h₁−h₀)/t₁ + 100·h₀·(1/t₁ − 1/t₀)
= 100·h₁/t₁ − 100·h₀/t₁ + 100·h₀/t₁ − 100·h₀/t₀
= 100·h₁/t₁ − 100·h₀/t₀The middle terms cancel, so the two effects sum to the delta with no residual. There is no third term, no interaction correction, and no ordering choice to defend — which is unusual for a decomposition and is the reason to prefer this one over the obvious alternative of attributing the numerator effect at the old denominator.
5. A Renamed Bin Is Not a New Bin
Counts cannot see a rename, and covergroups get restructured constantly:
before = {"cg.burst.len4", "cg.burst.len8", "cg.mode.idle", "cg.mode.run"}
after = {"cg.burst.len_4", "cg.burst.len8", "cg.mode.idle", "cg.mode.run",
"cg.err.parity"}
print(f"bins before: {len(before)}")
print(f"bins after : {len(after)}")
print(f"net change : {len(after) - len(before):+d}")
print()
print("appeared :", sorted(after - before))
print("disappeared:", sorted(before - after))
print("unchanged :", len(before & after))
print()
print(f"the net count of {len(after)-len(before):+d} is the sum of "
f"{len(after-before)} appearances and {len(before-after)} disappearances")bins before: 4
bins after : 5
net change : +1
appeared : ['cg.burst.len_4', 'cg.err.parity']
disappeared: ['cg.burst.len4']
unchanged : 3
the net count of +1 is the sum of 2 appearances and 1 disappearancesA net +1 describes two genuinely different events: one bin was really added (cg.err.parity) and one was renamed (len4 to len_4). The rename matters because cg.burst.len4 may well have been covered — and its replacement starts at zero, so the numerator drops for a reason that has nothing to do with the design or the tests.
Chapter 7.4's set operations are the whole tool here. Compare the names:
BEFORE = {"cg.burst.len4": 40, "cg.burst.len8": 40, "cg.mode.idle": 10,
"cg.mode.run": 10}
AFTER = {"cg.burst.len_4": 0, "cg.burst.len8": 40, "cg.mode.idle": 10,
"cg.mode.run": 12, "cg.err.parity": 0}
appeared = sorted(set(AFTER) - set(BEFORE))
gone = sorted(set(BEFORE) - set(AFTER))
common = sorted(set(BEFORE) & set(AFTER))
print("hits lost to bins that no longer exist:",
sum(BEFORE[b] for b in gone))
print("hits gained in bins that did not exist:",
sum(AFTER[b] for b in appeared))
print("hits moved within bins present in both:",
sum(AFTER[b] - BEFORE[b] for b in common))
print()
print(f"total before: {sum(BEFORE.values())}")
print(f"total after : {sum(AFTER.values())}")
print(f"net : {sum(AFTER.values()) - sum(BEFORE.values()):+d}")
print()
print("so a net of", sum(AFTER.values()) - sum(BEFORE.values()), "decomposes as:")
print(" -40 lost with a disappeared bin")
print(" +0 gained with bins that are new")
print(" +2 of real progress in bins present in both")hits lost to bins that no longer exist: 40
hits gained in bins that did not exist: 0
hits moved within bins present in both: 2
total before: 100
total after : 62
net : -38
so a net of -38 decomposes as:
-40 lost with a disappeared bin
+0 gained with bins that are new
+2 of real progress in bins present in bothThe three components sum to the net exactly: −40 + 0 + 2 = −38. And only the last of them is verification progress.
A week reported as "coverage hits fell by 38" was a week in which the team covered two more things and a covergroup was restructured. A comparator that reports the three components separately makes the rename self-evident; one that reports the net reports a number that is an accounting artefact with a week of real work buried inside it.
6. An Exclusion Is Not Progress
This is the number to remember from this chapter:
h, t = 900, 1000
base = 100.0 * h / t
print(f"starting point : {h}/{t} = {base:.2f}%")
print(f"uncovered bins : {t - h}")
print()
for excluded in (25, 50, 100):
new_total = t - excluded
print(f"exclude {excluded:>3} uncovered bins -> {h}/{new_total} = "
f"{100.0*h/new_total:>6.2f}% ({100.0*h/new_total - base:+.2f} points)")
print()
print("tests written:", 0)
print("bins covered :", 0)
print("lines of RTL exercised:", 0)starting point : 900/1000 = 90.00%
uncovered bins : 100
exclude 25 uncovered bins -> 900/975 = 92.31% (+2.31 points)
exclude 50 uncovered bins -> 900/950 = 94.74% (+4.74 points)
exclude 100 uncovered bins -> 900/900 = 100.00% (+10.00 points)
tests written: 0
bins covered : 0
lines of RTL exercised: 0One commit excluding every uncovered bin takes this design from 90% to a clean 100%, and the dashboard renders a ten-point green arrow. No test ran. Nothing was verified.
That is not a hypothetical abuse; it is the ordinary consequence of a legitimate activity. Excluding genuinely unreachable bins is correct and necessary work — Chapter 29.5 is about exactly which bins deserve it — and every one of those legitimate exclusions raises the percentage without testing anything. So a flow cannot treat exclusions as suspicious, and it cannot treat percentage movement as progress either. It has to report them as separate facts:
BEFORE = {"hits": 900, "total": 1000, "excluded": 0}
AFTER = {"hits": 912, "total": 950, "excluded": 50}
p0 = 100.0 * BEFORE["hits"] / BEFORE["total"]
p1 = 100.0 * AFTER["hits"] / AFTER["total"]
print(f"headline moved: {p0:.2f}% -> {p1:.2f}% ({p1-p0:+.2f} points)")
print()
print(f" bins covered : {AFTER['hits'] - BEFORE['hits']:+d}")
print(f" bins excluded : {AFTER['excluded'] - BEFORE['excluded']:+d}")
print(f" coverable bins : {AFTER['total'] - BEFORE['total']:+d}")
print()
hits_only = 100.0 * AFTER["hits"] / BEFORE["total"]
print(f"headline if NOTHING had been excluded: {hits_only:.2f}% "
f"({hits_only - p0:+.2f} points)")
print(f"points attributable to the exclusion : {p1 - hits_only:+.2f}")headline moved: 90.00% -> 96.00% (+6.00 points)
bins covered : +12
bins excluded : +50
coverable bins : -50
headline if NOTHING had been excluded: 91.20% (+1.20 points)
points attributable to the exclusion : +4.80Six points of apparent movement: 1.20 earned by covering twelve bins, and 4.80 granted by an exclusion commit. A reviewer who sees only +6.00 has no way to ask the right question, and the right question — what was excluded, and why? — is Chapter 29.5's subject and Chapter 29.6's evidence requirement.
7. Two Points Are Not a Trend
The chapter's title says trend, and the canonical purpose is explicit that a single number is not one. Here is why, with two series that a one-step delta cannot tell apart:
flat = [88.0, 88.4, 87.9, 88.2, 88.1, 88.3]
rising = [88.0, 88.9, 89.7, 90.6, 91.4, 91.6]
for name, s in (("flat", flat), ("rising", rising)):
print(f"{name:<7} {s}")
print(f"{'':<7} last delta {s[-1]-s[-2]:+.2f} "
f"spread {max(s)-min(s):.2f} "
f"total move {s[-1]-s[0]:+.2f}")
print()
print("both have a positive last delta:",
flat[-1] - flat[-2] > 0, rising[-1] - rising[-2] > 0)
print("the deltas alone are indistinguishable:",
abs((flat[-1]-flat[-2]) - 0.20) < 1e-9)flat [88.0, 88.4, 87.9, 88.2, 88.1, 88.3]
last delta +0.20 spread 0.50 total move +0.30
rising [88.0, 88.9, 89.7, 90.6, 91.4, 91.6]
last delta +0.20 spread 3.60 total move +3.60
both have a positive last delta: True True
the deltas alone are indistinguishable: TrueBoth series end with the same +0.20 step. One is six weeks of a project going nowhere; the other is six weeks of steady closure that happens to have ended on a quiet night. The last delta is identical, and a dashboard showing only the last delta showed the same green arrow for both.
What distinguishes them is history, and the cheapest useful amount of it is "enough runs to see the spread":
flat = [88.0, 88.4, 87.9, 88.2, 88.1, 88.3]
rising = [88.0, 88.9, 89.7, 90.6, 91.4, 91.6]
def describe(series):
moves = [b - a for a, b in zip(series, series[1:])]
up = sum(1 for m in moves if m > 0)
return {
"runs": len(series),
"net": series[-1] - series[0],
"spread": max(series) - min(series),
"steps_up": up,
"steps_down": len(moves) - up,
"monotonic": all(m > 0 for m in moves),
}
for name, s in (("flat", flat), ("rising", rising)):
d = describe(s)
print(f"{name:<7} runs {d['runs']} net {d['net']:+.2f} "
f"spread {d['spread']:.2f} up {d['steps_up']} "
f"down {d['steps_down']} monotonic {d['monotonic']}")flat runs 6 net +0.30 spread 0.50 up 3 down 2 monotonic False
rising runs 6 net +3.60 spread 3.60 up 5 down 0 monotonic TrueFive steps up out of five and a net of +3.60 against three up, two down and a net of +0.30. That is a trend, reported as four numbers over six runs. Not one of them is available from a pair.
8. The Comparison, as a Flow
Everything above is one pipeline, and it has a specific order: comparability is checked before names, names before counts, and the decomposition happens last because it needs the sets to be reconciled first.
Comparing two coverage runs
Stage 2 is the one most flows omit, and Chapter 29.1 Section 8's layout field is what makes it possible: two runs parsed by different layout handlers after a tool upgrade produce a delta that is an artefact of the parser. Stage 7 is what makes the output a trend rather than an arrow.
9. The Comparator, Assembled
from dataclasses import dataclass
from typing import Dict, Optional
@dataclass(frozen=True)
class CoverageRun:
label: str
metric: str
layout: str
scope: str
excluded: int
bins: Dict[str, int] # bin name -> hits
@property
def hits(self):
return sum(1 for v in self.bins.values() if v > 0)
@property
def total(self):
return len(self.bins)
@property
def pct(self):
return 100.0 * self.hits / self.total
def compare(a: CoverageRun, b: CoverageRun) -> Optional[dict]:
if (a.metric, a.layout, a.scope) != (b.metric, b.layout, b.scope):
return {"comparable": False,
"reason": f"{(a.metric, a.layout, a.scope)} vs {(b.metric, b.layout, b.scope)}"}
appeared = sorted(set(b.bins) - set(a.bins))
gone = sorted(set(a.bins) - set(b.bins))
common = sorted(set(a.bins) & set(b.bins))
covered_in_common = sum(
(1 if b.bins[k] > 0 else 0) - (1 if a.bins[k] > 0 else 0) for k in common)
lost_with_bins = sum(1 for k in gone if a.bins[k] > 0)
gained_with_bins = sum(1 for k in appeared if b.bins[k] > 0)
num_eff = 100.0 * (b.hits - a.hits) / b.total
den_eff = 100.0 * a.hits * (1.0 / b.total - 1.0 / a.total)
return {
"comparable": True,
"pct_before": round(a.pct, 2),
"pct_after": round(b.pct, 2),
"delta": round(b.pct - a.pct, 2),
"numerator_effect": round(num_eff, 2),
"denominator_effect": round(den_eff, 2),
"identity_holds": abs((num_eff + den_eff) - (b.pct - a.pct)) < 1e-9,
"appeared": appeared,
"disappeared": gone,
"progress_in_common": covered_in_common,
"lost_with_bins": lost_with_bins,
"gained_with_bins": gained_with_bins,
"excluded_delta": b.excluded - a.excluded,
}
MON = CoverageRun("Mon", "functional", "covergroup-list", "tb.dut", 0, {
"cg.burst.len4": 3, "cg.burst.len8": 1, "cg.mode.idle": 7,
"cg.mode.run": 2, "cg.err.parity": 0, "cg.err.crc": 0,
})
FRI = CoverageRun("Fri", "functional", "covergroup-list", "tb.dut", 1, {
"cg.burst.len_4": 0, "cg.burst.len8": 4, "cg.mode.idle": 9,
"cg.mode.run": 5, "cg.err.parity": 2, "cg.err.crc": 0,
"cg.err.timeout": 0,
})
r = compare(MON, FRI)
print("comparable:", r["comparable"])
print(f"headline : {r['pct_before']:.2f}% -> {r['pct_after']:.2f}% ({r['delta']:+.2f})")
print()
print(f" numerator effect {r['numerator_effect']:+.2f}")
print(f" denominator effect {r['denominator_effect']:+.2f}")
print(f" identity holds {r['identity_holds']}")
print()
print(" appeared :", r["appeared"])
print(" disappeared :", r["disappeared"])
print(f" hits lost with disappeared bins : {r['lost_with_bins']}")
print(f" hits gained with new bins : {r['gained_with_bins']}")
print(f" progress in bins present in both: {r['progress_in_common']}")
print(f" exclusions added : {r['excluded_delta']}")
print()
BAD = CoverageRun("Fri", "functional", "kv-block", "tb.dut", 0, {"cg.a": 1})
print("after a tool upgrade:", compare(MON, BAD))comparable: True
headline : 66.67% -> 57.14% (-9.52)
numerator effect +0.00
denominator effect -9.52
identity holds True
appeared : ['cg.burst.len_4', 'cg.err.timeout']
disappeared : ['cg.burst.len4']
hits lost with disappeared bins : 1
hits gained with new bins : 0
progress in bins present in both: 1
exclusions added : 1
after a tool upgrade: {'comparable': False, 'reason': "('functional', 'covergroup-list', 'tb.dut') vs ('functional', 'kv-block', 'tb.dut')"}Read the middle block rather than the headline, because this run pair contains something the headline cannot express: the numerator effect is exactly zero. Four bins were covered on Monday and four on Friday, so the entire −9.52 came from the denominator growing by one bin.
A flow that reported "numerator effect 0.00" and stopped would be telling a second untruth, though — nothing stood still. One covered bin was lost to the rename (cg.burst.len4 had hits, cg.burst.len_4 starts at zero) and one bin was genuinely covered (cg.err.parity went from 0 to 2). The two cancelled:
hits lost with disappeared bins -1
hits gained with new bins +0
progress in bins present in both +1
--
net change in covered bins 0A zero is the sum of a loss and a gain, and only the component view says so. That is six facts where the annotation produced one, and it is the reason stage 5 of the flow exists separately from stage 6.
And the last line is the stage-2 check doing its job: a run parsed by a different layout handler is refused rather than subtracted. Chapter 28.5 Section 2 made the same demand of two QoR runs — comparability is a precondition, not a caveat.
10. Predict, Run, Observe
h0, t0, h1, t1 = 900, 1000, 1056, 1200
# A
print("A:", f"{100.0*h1/t1 - 100.0*h0/t0:+.2f}")
# B
print("B:", h1 - h0, t1 - t0)
# C
print("C:", f"{100.0*(h1-h0)/t1:+.2f}", f"{100.0*h0*(1.0/t1-1.0/t0):+.2f}")
# D
print("D:", f"{100.0*900/900:.2f}")The verified output:
A: -2.00
B: 156 200
C: +13.00 -15.00
D: 100.00A — the annotation's answer: two points down.
B — the two numbers it discarded. 156 bins covered, 200 added.
C — the exact decomposition. Thirteen points earned, fifteen points of new code.
D — 900 of 900. Excluding every uncovered bin reads as perfect closure, which is Section 6 and the reason Chapter 29.5 exists.
11. Exercises
Exercise 1 — four causes. List the four events that produce a negative coverage delta in your flow and say which you could currently distinguish.
Exercise 2 — the discarded pair. Take a week of your own dashboard history and say whether the bin counts behind it are recoverable.
Exercise 3 — your own inversion. Find a week in your project where coverage fell and the team covered more bins. Report all four numbers.
Exercise 4 — decompose. Implement the numerator and denominator effects and verify the identity on your own data.
Exercise 5 — the other attribution. Compute the numerator effect at the old denominator and say why mixing the two in one table is wrong.
Exercise 6 — a rename. Find a renamed bin in your covergroup history and say what it did to the numerator.
Exercise 7 — three components. Report hits lost with disappeared bins, hits gained with new bins, and progress in common bins, for two of your runs.
Exercise 8 — the exclusion arithmetic. For your design, compute the headline you would get by excluding every uncovered bin.
Exercise 9 — split the movement. Take a real exclusion commit and separate the points earned from the points granted.
Exercise 10 — report the count. Add the excluded-bin count beside your headline and say what it lets a reviewer ask.
Exercise 11 — two series. Construct a flat series and a rising series with the same last delta, and say which four numbers separate them.
Exercise 12 — enough history. Decide how many runs your trend view should carry and justify the number.
Exercise 13 — stage 2. Add the comparability check to your comparator and run it across a tool upgrade boundary.
Exercise 14 — the order of stages. Explain why names are compared before counts and the decomposition happens last.
Exercise 15 — six facts. Take the Section 9 output and write the two sentences you would put in a weekly status mail.
Exercise 16 — all four causes in one week. Find a week in your history containing at least two of the four causes, and separate them.
Exercise 17 — the identity by hand. Write out the cancellation that makes the two effects sum to the delta exactly.
Exercise 18 — residual check. Add an assertion that your decomposition's two terms sum to the observed delta, and run it over a month.
Exercise 19 — zero is a sum. Construct a run pair whose numerator effect is zero because a loss and a gain cancelled, and report the three components.
Exercise 20 — the rename hunt. Diff two runs' bin-name sets and list every name that appeared and disappeared on the same night.
Exercise 21 — a covered bin lost. Say what happens to the numerator when a covered bin is renamed, and why no arithmetic over totals shows it.
Exercise 22 — exclusions beside the headline. Add the exclusion count and its weekly delta to your dashboard, and say what question it enables.
Exercise 23 — earned versus granted. For one real week, split your movement into points earned by covering and points granted by excluding.
Exercise 24 — the four trend numbers. Compute net, spread, steps up and steps down over your last eight runs.
Exercise 25 — stage order. Explain what breaks if the decomposition runs before the bin sets are reconciled.
12. Summary
- Four different events produce a coverage delta and a signed percentage can distinguish exactly one of them.
- The percentage fell two points in a week when the team covered 156 more bins. Both readings are true, which is why one number cannot carry the week.
- The delta decomposes exactly:
100·(h₁−h₀)/t₁ + 100·h₀·(1/t₁ − 1/t₀)equals the delta with no residual — +13.00 of work and −15.00 of new code. - Attribute the numerator effect at the new denominator, state that you did, and never mix the two attributions in one table.
- Counts cannot see a rename. A net +1 was two appearances and one disappearance, and the disappeared bin was covered.
- Compare bin names and report three components: hits lost with bins, hits gained with bins, and progress in bins present in both. Only the third is verification progress.
- Excluding 100 uncovered bins takes 90.00% to 100.00% with no testing at all — and legitimate exclusion work has exactly the same effect, so the defence is reporting the count, not suspicion.
- Separate points earned from points granted: +6.00 of movement was +1.20 of covering and +4.80 of excluding.
- Two points are not a trend. A flat series and a six-week climb had the same +0.20 last step.
- A trend is four numbers over N runs — net, spread, steps up, steps down — and none of them exists in a pair.
- Check comparability first. A delta across a tool-layout change is an artefact of the parser, and the
layoutfield is what detects it. - A zero numerator effect can be the sum of a loss and a gain — one covered bin lost to a rename and one genuinely covered, cancelling exactly.
- Assert the decomposition's residual is zero. It is an identity, so a non-zero residual means a bug in the comparator rather than an interesting result.
- A renamed bin's hits do not transfer. The new name starts at zero, and totals alone cannot show it.
- Report the exclusion count and its weekly delta, so legitimate exclusion work and a shrinking target look different.
- Reconcile the bin sets before decomposing; the two effects are only meaningful once appearances and disappearances are accounted for.
The next chapter takes the uncovered bins this one has been counting and asks which of them anybody should ever work on.
Where this fits
Part of the Python curriculum.
