20. Effective Python (3rd ed.) — crosswalk and gap closure
Brett Slatkin, Effective Python: 90 Specific Ways to Write Better Python, 3rd edition — 125 numbered Items across 14 chapters. This chapter maps every one of them onto the rest of this guide, then closes the gaps.
It is a companion to the book, not a replacement for it. Every claim here is written in my own words and every Item is cited by number so you can go read the original. Where the guide already covers an Item to comparable depth, this chapter tells you so and gets out of the way — that is the point, so you can skip a third of the book on a re-read.
Contents
- 1. The verdict at a glance
- 2. How to use the matrix
- 3. The full coverage matrix
- 4. The one real disagreement: Item 109, settled by measurement
- 5. The gaps, closed
- 6. The high-priority partials
- 7. What this book covers that the guide deliberately will not
- 8. Reading order if you own the book
Everything executable in this chapter was run on Python 3.11.15. Where an Item depends on 3.12+ behaviour I say so rather than guessing.
1. The verdict at a glance
| Verdict | Items | Share | What it means for you |
|---|---|---|---|
| covered | 45 | 36% | The guide already makes this point at comparable or greater depth. Skip. |
| partial | 48 | 38% | The guide states the rule but misses the why, the failure mode, or the framing. Read the Item. |
| gap | 18 | 14% | Genuinely absent from the guide. Closed in §5. |
| diverges | 1 | 1% | The book and the guide disagree. Settled in §4. |
| skip | 13 | 10% | Real advice, not study material — packaging, virtualenvs, docstrings, deployment. |
The shape of this is worth noticing. The overlap is heaviest exactly where you
would expect — the guide’s 04-python-core.md and 12-design-patterns-python.md
between them cover most of chapters 7, 8 and 12 of the book. The gaps cluster in
two places the guide never set out to cover: small language traps (chapters 1–3
and 5–6) and the standard library’s operational corners (chapters 10–11 and
13–14). If you have been writing Python for years, the traps are the part worth
your time — they are precisely the things that stop being visible once you stop
being a beginner.
2. How to use the matrix
Read down the Verdict column and act on it:
- covered — do not re-read the Item. Follow the guide reference if you want a refresher; it will be at least as deep.
- partial — read the Item. The reference tells you which guide section it extends, so you can read them together.
- gap — read the Item, then read §5 here, which closes it with runnable code.
- skip — read it once for completeness if you like, but it will not come up in an interview and it is not what this guide is for.
The Pri column is interview-relevance, not importance: H would plausibly come up in an interview or corrects a live misconception; M is good practice you are unlikely to be asked about directly; L is stylistic or situational.
3. The full coverage matrix
| # | Item | Verdict | Where it lives in the guide | Pri |
|---|---|---|---|---|
| 1 | Know Which Version of Python You’re Using | skip | — | L |
| 2 | Follow the PEP 8 Style Guide | skip | — | L |
| 3 | Never Expect Python to Detect Errors at Compile Time | partial | 04-python-core.md § The object model | M |
| 4 | Write Helper Functions Instead of Complex Expressions | skip | — | L |
| 5 | Prefer Multiple-Assignment Unpacking over Indexing | partial | 13-problem-sets.md; 15-study-plan-and-flashcards.md | M |
| 6 | Always Surround Single-Element Tuples with Parentheses | gap | — | M |
| 7 | Consider Conditional Expressions for Simple Inline Logic | partial | 14-cheatsheets.md § TypeScript and JavaScript quick reference | L |
| 8 | Prevent Repetition with Assignment Expressions | partial | 12-design-patterns-python.md | M |
| 9 | Consider match for Destructuring in Flow Control | partial | 12-design-patterns-python.md § match state machines; 11-design-patterns-typescript.md | H |
| 10 | Know the Differences Between bytes and str | partial | 04-python-core.md; 05-data-structures-typescript.md | M |
| 11 | Prefer Interpolated F-Strings over C-Style Format Strings | partial | 14-cheatsheets.md | L |
| 12 | Understand the Difference Between repr and str when Printing Objects | partial | 04-python-core.md § Data model and dunders | M |
| 13 | Prefer Explicit String Concatenation over Implicit | gap | — | M |
| 14 | Know How to Slice Sequences | partial | 06-data-structures-python.md | M |
| 15 | Avoid Striding and Slicing in a Single Expression | partial | 06-data-structures-python.md | M |
| 16 | Prefer Catch-All Unpacking over Slicing | partial | 15-study-plan-and-flashcards.md | M |
| 17 | Prefer enumerate over range | covered | 08-algorithm-patterns.md | L |
| 18 | Use zip to Process Iterators in Parallel | partial | 14-cheatsheets.md | M |
| 19 | Avoid else Blocks After for and while Loops | gap | — | M |
| 20 | Never Use for Loop Variables After the Loop Ends | gap | — | M |
| 21 | Be Defensive when Iterating over Arguments | covered | 12-design-patterns-python.md (iter(g) is g, single-pass exhaustion); 04-python-core.md § Generators and coroutines | H |
| 22 | Never Modify Containers While Iterating over Them | partial | 06-data-structures-python.md | H |
| 23 | Pass Iterators to any and all for Efficient Short-Circuiting | partial | 02-javascript-core.md; 12-design-patterns-python.md | M |
| 24 | Consider itertools for Working with Iterators and Generators | covered | 06-data-structures-python.md; 12-design-patterns-python.md; 04-python-core.md | M |
| 25 | Be Cautious when Relying on Dictionary Insertion Ordering | covered | 06-data-structures-python.md § dict compact layout; 04-python-core.md | M |
| 26 | Prefer get over in and KeyError to Handle Missing Dictionary Keys | covered | 06-data-structures-python.md | M |
| 27 | Prefer defaultdict over setdefault for Internal State | covered | 06-data-structures-python.md | M |
| 28 | Know How to Construct Key-Dependent Default Values with missing | gap | 04-python-core.md (one passing mention) | M |
| 29 | Compose Classes Instead of Deeply Nesting Dictionaries, Lists, and Tuples | covered | 12-design-patterns-python.md; 06-data-structures-python.md § dataclass comparison table | M |
| 30 | Know That Function Arguments Can Be Mutated | partial | 04-python-core.md § The object model (rebinding vs mutation) | H |
| 31 | Return Dedicated Result Objects Instead of Unpacking More Than Three Variables | covered | 12-design-patterns-python.md § Result/Either; 06-data-structures-python.md § dataclass comparison | M |
| 32 | Prefer Raising Exceptions to Returning None | covered | 12-design-patterns-python.md § Result/Either; 14-cheatsheets.md § trap list | M |
| 33 | Know How Closures Interact with Variable Scope and nonlocal | partial | 04-python-core.md (late-binding closures, the i=i fix) | H |
| 34 | Reduce Visual Noise with Variable Positional Arguments | partial | 12-design-patterns-python.md | M |
| 35 | Provide Optional Behavior with Keyword Arguments | skip | — | L |
| 36 | Use None and Docstrings to Specify Dynamic Default Arguments | covered | 04-python-core.md § Trap 1 - mutable default arguments (plus a Q&A drill); 12-design-patterns-python.md | H |
| 37 | Enforce Clarity with Keyword-Only and Positional-Only Arguments | partial | 12-design-patterns-python.md; 14-cheatsheets.md | M |
| 38 | Define Function Decorators with functools.wraps | covered | 12-design-patterns-python.md § Decorators (four shapes, bottom-up stacking); 04-python-core.md | H |
| 39 | Prefer functools.partial over lambda Expressions for Glue Functions | covered | 12-design-patterns-python.md | M |
| 40 | Use Comprehensions Instead of map and filter | partial | 06-data-structures-python.md; 08-algorithm-patterns.md | M |
| 41 | Avoid More Than Two Control Subexpressions in Comprehensions | skip | — | L |
| 42 | Reduce Repetition in Comprehensions with Assignment Expressions | gap | — | M |
| 43 | Consider Generators Instead of Returning Lists | covered | 04-python-core.md § Generators and coroutines; 12-design-patterns-python.md | H |
| 44 | Consider Generator Expressions for Large List Comprehensions | covered | 04-python-core.md; 06-data-structures-python.md | M |
| 45 | Compose Multiple Generators with yield from | covered | 04-python-core.md; 12-design-patterns-python.md | M |
| 46 | Pass Iterators into Generators as Arguments Instead of Calling send | partial | 04-python-core.md § Generators and coroutines; 12-design-patterns-python.md | M |
| 47 | Manage Iterative State Transitions with a Class Instead of throw | partial | 12-design-patterns-python.md § match state machines | M |
| 48 | Accept Functions Instead of Classes for Simple Interfaces | covered | 12-design-patterns-python.md (__call__, strategy) | M |
| 49 | Prefer Object-Oriented Polymorphism over Functions with isinstance Checks | covered | 12-design-patterns-python.md § Protocol vs ABC | M |
| 50 | Consider functools.singledispatch for Functional-Style Programming | covered | 12-design-patterns-python.md (18 mentions, incl. visitor discussion) | M |
| 51 | Prefer dataclasses for Defining Lightweight Classes | covered | 06-data-structures-python.md § tuple/namedtuple/NamedTuple/dataclass comparison table; 12-design-patterns-python.md | H |
| 52 | Use @classmethod Polymorphism to Construct Objects Generically | covered | 12-design-patterns-python.md § Factory; 04-python-core.md | M |
| 53 | Initialize Parent Classes with super | covered | 04-python-core.md § C3 MRO and cooperative super(); 12-design-patterns-python.md | H |
| 54 | Consider Composing Functionality with Mix-in Classes | covered | 12-design-patterns-python.md; 11-design-patterns-typescript.md | M |
| 55 | Prefer Public Attributes over Private Ones | gap | — | M |
| 56 | Prefer dataclasses for Creating Immutable Objects | covered | 04-python-core.md (frozen=True gives __hash__); 06-data-structures-python.md § hashability table; 12-design-patterns-python.md § structural sharing | M |
| 57 | Inherit from collections.abc Classes for Custom Container Types | covered | 12-design-patterns-python.md; 04-python-core.md | M |
| 58 | Use Plain Attributes Instead of Setter and Getter Methods | partial | 04-python-core.md § Descriptors; 12-design-patterns-python.md | M |
| 59 | Consider @property Instead of Refactoring Attributes | partial | 04-python-core.md § Descriptors | M |
| 60 | Use Descriptors for Reusable @property Methods | covered | 04-python-core.md § Descriptors and attribute lookup order; 12-design-patterns-python.md § Descriptors | H |
| 61 | Use getattr, getattribute, and setattr for Lazy Attributes | covered | 04-python-core.md § Attribute lookup order; 12-design-patterns-python.md § Proxy | H |
| 62 | Validate Subclasses with init_subclass | covered | 12-design-patterns-python.md § init_subclass over metaclasses | H |
| 63 | Register Class Existence with init_subclass | covered | 12-design-patterns-python.md § Registry | M |
| 64 | Annotate Class Attributes with set_name | partial | 12-design-patterns-python.md § Descriptors | M |
| 65 | Consider Class Body Definition Order to Establish Relationships Between Attributes | gap | — | M |
| 66 | Prefer Class Decorators over Metaclasses for Composable Class Extensions | covered | 12-design-patterns-python.md § Class decorators; § init_subclass over metaclasses | M |
| 67 | Use subprocess to Manage Child Processes | gap | 04-python-core.md (one passing mention) | M |
| 68 | Use Threads for Blocking I/O; Avoid for Parallelism | covered | 04-python-core.md § The GIL, measured; 15-study-plan-and-flashcards.md | H |
| 69 | Use Lock to Prevent Data Races in Threads | partial | 11-design-patterns-typescript.md; 12-design-patterns-python.md | H |
| 70 | Use Queue to Coordinate Work Between Threads | partial | 04-python-core.md; 12-design-patterns-python.md § Producer-consumer | M |
| 71 | Know How to Recognize When Concurrency Is Necessary | partial | 12-design-patterns-python.md | M |
| 72 | Avoid Creating New Thread Instances for On-Demand Fan-out | partial | 04-python-core.md; 12-design-patterns-python.md | M |
| 73 | Understand How Using Queue for Concurrency Requires Refactoring | partial | 12-design-patterns-python.md | L |
| 74 | Consider ThreadPoolExecutor When Threads Are Necessary | partial | 04-python-core.md | M |
| 75 | Achieve Highly Concurrent I/O with Coroutines | covered | 04-python-core.md § Generators and coroutines; 12-design-patterns-python.md (21 mentions) | H |
| 76 | Know How to Port Threaded I/O to asyncio | covered | 04-python-core.md; 12-design-patterns-python.md | M |
| 77 | Mix Threads and Coroutines to Ease the Transition to asyncio | gap | — | M |
| 78 | Maximize Responsiveness of asyncio Event Loops with async-Friendly Worker Threads | partial | 04-python-core.md; 02-javascript-core.md § The event loop | H |
| 79 | Consider concurrent.futures for True Parallelism | covered | 04-python-core.md § The GIL; 12-design-patterns-python.md | M |
| 80 | Take Advantage of Each Block in try/except/else/finally | partial | 04-python-core.md (one mention) | M |
| 81 | assert Internal Assumptions and raise Missed Expectations | partial | 04-python-core.md; 18-testing-strategy.md | M |
| 82 | Consider contextlib and with Statements for Reusable try/finally Behavior | covered | 12-design-patterns-python.md § Context managers (__exit__ truthy suppresses); 04-python-core.md | H |
| 83 | Always Make try Blocks as Short as Possible | partial | 04-python-core.md | M |
| 84 | Beware of Exception Variables Disappearing | gap | — | H |
| 85 | Beware of Catching the Exception Class | partial | 12-design-patterns-python.md | M |
| 86 | Understand the Difference Between Exception and BaseException | partial | 04-python-core.md | H |
| 87 | Use traceback for Enhanced Exception Reporting | gap | — | M |
| 88 | Consider Explicitly Chaining Exceptions to Clarify Tracebacks | partial | 04-python-core.md (raise ... from); 12-design-patterns-python.md | M |
| 89 | Always Pass Resources into Generators and Have Callers Clean Them Up Outside | partial | 04-python-core.md (GeneratorExit) | H |
| 90 | Never Set debug to False | gap | — | M |
| 91 | Avoid exec and eval Unless You’re Building a Developer Tool | partial | 16-testing-node-test.md (JS side only) | M |
| 92 | Profile Before Optimizing | partial | 04-python-core.md | H |
| 93 | Optimize Performance-Critical Code Using timeit Microbenchmarks | partial | 04-python-core.md; 01-complexity-and-big-o.md § The measured traps | H |
| 94 | Know When and How to Replace Python with Another Programming Language | skip | — | L |
| 95 | Consider ctypes to Rapidly Integrate with Native Libraries | skip | 04-python-core.md (one mention) | L |
| 96 | Consider Extension Modules to Maximize Performance and Ergonomics | skip | — | L |
| 97 | Rely on Precompiled Bytecode and File System Caching to Improve Startup Time | gap | 04-python-core.md § Bytecode (disassembly only) | M |
| 98 | Lazy-Load Modules with Dynamic Imports to Reduce Startup Time | gap | — | M |
| 99 | Consider memoryview and bytearray for Zero-Copy Interactions with bytes | covered | 06-data-structures-python.md § memoryview/bytearray; 04-python-core.md | M |
| 100 | Sort by Complex Criteria Using the key Parameter | covered | 07-sorting-and-searching.md; 06-data-structures-python.md; 12-design-patterns-python.md | H |
| 101 | Know the Difference Between sort and sorted | covered | 07-sorting-and-searching.md; 14-cheatsheets.md | M |
| 102 | Consider Searching Sorted Sequences with bisect | covered | 06-data-structures-python.md § bisect/SortedList; 07-sorting-and-searching.md; 01-complexity-and-big-o.md | H |
| 103 | Prefer deque for Producer-Consumer Queues | covered | 06-data-structures-python.md § deque; 05-data-structures-typescript.md § ring buffer; 01-complexity-and-big-o.md | H |
| 104 | Know How to Use heapq for Priority Queues | covered | 06-data-structures-python.md § binary heap (O(n) heapify, indexed decrease-key); 09-graphs-and-trees.md § Dijkstra | H |
| 105 | Use datetime Instead of time for Local Clocks | gap | 12-design-patterns-python.md (injected-clock pattern only) | M |
| 106 | Use decimal when Precision Is Paramount | partial | 02-javascript-core.md § IEEE-754; 12-design-patterns-python.md | H |
| 107 | Make pickle Serialization Maintainable with copyreg | partial | 12-design-patterns-python.md (pickle in serialization discussion) | M |
| 108 | Verify Related Behaviors in TestCase Subclasses | covered | 17-testing-python-unittest.md § TestCase, all 41 assertions, subTest; 19-testing-cheatsheet.md | H |
| 109 | Prefer Integration Tests over Unit Tests | diverges | 18-testing-strategy.md § pyramid/trophy/honeycomb; § functional core, imperative shell | H |
| 110 | Isolate Tests from Each Other with setUp, tearDown, setUpModule, tearDownModule | covered | 17-testing-python-unittest.md § fixtures, addCleanup/enterContext | H |
| 111 | Use Mocks to Test Code with Complex Dependencies | covered | 17-testing-python-unittest.md § unittest.mock (Mock/MagicMock/AsyncMock, 7 patch forms, autospec); 19-testing-cheatsheet.md | H |
| 112 | Encapsulate Dependencies to Facilitate Mocking and Testing | covered | 18-testing-strategy.md § seams (parameterize/wrap/sprout/extract-pure-core); 17-testing-python-unittest.md | H |
| 113 | Use assertAlmostEqual to Control Precision in Floating Point Tests | partial | 17-testing-python-unittest.md (in the assertion table); 18-testing-strategy.md § flakiness taxonomy | H |
| 114 | Consider Interactive Debugging with pdb | gap | — | M |
| 115 | Use tracemalloc to Understand Memory Usage and Leaks | covered | 04-python-core.md § refcounting and generational GC | M |
| 116 | Know Where to Find Community-Built Modules | skip | — | L |
| 117 | Use Virtual Environments for Isolated and Reproducible Dependencies | skip | — | L |
| 118 | Write Docstrings for Every Function, Class, and Module | skip | 12-design-patterns-python.md | L |
| 119 | Use Packages to Organize Modules and Provide Stable APIs | partial | 12-design-patterns-python.md; 17-testing-python-unittest.md (__init__.py in the test package) | M |
| 120 | Consider Module-Scoped Code to Configure Deployment Environments | skip | — | L |
| 121 | Define a Root Exception to Insulate Callers from APIs | gap | — | H |
| 122 | Know How to Break Circular Dependencies | partial | 02-javascript-core.md § CJS vs ESM live bindings; 12-design-patterns-python.md | M |
| 123 | Consider warnings to Refactor and Migrate Usage | partial | 17-testing-python-unittest.md (assertWarns in the assertion table) | M |
| 124 | Consider Static Analysis via typing to Obviate Bugs | partial | 04-python-core.md; 12-design-patterns-python.md § Protocol vs ABC; 03-typescript-type-system.md | H |
| 125 | Prefer Open Source Projects for Bundling over zipimport and zipapp | skip | — | L |
4. The one real disagreement: Item 109, settled by measurement
Item 109 — “Prefer Integration Tests over Unit Tests.” The book’s argument is specific and worth stating fairly: because Python resolves almost everything at runtime, a unit test with its collaborators mocked verifies very little about whether the program actually works, so integration tests are the reliable source of confidence and unit tests should be reserved for code dense with edge cases.
This guide’s 18-testing-strategy.md takes a different line. It presents the
pyramid, the trophy and the honeycomb as a design choice rather than a law, and it
pushes a functional-core / imperative-shell split precisely so that the interesting
logic can be unit-tested cheaply and deterministically.
Two credible positions on the same language. Rather than pick one by assertion, I built both and measured them.
The experiment
graph TD
subgraph PUT["pricing.py — 15 statements"]
LT["line_total()"]
DP["discount_pct()"]
AD["apply_discount()"]
PO["price_order()<br/>wiring"]
end
US["unit suite<br/>13 tests, Mock repo"] -->|direct calls| LT
US -->|direct calls| DP
US -->|direct calls| AD
US -.->|"mocked wiring"| PO
IS["integration suite<br/>12 tests, real repo"] -->|only through| PO
PO --> LT
PO --> DP
PO --> AD
US --> R1["11/15 mutants killed"]
IS --> R2["11/15 mutants killed"]
R1 --> UN["union: 13/15 killed<br/>2 survive both suites"]
R2 --> UN
One module — a pricing routine with a pure core of three small functions and a shell that fetches prices and wires them together. Fifteen executable statements.
TIERS = {"none": 0, "silver": 5, "gold": 10}
def line_total(qty, unit_price_cents):
if qty <= 0:
raise ValueError("qty must be positive")
return qty * unit_price_cents
def discount_pct(subtotal_cents, tier):
pct = TIERS.get(tier, 0)
if subtotal_cents >= 10_000:
pct += 5
return min(pct, 20)
def apply_discount(subtotal_cents, pct):
kept = subtotal_cents * (100 - pct)
return (kept + 50) // 100
def price_order(order, repo):
subtotal = 0
for line in order["lines"]:
item = repo.price_of(line["sku"])
subtotal += line_total(line["qty"], item)
pct = discount_pct(subtotal, order.get("tier", "none"))
return apply_discount(subtotal, pct)
Two suites, built at deliberately comparable effort:
- unit — 13 tests. Every function called directly;
price_ordertested with the repository replaced by aMockand assertions on the call count. The style Item 109 argues against. - integration — 12 tests. Everything through
price_order, against a real in-memory repository object. No mocks, no direct calls into the pure core. The style Item 109 argues for.
Then 15 mutants, and each suite run against each one.
The result
Both suites reach 100% statement coverage of the module (15/15, measured with
the stdlib trace module — so the comparison is not confounded by one suite simply
executing more code). Both score 11/15. And they kill different mutants.
mutant unit integration
------------------------------------------------------------------
line_total: qty <= 0 -> qty < 0 KILLED KILLED
line_total: * -> + KILLED KILLED
discount_pct: >= -> > KILLED KILLED
discount_pct: threshold 10000 -> 10001 KILLED KILLED
discount_pct: bonus 5 -> 6 KILLED KILLED
discount_pct: drop the bonus KILLED KILLED
discount_pct: cap 20 -> 25 survived survived
discount_pct: min -> max KILLED KILLED
apply_discount: 100 - pct -> 100 + pct KILLED KILLED
apply_discount: drop half-up rounding KILLED survived
apply_discount: round 50 -> 49 survived survived
price_order: ignore the discount survived KILLED
price_order: default tier none -> gold KILLED survived
price_order: subtotal += -> = KILLED KILLED
price_order: swap line_total arguments survived KILLED
------------------------------------------------------------------
mutation score 11/15 11/15
killed by both: 9/15 killed by neither: 2/15
unit-only kills: 2 integration-only kills: 2 union: 13/15
Why each style is blind where it is
The blind spots are not noise. Each follows from how the suite is built, and each generalises.
Mocks cannot see wiring. price_order: ignore the discount rewrites
apply_discount(subtotal, pct) as apply_discount(subtotal, 0). The unit suite
tests apply_discount exhaustively in isolation, so mutating the call site is
invisible to it; and its single wiring test happened to use the default tier, where
pct is already 0 and the mutant is behaviourally identical. This is Item 109’s
argument, and on this mutant the book is exactly right.
Commutativity plus a mock hides argument order. swap line_total arguments
survives the unit suite because qty * unit_price is commutative — no assertion on
a returned value can ever catch it. The integration suite kills it, but not
arithmetically: with the arguments swapped, line_total(100, 0) no longer trips the
qty <= 0 guard, so the assertRaises test fails. The kill comes from an error-path
test. Worth internalising: the exception tests are doing more work than they look
like they are.
Fixtures cannot see boundaries they do not construct. drop half-up rounding
survives the integration suite because none of its fixture arithmetic happens to
land on a fractional cent. The unit suite kills it because it deliberately builds
1005 * 95 = 95475 to sit on the boundary. Boundary-value targeting is the thing
unit tests are actually for.
A helper that fills in every field makes every default unreachable.
default tier none -> gold survives the integration suite for a reason worth
pausing on: its convenience helper builds every order with tier set explicitly, so
order.get("tier", "none") never once falls back to its default. The test data
constructor silently deleted a branch from the program under test. If you take one
thing from this section, take this one — it is invisible in a coverage report,
because the line is covered; only the default’s value is untested.
Two mutants survive both, for different reasons. round 50 -> 49 needs an exact
half-cent input that neither suite constructs — a real, shared gap. cap 20 -> 25
is a genuine equivalent mutant: the largest achievable discount is 15 (gold’s 10
plus the 5-point threshold bonus), so min(pct, 20) is unreachable and no test
can distinguish 20 from 25. Verified rather than assumed:
>>> max(discount_pct(s, t) for s in (0, 9999, 10_000, 10**9)
... for t in list(TIERS) + ["bogus"])
15
A bonus find from the same run, and a nice piece of self-criticism: the unit
suite’s test_cap asserts discount_pct(10_000, "gold") == 15. The test named
after the cap never exercises the cap. Naming a test after a branch is not the
same as covering it.
The verdict
For this module, the book’s claim and the guide’s claim are each about half right, and the useful conclusion is neither one:
Mock-based tests cannot see wiring. Fixture-based tests cannot see the inputs your fixtures do not construct. The two failure modes are complementary and predictable, so the choice is not “prefer integration” or “prefer unit” — it is to know which class of mutant you are trying to catch, and to measure rather than argue.
The union of the two suites scores 13/15 at exactly the effort of writing both. If you want a single rule to carry into an interview: coverage tells you what ran, mutation score tells you what was checked, and the style of your test determines which mutants you are even capable of catching.
The harness lives in verification/books/ if you want to re-run it or add mutants.
A trap that ate this experiment once. The first run of the table above reported 12/15 for both suites. It was wrong, and the cause is Item 97 — the very Item this matrix marks as a gap. Several mutants preserve the source file’s byte length exactly (
line_total(line["qty"], item)→line_total(item, line["qty"])— 17 characters either way), and CPython validates a cached.pycagainst the source’s (size, mtime) only. Rewriting the file in a tight loop can produce a same-size rewrite inside one mtime tick, at which point the interpreter runs stale bytecode from a different mutant and every verdict after that is fiction. The symptom wasline_total(0, 100)quietly failing to raise. The fix is-BplusPYTHONDONTWRITEBYTECODE=1plus clearing__pycache__between runs, and afinallythat restores the pristine source so a crash cannot leave a mutant on disk. If you ever write a mutation harness, assume this will happen to you.
5. The gaps, closed
Eighteen Items are genuinely absent from the guide. Here they are, with output from
an actual run rather than from memory. The full script is
verification/books/gaps_python.py.
5.1 Item 6 — a trailing comma silently changes a type
Parentheses do not make a tuple; the comma does.
a = (1) # int
b = (1,) # tuple
c = 1, # also a tuple
(1) -> int 1
(1,) -> tuple (1,)
1, -> tuple (1,)
total = 5, -> tuple (5,) <- silent type change
The bug this causes is a stray comma at the end of an assignment. total = 5, is
not a syntax error and not a warning; it is a one-element tuple, and the failure
surfaces somewhere far away when arithmetic hits it.
5.2 Item 13 — adjacent string literals concatenate
good = ["alpha", "beta", "gamma"]
oops = ["alpha", "beta" "gamma"] # missing comma
with commas: 3 items ['alpha', 'beta', 'gamma']
missing comma: 2 items ['alpha', 'betagamma']
A missing comma in a list of strings does not raise — it merges two elements. This is the single strongest argument for a linter in a codebase with long literal lists.
5.3 Item 19 — for ... else
The else block after a loop runs when the loop completed without break —
including when the loop body never ran at all.
def find(hay, needle):
for x in hay:
if x == needle:
break
else:
return "not found"
return "found"
find([1,2,3], 2) -> found
find([1,2,3], 9) -> not found
empty loop still runs else -> not found
It reads as “else, if the condition failed”, which is not what it does. Slatkin’s advice is to avoid it; I would soften that to “avoid it in code others will read, but recognise it instantly,” because it appears in the standard library and it is a plausible interview question.
5.4 Item 20 — the loop variable outlives the loop
after the loop, i = 2
never-entered loop leaves j unbound: UnboundLocalError
comprehension does not leak: True
Three separate rules in one: the loop variable survives the loop with its last value; a loop that never iterated leaves the name unbound entirely (so the “safe” pattern of reading it afterwards has two different failure modes); and comprehensions deliberately do not leak theirs.
This is the natural pairing for 02-javascript-core.md’s treatment of var versus
let and the temporal dead zone. Python and JavaScript made opposite choices here,
and being able to state both is worth more than knowing either.
5.5 Item 28 — __missing__ for key-dependent defaults
defaultdict’s factory takes no arguments, so it can never see the key that was
missing. Subclassing dict and defining __missing__ can.
class KeyAware(dict):
def __missing__(self, key):
value = f"computed:{key.upper()}"
self[key] = value # cache it, or the next lookup recomputes
return value
d['abc'] -> computed:ABC
stored -> {'abc': 'computed:ABC'}
defaultdict factory arity: list (no key available)
__missing__ fires only for d[key], not for d.get(key) — which is usually what
you want, and is worth knowing before you rely on it.
5.6 Item 42 — a walrus in a comprehension leaks; the loop variable does not
values = [1, 2, 3, 4, 5]
kept = [y for x in values if (y := x * 10) > 20]
kept = [30, 40, 50]
y leaked from the comprehension: 50
but the loop variable x did not: True
Two scoping rules that point in opposite directions inside one expression. The practical use is real — it lets you compute a value once and use it in both the condition and the output — but the leak is a genuine surprise.
5.7 Item 55 — private attributes are only name-mangled
class Base:
def __init__(self):
self.__secret = "base"
def peek(self):
return self.__secret
class Child(Base):
def __init__(self):
super().__init__()
self.__secret = "child" # a DIFFERENT attribute
attributes: ['_Base__secret', '_Child__secret']
Base.peek() still sees: base
reachable from outside: base
__x inside class Base compiles to _Base__x. That is the whole mechanism.
There is no access control — the attribute is trivially reachable as
obj._Base__secret — and the mangling means a subclass writing self.__secret
creates a second, independent attribute rather than overriding the first. Which
is exactly what mangling is for (avoiding accidental collisions), and exactly why
it is the wrong tool for “make this private”.
5.8 Item 65 — class body definition order is preserved
class Row:
first = "col0"
last = "col1"
email = "col2"
class body order preserved: ['first', 'last', 'email']
field -> column index: {'first': 0, 'last': 1, 'email': 2}
__dict__ keeps the order in which the class body defined things, so declaration
position can carry meaning — mapping fields to CSV column indexes is the canonical
use. This is the mechanism underneath every declarative field API you have used
(dataclasses, ORM models, serializer schemas), and it pairs directly with the
descriptor and __set_name__ material in 12-design-patterns-python.md.
5.9 Item 84 — the exception variable disappears
try:
raise ValueError("boom")
except ValueError as e:
captured = e # copy it out if you need it later
inside the handler: boom
after the handler, `e` is unbound (NameError)
the copy survives: boom
except E as e deletes e at the end of the block. This is deliberate — the
exception holds a traceback that references the frame, so keeping the name bound
would create a reference cycle — but it means the obvious code of assigning inside
the handler and reading afterwards raises NameError, including in a following
finally. Copy it to another name.
5.10 Item 86 — Exception versus BaseException
KeyboardInterrupt subclass of Exception: False
SystemExit subclass of Exception: False
GeneratorExit subclass of Exception: False
ValueError subclass of Exception: True
order: ['finally', 'except BaseException']
except Exception deliberately does not catch Ctrl-C, sys.exit(), or the
generator-shutdown signal — that is the entire reason the split exists. Meanwhile
finally and with still run for BaseException, so cleanup is safe without
catching. This is the correct answer to “what’s wrong with except Exception?” and
the correct answer to “why did my except Exception retry loop become
un-interruptible?” — because someone wrote except BaseException or a bare
except:.
5.11 Item 89 — a generator’s finally runs late
def gen():
try:
yield 1
yield 2
finally:
log.append("cleanup ran")
after one next(), finally has run? False
after close(), finally has run? True
abandoned generator cleaned up by GC? True
In an ordinary function finally runs before the value is returned. In a generator
it runs at exhaustion, close(), or garbage collection — whichever comes first.
A partially-consumed generator holding a file or a lock keeps holding it until the
collector injects GeneratorExit, which on CPython is prompt because of
refcounting and on other implementations is not.
The rule that follows: do not let a generator own a resource. Open the file in
the caller, pass it in, close it in the caller’s with.
5.12 Item 121 — a root exception per module
class ApiError(Exception): pass # the root
class NotFound(ApiError): pass
class RateLimited(ApiError): pass
one handler caught NotFound: no user 7
one handler caught RateLimited: slow down
Give a module one root exception and raise only its subclasses. Callers get a
single thing to catch; you get room to add specificity later without breaking them;
and an Exception that escapes your root means the bug is yours, not the
caller’s — which turns a broad handler into a real diagnostic. Small design move,
disproportionate payoff, and it composes with the Result/Either material in
12-design-patterns-python.md rather than competing with it.
5.13 Items 67, 77, 87, 90, 97, 98, 105, 114 — the operational gaps
These are real gaps but they are tool-shaped rather than concept-shaped, so they get a paragraph each rather than a worked example.
| Item | What to know |
|---|---|
67 subprocess | A child process has its own interpreter and therefore its own GIL, which is the one way to get true CPU parallelism without multiprocessing’s pickling constraints. run() for the simple case, Popen for pipelines, and always pass timeout= — a wedged child otherwise hangs the parent forever. |
77 threads ↔ asyncio | loop.run_in_executor(...) lets a coroutine await blocking code; asyncio.run_coroutine_threadsafe(...) lets synchronous code drive a coroutine from another thread. Together they make an incremental migration possible instead of a rewrite — the single most useful fact in the book’s concurrency chapter. |
87 traceback | Gives programmatic access to stack frames. Matters most in concurrent code, where the default printout is interleaved or swallowed entirely — see Item 72 on Thread not propagating exceptions. |
90 __debug__ | python -O compiles out every assert. So an assert must never carry program logic (validation, side effects), and conversely leaving asserts in a normal run is free diagnostics. |
97 __pycache__ | Source compiles to bytecode cached beside it; cold start is fastest when those files already exist and are in the OS page cache. Also the direct cause of the harness bug in §4 — the cache is validated on (size, mtime), which is weaker than you would assume. |
| 98 lazy imports | python -X importtime attributes startup cost per module. An import moved inside a function costs roughly twenty additions on the warm path and removes the cold-start hit entirely — a good trade for a CLI. |
105 datetime + zoneinfo | Keep everything in UTC internally and convert once, at the presentation edge. The time module is the wrong tool for zone conversion. Pairs with the injected-clock testing pattern in 12-design-patterns-python.md — a clock you can inject is also a clock you can pin to a timezone. |
114 pdb | breakpoint() drops in at a chosen point. The two you will forget: python -m pdb -c continue prog.py for post-mortem on a crash, and import pdb; pdb.pm() to inspect the exception you just got in a REPL. |
6. The high-priority partials
Thirteen Items where the guide states the rule but misses the part that makes the Item worth reading. These are the highest-value re-reads in the book, because you already have the scaffolding and are only adding the load-bearing detail.
6.1 Item 9 — the match capture trap is narrower than advertised, and worse where it bites
The guide teaches match state machines in 12-design-patterns-python.md with no
warning that a bare name in a case captures rather than compares. Testing the
actual boundary produced a better answer than the book’s:
A: bare name then case _ -> SyntaxError: name capture 'RED' makes remaining patterns unreachable
B: bare name last -> compiled; f('green') = red-branch (captured 'green')
...and RED is now rebound to: red
C: dotted name -> f('green') = other | f('red') = red-branch
So the compiler does protect you in the obvious form — a bare-name case
followed by any other pattern is a hard SyntaxError on 3.10+. The trap only
survives compilation when the bare-name case is last, and then it is worse than
the book suggests: it matches everything and rebinds the name you thought you
were comparing against. The fix is a dotted name (Colour.RED, C.RED), an enum
member, or a literal.
Read the Item; then read it as “use a dotted name, always” rather than “avoid
match”.
6.2 Item 22 — the list case is the dangerous one
The guide has one sentence, in 06-data-structures-python.md, about dicts raising
RuntimeError. The list behaviour is worse because it does not raise:
xs = [1, 2, 3, 4, 5, 6]
for x in xs:
if x < 4:
xs.remove(x)
remove-while-iterating gave: [2, 4, 5, 6] (correct answer: [4, 5, 6])
rebuild in place gave: [4, 5, 6]
dict raises instead of skipping: dictionary changed size during iteration
2 survives. The list iterator holds an index; removing element 0 shifts
everything left, so the next __next__ skips what is now at index 0. Dicts fail
loudly, lists fail silently — and silent is the one that ships. The remedy is
xs[:] = [...], which rebuilds while keeping the same list object for any other
references.
6.3 Items 30 and 33 — the two halves of Python’s scoping story
The guide covers rebinding versus mutation as mechanics (04-python-core.md)
and the late-binding closure trap with its i=i fix. What is missing is the
API-design rule that follows from the first, and the write side of the second.
caller's list was mutated: ['a', 'b']
copy-on-entry leaves it alone: ['a']
nonlocal lets the closure write: 1 2 3
without nonlocal: UnboundLocalError: cannot access local variable 'n' where it is not associated with a value
Item 30’s rule: if your function mutates an argument, the name must say so, or it
must not do it. Item 33’s other half: a closure can read an enclosing scope
freely, but any assignment makes the name local for the whole function body — hence
UnboundLocalError on a += that looks like it should work. nonlocal is the
opt-out. (Note 3.11’s much-improved message; on 3.8 this said only
local variable 'n' referenced before assignment.)
This is the direct counterpart to 02-javascript-core.md’s closure material, and
“contrast Python’s nonlocal with JavaScript’s lexical capture” is a very
answerable interview question that the guide currently cannot answer.
6.4 Item 69 — the GIL does not make your invariants atomic
04-python-core.md measures the GIL’s effect on throughput thoroughly. It never
makes the corollary point, which is the one that causes bugs: GIL-protected
bytecode is not a critical section. A read-modify-write spanning two objects, or
even a += on a shared counter, can interleave. You still need threading.Lock.
This is the most commonly misunderstood consequence of the GIL and it belongs
directly after the measurement.
6.5 Item 78 — the asyncio version of “don’t block the loop”
The guide teaches Node’s event loop in real depth, including libuv phase ordering
and the nextTick → microtask → phase hierarchy. It never states the identical rule
for asyncio: a blocking syscall inside a coroutine stalls the entire loop.
asyncio.run(main(), debug=True) will name the offending coroutine for you. The
cross-language symmetry here is free teaching and the guide leaves it on the floor.
6.6 Items 92 and 93 — the guide’s own methodology, unnamed
This pair is a genuinely awkward omission. The whole guide is measurement-first: it
reports benchmark numbers throughout, 01-complexity-and-big-o.md teaches empirical
growth-exponent estimation by doubling, and 02-javascript-core.md documents a
benchmark that showed no difference because it was broken. Yet cProfile, Stats
and timeit are barely named.
So the guide teaches the discipline better than the book does, and the tools worse.
Read Items 92–93 for the tools: cProfile not profile, Profile.runcall to scope
a subtree, Stats to slice the output, and timeit with setup= to keep
initialisation out of the measurement. Then keep the guide’s scepticism about what
a microbenchmark actually proves.
6.7 Items 86, 89, 106, 113, 124 — briefly
-
86 and 89 are closed in §5.10 and §5.11; they are listed as partials only because the guide mentions
BaseExceptionandGeneratorExitin passing. -
106
Decimal— the guide explains IEEE-754 and its traps thoroughly on the JavaScript side and never introduces Python’s remedy. The measured contrast:float sum of 100 x 0.01 -> 1.0000000000000007 == 1.0? False Decimal sum of 100 x 0.01 -> 1.00 == 1? True Decimal quantize half-up -> 2.68 float round() half-even -> 2.67Two lessons in four lines: accumulated float error is real at trivial scale, and
round()is banker’s rounding, not the half-up rounding money wants. And the rule people get wrong —Decimal(1.1)imports the float error you were trying to escape (1.100000000000000088817841970012523233890533447265625); construct from a string. -
113
assertAlmostEqual— present in ch.17’s assertion table, but the reason is orphaned. Float comparison flakiness is a named category in ch.18’s flakiness taxonomy and these two should reference each other. -
124 static typing — the guide goes extremely deep on the TypeScript type system and covers Python’s
Protocolversus ABC, but never treats Python’s own gradual typing as a subject. This is the one place the two halves of the guide should meet and do not.
7. What this book covers that the guide deliberately will not
Thirteen Items are real, useful advice that is not study material: Items 1, 2, 4,
35, 41, 94, 96, 116, 117, 120, 125 (environment, style, packaging, deployment,
choosing libraries) and 95 (ctypes), 118 (docstrings).
Read them once when you are setting up a project. They will not be asked about, and adding them here would dilute what this guide is for. The exception is Item 121, which looks like collaboration advice and is actually API design — it is closed in §5.12.
8. Reading order if you own the book
If you are working through the book alongside this guide, this order front-loads the value:
- The gaps (§5): Items 6, 13, 19, 20, 28, 42, 55, 65, 84, 86, 89, 121, then the operational eight — 67, 77, 87, 90, 97, 98, 105, 114.
- The high-priority partials (§6): Items 9, 22, 30, 33, 69, 78, 92, 93, 106, 113, 124.
- Item 109 with §4 open beside it — it is the one place the book and this guide disagree, and the measurement is more useful than either position.
- Skim the covered 45 only if a topic feels rusty; the guide reference in the matrix will be at least as deep.
- Skip the 13 in §7 until you need them.
That is roughly 40 Items of real reading out of 125 — which is what the matrix is for.
Next: 21. Effective TypeScript (2nd ed.) — crosswalk and gap closure
Verify it yourself
books/pricing.py
"""System under test for the unit-vs-integration mutation experiment.
Deliberately shaped like real code: a pure core (three small functions with
real boundaries) plus an imperative shell that fetches data and wires the core
together. Both kinds of suite can reach all of it, by different routes.
"""
TIERS = {"none": 0, "silver": 5, "gold": 10}
def line_total(qty, unit_price_cents):
"""Cents for one line. Rejects non-positive quantities."""
if qty <= 0:
raise ValueError("qty must be positive")
return qty * unit_price_cents
def discount_pct(subtotal_cents, tier):
"""Tier discount, with a spend threshold that unlocks a 5-point bonus."""
pct = TIERS.get(tier, 0)
if subtotal_cents >= 10_000:
pct += 5
return min(pct, 20)
def apply_discount(subtotal_cents, pct):
"""Subtract pct% and round half-up to the cent."""
kept = subtotal_cents * (100 - pct)
return (kept + 50) // 100
def price_order(order, repo):
"""Shell: look each sku up, sum the lines, discount by tier."""
subtotal = 0
for line in order["lines"]:
item = repo.price_of(line["sku"])
subtotal += line_total(line["qty"], item)
pct = discount_pct(subtotal, order.get("tier", "none"))
return apply_discount(subtotal, pct)
books/test_unit.py
"""Unit-only suite: every function in isolation, dependency mocked.
The style Item 109 argues against. 12 test methods.
"""
import unittest
from unittest.mock import Mock
import pricing
class TestLineTotal(unittest.TestCase):
def test_multiplies(self):
self.assertEqual(pricing.line_total(3, 250), 750)
def test_one(self):
self.assertEqual(pricing.line_total(1, 999), 999)
def test_zero_rejected(self):
with self.assertRaises(ValueError):
pricing.line_total(0, 100)
def test_negative_rejected(self):
with self.assertRaises(ValueError):
pricing.line_total(-1, 100)
class TestDiscountPct(unittest.TestCase):
def test_tiers(self):
self.assertEqual(pricing.discount_pct(0, "none"), 0)
self.assertEqual(pricing.discount_pct(0, "silver"), 5)
self.assertEqual(pricing.discount_pct(0, "gold"), 10)
def test_unknown_tier(self):
self.assertEqual(pricing.discount_pct(0, "platinum"), 0)
def test_threshold_exact(self):
self.assertEqual(pricing.discount_pct(10_000, "none"), 5)
def test_threshold_just_below(self):
self.assertEqual(pricing.discount_pct(9_999, "none"), 0)
def test_cap(self):
self.assertEqual(pricing.discount_pct(10_000, "gold"), 15)
self.assertEqual(pricing.discount_pct(1_000_000, "gold"), 15)
class TestApplyDiscount(unittest.TestCase):
def test_no_discount(self):
self.assertEqual(pricing.apply_discount(1000, 0), 1000)
def test_ten_pct(self):
self.assertEqual(pricing.apply_discount(1000, 10), 900)
def test_rounds_half_up(self):
# 1005 * 95 = 95475 -> 954.75 -> 955
self.assertEqual(pricing.apply_discount(1005, 5), 955)
class TestPriceOrderWiring(unittest.TestCase):
def test_calls_repo_per_line_and_returns(self):
repo = Mock()
repo.price_of.return_value = 100
out = pricing.price_order(
{"lines": [{"sku": "A", "qty": 2}, {"sku": "B", "qty": 1}]}, repo
)
self.assertEqual(repo.price_of.call_count, 2)
self.assertEqual(out, 300)
if __name__ == "__main__":
unittest.main()
books/test_integration.py
"""Integration-only suite: everything through price_order with a real repo.
The style Item 109 argues for. No mocks, no direct calls to the pure core.
Comparable effort: 12 test methods, similar line count to the unit suite.
"""
import unittest
import pricing
class InMemoryRepo:
"""A real collaborator, not a double: same contract, in-process storage."""
def __init__(self, prices):
self._prices = dict(prices)
def price_of(self, sku):
return self._prices[sku]
REPO = InMemoryRepo({"CHEAP": 100, "MID": 2_500, "DEAR": 6_000})
def order(lines, tier="none"):
return {"lines": [{"sku": s, "qty": q} for s, q in lines], "tier": tier}
class TestPriceOrder(unittest.TestCase):
def test_single_line_no_discount(self):
self.assertEqual(pricing.price_order(order([("CHEAP", 1)]), REPO), 100)
def test_multi_line_sums(self):
self.assertEqual(
pricing.price_order(order([("CHEAP", 2), ("MID", 1)]), REPO), 2_700
)
def test_quantity_multiplies(self):
self.assertEqual(pricing.price_order(order([("CHEAP", 7)]), REPO), 700)
def test_silver_tier(self):
self.assertEqual(
pricing.price_order(order([("CHEAP", 1)], "silver"), REPO), 95
)
def test_gold_tier(self):
self.assertEqual(
pricing.price_order(order([("CHEAP", 1)], "gold"), REPO), 90
)
def test_unknown_tier_is_full_price(self):
self.assertEqual(
pricing.price_order(order([("CHEAP", 1)], "platinum"), REPO), 100
)
def test_threshold_bonus_applies(self):
# 2 x 6000 = 12000 >= 10000 -> 0 + 5 = 5%
self.assertEqual(pricing.price_order(order([("DEAR", 2)]), REPO), 11_400)
def test_threshold_bonus_stacks_with_tier(self):
# 12000, gold -> 10 + 5 = 15%
self.assertEqual(
pricing.price_order(order([("DEAR", 2)], "gold"), REPO), 10_200
)
def test_below_threshold_no_bonus(self):
# 4 x 2500 = 10000 is exactly at the threshold
self.assertEqual(pricing.price_order(order([("MID", 4)]), REPO), 9_500)
def test_rounding_visible_end_to_end(self):
# 1 x 100 + 1 x 2500 = 2600, silver 5% -> 2470
self.assertEqual(
pricing.price_order(order([("CHEAP", 1), ("MID", 1)], "silver"), REPO),
2_470,
)
def test_bad_quantity_propagates(self):
with self.assertRaises(ValueError):
pricing.price_order(order([("CHEAP", 0)]), REPO)
def test_empty_order(self):
self.assertEqual(pricing.price_order(order([]), REPO), 0)
if __name__ == "__main__":
unittest.main()
books/mutate.py
#!/usr/bin/env python3
"""Settle EP3 Item 109 with data instead of opinion.
Two suites over the same module, built at comparable effort - one unit-only with
the dependency mocked, one integration-only through the entry point with a real
collaborator. Mutate the module, run each suite against each mutant, and report
which mutants each suite kills.
Also reports line coverage per suite, so the comparison is not confounded by one
suite simply touching more code.
"""
import os, shutil, subprocess, sys
from pathlib import Path
HERE = Path(__file__).parent
SRC = HERE / "pricing.py"
ORIG = SRC.read_text()
# (label, pattern, replacement) - each must apply exactly once
MUTANTS = [
("line_total: qty <= 0 -> qty < 0", "if qty <= 0:", "if qty < 0:"),
("line_total: * -> +", "return qty * unit_price_cents", "return qty + unit_price_cents"),
("discount_pct: >= -> >", "if subtotal_cents >= 10_000:", "if subtotal_cents > 10_000:"),
("discount_pct: threshold 10000 -> 10001", "if subtotal_cents >= 10_000:", "if subtotal_cents >= 10_001:"),
("discount_pct: bonus 5 -> 6", "pct += 5", "pct += 6"),
("discount_pct: drop the bonus", "pct += 5", "pct += 0"),
("discount_pct: cap 20 -> 25", "return min(pct, 20)", "return min(pct, 25)"),
("discount_pct: min -> max", "return min(pct, 20)", "return max(pct, 20)"),
("apply_discount: 100 - pct -> 100 + pct", "kept = subtotal_cents * (100 - pct)", "kept = subtotal_cents * (100 + pct)"),
("apply_discount: drop half-up rounding", "return (kept + 50) // 100", "return kept // 100"),
("apply_discount: round 50 -> 49", "return (kept + 50) // 100", "return (kept + 49) // 100"),
("price_order: ignore the discount", "return apply_discount(subtotal, pct)", "return apply_discount(subtotal, 0)"),
("price_order: default tier none -> gold", 'order.get("tier", "none")', 'order.get("tier", "gold")'),
("price_order: subtotal += -> =", "subtotal += line_total(line[\"qty\"], item)", "subtotal = line_total(line[\"qty\"], item)"),
("price_order: swap line_total arguments", "line_total(line[\"qty\"], item)", "line_total(item, line[\"qty\"])"),
]
SUITES = [("unit", "test_unit.py"), ("integration", "test_integration.py")]
def run(suite_file):
"""True if the suite passes.
Bytecode caching MUST be off here. Several mutants below preserve the file's
byte length exactly (`line["qty"], item` -> `item, line["qty"]`), and
CPython validates a cached .pyc on (size, mtime) only - so with a coarse
mtime the interpreter will happily run stale bytecode from a *different*
mutant and the whole table becomes fiction. Cf. Effective Python Item 97.
"""
shutil.rmtree(HERE / "__pycache__", ignore_errors=True)
env = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}
r = subprocess.run([sys.executable, "-B", "-m", "unittest", "-q",
suite_file.replace(".py", "")],
cwd=HERE, capture_output=True, text=True, env=env)
return r.returncode == 0
def main():
(HERE / "pricing.py.orig").write_text(ORIG) # pristine backup, for recovery
try:
_main()
finally:
SRC.write_text(ORIG) # never leave a mutant on disk
shutil.rmtree(HERE / "__pycache__", ignore_errors=True)
def _main():
# sanity: both suites must be green on the unmutated module
SRC.write_text(ORIG)
for name, f in SUITES:
if not run(f):
print(f"FATAL: {name} suite fails on the unmutated module", file=sys.stderr)
sys.exit(1)
print("both suites green on the original module\n")
width = max(len(m[0]) for m in MUTANTS)
print(f"{'mutant':{width}} {'unit':>11} {'integration':>12}")
print("-" * (width + 26))
score = {"unit": 0, "integration": 0}
both = neither = 0
for label, pat, rep in MUTANTS:
if ORIG.count(pat) != 1:
print(f"FATAL: pattern for {label!r} matched {ORIG.count(pat)} times", file=sys.stderr)
sys.exit(1)
SRC.write_text(ORIG.replace(pat, rep))
res = {}
for name, f in SUITES:
killed = not run(f) # suite fails => mutant detected
res[name] = killed
score[name] += killed
if res["unit"] and res["integration"]:
both += 1
if not res["unit"] and not res["integration"]:
neither += 1
fmt = lambda k: "KILLED" if k else "survived"
print(f"{label:{width}} {fmt(res['unit']):>11} {fmt(res['integration']):>12}")
SRC.write_text(ORIG)
n = len(MUTANTS)
print("-" * (width + 26))
print(f"{'mutation score':{width}} {score['unit']:>8}/{n} {score['integration']:>9}/{n}")
print(f"\nkilled by both: {both}/{n} killed by neither: {neither}/{n}")
print(f"unit-only kills: {score['unit'] - both} integration-only kills: {score['integration'] - both}")
if __name__ == "__main__":
main()
books/cover.py
#!/usr/bin/env python3
"""Statement coverage of pricing.py per suite, using stdlib trace (no pip needed).
Confounder check for the mutation experiment: if one suite simply executes more
of the module, a difference in mutation score says nothing interesting. Equal
coverage with different kills is the finding.
Each suite runs in a FRESH subprocess with bytecode writing disabled - in-process
sys.modules surgery plus a stale __pycache__ produced wrong numbers once already.
"""
import ast, json, os, shutil, subprocess, sys
from pathlib import Path
HERE = Path(__file__).parent
TARGET = str((HERE / "pricing.py").resolve())
CHILD = r'''
import json, os, sys, trace, unittest
sys.path.insert(0, os.getcwd())
TARGET = os.path.abspath("pricing.py")
name = sys.argv[1]
t = trace.Trace(count=1, trace=0, ignoredirs=[sys.prefix, sys.exec_prefix])
suite = unittest.TestLoader().loadTestsFromName(name)
runner = unittest.TextTestRunner(stream=open(os.devnull, "w"), verbosity=0)
res = t.runfunc(runner.run, suite)
hit = sorted({ln for (fn, ln) in t.results().counts if fn == TARGET})
print(json.dumps({"ok": res.wasSuccessful(), "run": res.testsRun, "lines": hit}))
'''
def executable_lines():
"""Statements inside function bodies, excluding docstrings (never executed)."""
tree = ast.parse(Path(TARGET).read_text())
lines = set()
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
body = node.body
if (body and isinstance(body[0], ast.Expr)
and isinstance(body[0].value, ast.Constant)
and isinstance(body[0].value.value, str)):
body = body[1:]
for stmt in body:
for sub in ast.walk(stmt):
if isinstance(sub, ast.stmt) and not isinstance(sub, ast.FunctionDef):
lines.add(sub.lineno)
return lines
def measure(name):
shutil.rmtree(HERE / "__pycache__", ignore_errors=True)
env = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}
r = subprocess.run([sys.executable, "-B", "-c", CHILD, name],
cwd=HERE, capture_output=True, text=True, env=env)
if r.returncode != 0:
sys.exit(f"child failed for {name}:\n{r.stderr}")
return json.loads(r.stdout.strip().splitlines()[-1])
def main():
want = executable_lines()
out = {n: measure(n) for n in ("test_unit", "test_integration")}
for n, d in out.items():
if not d["ok"]:
sys.exit(f"FATAL: {n} is not green ({d['run']} tests) - fix before measuring")
print(f"executable statements in pricing.py functions: {len(want)}")
print(f"(suites: {out['test_unit']['run']} unit tests, "
f"{out['test_integration']['run']} integration tests)\n")
hit = {n: set(d["lines"]) & want for n, d in out.items()}
for n, lines in hit.items():
miss = sorted(want - lines)
print(f"{n:18} {len(lines):>2}/{len(want)} {100*len(lines)/len(want):5.1f}%"
+ (f" missing: {miss}" if miss else " (all covered)"))
union = hit["test_unit"] | hit["test_integration"]
print(f"{'union':18} {len(union):>2}/{len(want)} {100*len(union)/len(want):5.1f}%")
print(f"\nreached only by the unit suite: "
f"{sorted(hit['test_unit'] - hit['test_integration']) or 'none'}")
print(f"reached only by the integration suite: "
f"{sorted(hit['test_integration'] - hit['test_unit']) or 'none'}")
shutil.rmtree(HERE / "__pycache__", ignore_errors=True)
if __name__ == "__main__":
main()
books/gaps_python.py
#!/usr/bin/env python3
"""Every Python snippet destined for chapter 20, executed. Nothing ships unrun.
Run: python3 -B gaps_python.py
"""
import io, sys, traceback
OUT = []
def show(label, fn):
"""Run fn, capture what it printed or raised, and record it."""
buf = io.StringIO()
old, sys.stdout = sys.stdout, buf
try:
fn()
err = None
except BaseException as e: # noqa: BLE001 - deliberate
err = f"{type(e).__name__}: {e}"
finally:
sys.stdout = old
OUT.append((label, buf.getvalue().rstrip(), err))
# ---------------------------------------------------------------- Item 6
def item6():
a = (1) # not a tuple
b = (1,) # tuple
c = 1, # also a tuple
print(f"(1) -> {type(a).__name__} {a!r}")
print(f"(1,) -> {type(b).__name__} {b!r}")
print(f"1, -> {type(c).__name__} {c!r}")
total = 5, # the stray comma bug
print(f"total = 5, -> {type(total).__name__} {total!r} <- silent type change")
# ---------------------------------------------------------------- Item 13
def item13():
good = ["alpha", "beta", "gamma"]
oops = ["alpha", "beta" "gamma"] # missing comma: implicit concat
print(f"with commas: {len(good)} items {good}")
print(f"missing comma: {len(oops)} items {oops}")
# ---------------------------------------------------------------- Item 19
def item19():
def find(hay, needle):
for x in hay:
if x == needle:
break
else:
return "not found"
return "found"
print("find([1,2,3], 2) ->", find([1, 2, 3], 2))
print("find([1,2,3], 9) ->", find([1, 2, 3], 9))
print("empty loop still runs else ->", find([], 1))
# ---------------------------------------------------------------- Item 20
def item20():
for i in range(3):
pass
print("after the loop, i =", i)
for j in []:
pass
try:
j
except NameError as e:
print("never-entered loop leaves j unbound:", type(e).__name__)
print("comprehension does not leak:", "k" not in dir())
# ---------------------------------------------------------------- Item 28
def item28():
from collections import defaultdict
class KeyAware(dict):
def __missing__(self, key):
value = f"computed:{key.upper()}"
self[key] = value
return value
d = KeyAware()
print("d['abc'] ->", d["abc"])
print("stored ->", dict(d))
dd = defaultdict(list) # factory takes no args - cannot see the key
print("defaultdict factory arity:", dd.default_factory.__name__, "(no key available)")
# ---------------------------------------------------------------- Item 42
def item42():
values = [1, 2, 3, 4, 5]
kept = [y for x in values if (y := x * 10) > 20]
print("kept =", kept)
print("y leaked from the comprehension:", y)
print("but the loop variable x did not:", "x" not in locals())
# ---------------------------------------------------------------- Item 55
def item55():
class Base:
def __init__(self):
self.__secret = "base" # name-mangled
self._protected = "base"
def peek(self):
return self.__secret
class Child(Base):
def __init__(self):
super().__init__()
self.__secret = "child" # a DIFFERENT attribute
c = Child()
print("attributes:", sorted(k for k in vars(c) if "secret" in k))
print("Base.peek() still sees:", c.peek())
print("reachable from outside:", c._Base__secret)
# ---------------------------------------------------------------- Item 65
def item65():
class Row:
first = "col0"
last = "col1"
email = "col2"
order = [k for k in vars(Row) if not k.startswith("__")]
print("class body order preserved:", order)
print("field -> column index:", {k: i for i, k in enumerate(order)})
# ---------------------------------------------------------------- Item 84
def item84():
try:
raise ValueError("boom")
except ValueError as e:
captured = e # copy it out
print("inside the handler:", e)
try:
e
except NameError:
print("after the handler, `e` is unbound (NameError)")
print("the copy survives:", captured)
# ---------------------------------------------------------------- Item 86
def item86():
for exc in (KeyboardInterrupt, SystemExit, GeneratorExit, ValueError):
caught = issubclass(exc, Exception)
print(f"{exc.__name__:18} subclass of Exception: {caught}")
ran = []
try:
try:
raise KeyboardInterrupt("ctrl-c")
except Exception:
ran.append("except Exception")
finally:
ran.append("finally")
except BaseException:
ran.append("except BaseException")
print("order:", ran)
# ---------------------------------------------------------------- Item 89
def item89():
log = []
def gen():
try:
yield 1
yield 2
finally:
log.append("cleanup ran")
g = gen()
next(g)
print("after one next(), finally has run?", bool(log))
g.close()
print("after close(), finally has run?", bool(log))
log.clear()
for _ in gen():
break # abandoned mid-iteration
import gc; gc.collect()
print("abandoned generator cleaned up by GC?", bool(log))
# ---------------------------------------------------------------- Item 106
def item106():
from decimal import Decimal, ROUND_HALF_UP
print("0.1 + 0.2 == 0.3 ->", 0.1 + 0.2 == 0.3)
print("Decimal from float ->", Decimal(1.1))
print("Decimal from str ->", Decimal("1.1"))
# a hundred 1-cent charges
f = sum(0.01 for _ in range(100))
d = sum(Decimal("0.01") for _ in range(100))
print(f"float sum of 100 x 0.01 -> {f!r} == 1.0? {f == 1.0}")
print(f"Decimal sum of 100 x 0.01 -> {d} == 1? {d == 1}")
# and the rounding you actually want for money
print("Decimal quantize half-up ->",
(Decimal("2.675")).quantize(Decimal("0.01"), ROUND_HALF_UP))
print("float round() half-even ->", round(2.675, 2))
# ---------------------------------------------------------------- Item 22 (list case)
def item22():
xs = [1, 2, 3, 4, 5, 6]
for x in xs:
if x < 4:
xs.remove(x)
print("remove-while-iterating gave:", xs, " (correct answer: [4, 5, 6])")
ys = [1, 2, 3, 4, 5, 6]
ys[:] = [y for y in ys if y >= 4]
print("rebuild in place gave: ", ys)
d = {"a": 1, "b": 2}
try:
for k in d:
d[k + "!"] = 0
except RuntimeError as e:
print("dict raises instead of skipping:", e)
# ---------------------------------------------------------------- Item 30
def item30():
def add_tag(tags, t):
tags.append(t) # mutates the caller's list
return tags
mine = ["a"]
add_tag(mine, "b")
print("caller's list was mutated:", mine)
def add_tag_safe(tags, t):
return [*tags, t]
mine2 = ["a"]
add_tag_safe(mine2, "b")
print("copy-on-entry leaves it alone:", mine2)
# ---------------------------------------------------------------- Item 33
def item33():
def counter():
n = 0
def bump():
nonlocal n
n += 1
return n
return bump
b = counter()
print("nonlocal lets the closure write:", b(), b(), b())
def broken():
n = 0
def bump():
try:
n += 1 # UnboundLocalError: assignment makes it local
except UnboundLocalError as e:
return f"UnboundLocalError: {e}"
return bump()
print("without nonlocal:", broken())
# ---------------------------------------------------------------- Item 9
def item9():
# A: bare-name capture with a later pattern -> the COMPILER rejects it.
src_a = (
"RED = 'red'\n"
"def f(c):\n"
" match c:\n"
" case RED: return 'red-branch'\n"
" case _: return 'other'\n"
)
try:
compile(src_a, "<a>", "exec")
print("A: bare name then case _ -> compiled (no protection)")
except SyntaxError as e:
print(f"A: bare name then case _ -> SyntaxError: {e.msg}")
# B: bare-name capture as the LAST pattern -> compiles, and swallows everything.
src_b = (
"RED = 'red'\n"
"def f(c):\n"
" match c:\n"
" case 'blue': return 'blue-branch'\n"
" case RED: return f'red-branch (captured {RED!r})'\n"
" return 'unreachable'\n"
)
ns = {}
exec(compile(src_b, "<b>", "exec"), ns)
print("B: bare name last -> compiled; f('green') =", ns["f"]("green"))
print(" ...and RED is now rebound to:", ns["RED"])
# C: the fix - a dotted name compares instead of capturing.
class C:
RED = "red"
def classify(colour):
match colour:
case C.RED:
return "red-branch"
case _:
return "other"
print("C: dotted name -> f('green') =", classify("green"),
"| f('red') =", classify("red"))
# ---------------------------------------------------------------- Item 121
def item121():
class ApiError(Exception): pass
class NotFound(ApiError): pass
class RateLimited(ApiError): pass
for exc in (NotFound("no user 7"), RateLimited("slow down")):
try:
raise exc
except ApiError as e:
print(f"one handler caught {type(e).__name__}: {e}")
CASES = [
("Item 6 - single-element tuples", item6),
("Item 9 - match bare-name capture trap", item9),
("Item 13 - implicit string concatenation", item13),
("Item 19 - for/else", item19),
("Item 20 - loop variable after the loop", item20),
("Item 22 - mutating while iterating", item22),
("Item 28 - __missing__", item28),
("Item 30 - arguments can be mutated", item30),
("Item 33 - closures and nonlocal", item33),
("Item 42 - walrus leaks from a comprehension", item42),
("Item 55 - private attributes are only mangled", item55),
("Item 65 - class body definition order", item65),
("Item 84 - exception variable disappears", item84),
("Item 86 - Exception vs BaseException", item86),
("Item 89 - generator finally and GeneratorExit", item89),
("Item 106 - Decimal", item106),
("Item 121 - root exception", item121),
]
if __name__ == "__main__":
for label, fn in CASES:
show(label, fn)
bad = 0
for label, out, err in OUT:
print(f"\n### {label}")
if out:
print(out)
if err:
print(f"!!! UNEXPECTED RAISE -> {err}")
bad += 1
print(f"\n{'='*60}\n{len(OUT)} cases, {bad} unexpected failures")
sys.exit(1 if bad else 0)
books/coverage-matrix.tsv
EP3 1 Know Which Version of Python You're Using OUT_OF_SCOPE Environment hygiene: confirm which interpreter your `python` actually is. LOW
EP3 2 Follow the PEP 8 Style Guide OUT_OF_SCOPE Adopt the community style guide; let black/pylint enforce it. LOW
EP3 3 Never Expect Python to Detect Errors at Compile Time PARTIAL 04-python-core.md § The object model Python defers almost all error checking to runtime, so a typo on a cold path ships; static analysis is the only pre-runtime net. MED
EP3 4 Write Helper Functions Instead of Complex Expressions OUT_OF_SCOPE Extract dense one-liners into named helpers. LOW
EP3 5 Prefer Multiple-Assignment Unpacking over Indexing PARTIAL 13-problem-sets.md; 15-study-plan-and-flashcards.md Unpacking generalises to any iterable and to arbitrary nesting; prefer it to index arithmetic. Guide uses unpacking pervasively but never teaches it; a reader who is rusty gets no explanation. MED
EP3 6 Always Surround Single-Element Tuples with Parentheses MISSING A lone trailing comma silently makes a one-element tuple, so a stray comma changes a value's type rather than erroring. MED
EP3 7 Consider Conditional Expressions for Simple Inline Logic PARTIAL 14-cheatsheets.md § TypeScript and JavaScript quick reference Python's ternary puts the true-branch first, unlike C-family `?:`; use it only where it does not cost clarity. Guide's cross-language cheatsheet does not contrast Python's operand order with the JS/TS ternary, which is the actual trip hazard for someone switching languages daily. LOW
EP3 8 Prevent Repetition with Assignment Expressions PARTIAL 12-design-patterns-python.md The walrus operator assigns and evaluates in one expression, and can stand in for the do/while and switch forms Python lacks. Guide uses `:=` in examples without ever introducing it or showing the loop-and-a-half idiom it exists to serve. MED
EP3 9 Consider match for Destructuring in Flow Control PARTIAL 12-design-patterns-python.md § match state machines; 11-design-patterns-typescript.md `match` shines for destructuring heterogeneous object graphs, but a bare name in a `case` captures rather than compares, and each builtin type has its own pattern semantics. Guide teaches `match` state machines with no warning about the bare-name capture trap; a reader following the guide would write `case RED:` expecting a comparison and silently match everything. HIGH
EP3 10 Know the Differences Between bytes and str PARTIAL 04-python-core.md; 05-data-structures-typescript.md `bytes` holds octets, `str` holds code points, the two never mix under operators, and `open` needs an explicit `encoding=` or a binary mode. Guide covers the TS/JS side of text encoding but the Python str/bytes divide and the default-encoding hazard are close to absent. MED
EP3 11 Prefer Interpolated F-Strings over C-Style Format Strings PARTIAL 14-cheatsheets.md F-strings subsume `%` and `str.format`, and allow arbitrary expressions inside the format specifier itself. LOW
EP3 12 Understand the Difference Between repr and str when Printing Objects PARTIAL 04-python-core.md § Data model and dunders `str` is for humans and hides type, `repr` is for programmers and should round-trip; `!r` and `%r` select the latter. Guide lists `__repr__`/`__str__` in the dunder table but never makes the debugging argument for defining `__repr__` on every class. MED
EP3 13 Prefer Explicit String Concatenation over Implicit MISSING Adjacent string literals concatenate silently, so a missing comma in a list of strings merges two elements instead of erroring. MED
EP3 14 Know How to Slice Sequences PARTIAL 06-data-structures-python.md Slicing tolerates out-of-range bounds, and assigning to a slice splices in place even when the lengths differ. Guide slices constantly but never states the two non-obvious rules; slice assignment with a length change is the one that surprises people. MED
EP3 15 Avoid Striding and Slicing in a Single Expression PARTIAL 06-data-structures-python.md Combining start, end and a negative stride in one slice is near-unreadable; split it in two, or use `itertools.islice`. MED
EP3 16 Prefer Catch-All Unpacking over Slicing PARTIAL 15-study-plan-and-flashcards.md A starred target absorbs the remainder into a list and may sit in any position, which beats parallel slicing arithmetic. MED
EP3 17 Prefer enumerate over range COVERED 08-algorithm-patterns.md Loop with `enumerate` rather than indexing a `range`; it takes an optional start value. LOW
EP3 18 Use zip to Process Iterators in Parallel PARTIAL 14-cheatsheets.md `zip` is lazy and stops at the shortest input without complaint; `strict=True` turns that silent truncation into an error. The silent-truncation trap and `strict=True` appear only in the cheatsheet table, not in any chapter that teaches iteration. MED
EP3 19 Avoid else Blocks After for and while Loops MISSING A loop's `else` runs only when no `break` fired, which almost nobody reads correctly; avoid it. MED
EP3 20 Never Use for Loop Variables After the Loop Ends MISSING The loop variable outlives the loop, is simply unbound if the loop never ran, and comprehensions deliberately do not leak theirs. Guide covers JS `let`/`var`/TDZ scoping in ch02 in depth but never states Python's contrasting rule, which is the natural cross-language pairing. MED
EP3 21 Be Defensive when Iterating over Arguments COVERED 12-design-patterns-python.md (`iter(g) is g`, single-pass exhaustion); 04-python-core.md § Generators and coroutines A function that walks its argument twice breaks silently on an iterator; detect one with `iter(x) is x` or `collections.abc.Iterator`. HIGH
EP3 22 Never Modify Containers While Iterating over Them PARTIAL 06-data-structures-python.md Mutating a container mid-iteration raises or silently skips; iterate a copy, or stage changes in a second container and merge. Guide has exactly one sentence on the dict `RuntimeError` and nothing on the list case, where deletion silently skips elements instead of raising - the more dangerous of the two. HIGH
EP3 23 Pass Iterators to any and all for Efficient Short-Circuiting PARTIAL 02-javascript-core.md; 12-design-patterns-python.md `any`/`all` short-circuit and always return a real bool, unlike `or`/`and`; wrapping the argument in a list comprehension throws the short-circuit away. The listcomp-defeats-short-circuit point is the actionable half and the guide does not make it. MED
EP3 24 Consider itertools for Working with Iterators and Generators COVERED 06-data-structures-python.md; 12-design-patterns-python.md; 04-python-core.md `itertools` splits into linking, filtering, and combining iterators. MED
EP3 25 Be Cautious when Relying on Dictionary Insertion Ordering COVERED 06-data-structures-python.md § dict compact layout; 04-python-core.md `dict` has preserved insertion order since 3.7, but dict-*like* objects need not; guard with a type check or an annotation. MED
EP3 26 Prefer get over in and KeyError to Handle Missing Dictionary Keys COVERED 06-data-structures-python.md Of the four missing-key idioms, `get` wins for simple values; reach for `defaultdict` where `setdefault` tempts you. MED
EP3 27 Prefer defaultdict over setdefault for Internal State COVERED 06-data-structures-python.md Own the dict, use `defaultdict`; receive someone else's, use `get`. MED
EP3 28 Know How to Construct Key-Dependent Default Values with __missing__ MISSING 04-python-core.md (one passing mention) Subclass `dict` and define `__missing__` when the default must depend on the key - which `defaultdict`'s zero-argument factory cannot do. MED
EP3 29 Compose Classes Instead of Deeply Nesting Dictionaries, Lists, and Tuples COVERED 12-design-patterns-python.md; 06-data-structures-python.md § dataclass comparison table Stop at one level of nested builtins; promote bookkeeping state to dataclasses and then to real classes. MED
EP3 30 Know That Function Arguments Can Be Mutated PARTIAL 04-python-core.md § The object model (rebinding vs mutation) Arguments arrive as references, so a callee can mutate what you passed; say so in the name and docs, or copy on entry. Guide explains rebinding-vs-mutation as language mechanics but never turns it into the API-design rule, which is the part that prevents bugs. HIGH
EP3 31 Return Dedicated Result Objects Instead of Unpacking More Than Three Variables COVERED 12-design-patterns-python.md § Result/Either; 06-data-structures-python.md § dataclass comparison Past three return values, positional unpacking becomes a silent-reordering bug; return a small class instead. MED
EP3 32 Prefer Raising Exceptions to Returning None COVERED 12-design-patterns-python.md § Result/Either; 14-cheatsheets.md § trap list `None` as a sentinel collides with every other falsy value; raise instead, and annotate the non-optional return. MED
EP3 33 Know How Closures Interact with Variable Scope and nonlocal PARTIAL 04-python-core.md (late-binding closures, the `i=i` fix) Closures read enclosing scopes freely but cannot rebind them without `nonlocal`; keep `nonlocal` to small functions. Guide covers the late-binding-in-a-loop trap and its default-argument fix, but `nonlocal` itself gets two passing mentions - and the write-to-enclosing-scope half is what interviewers actually probe against JS closures. HIGH
EP3 34 Reduce Visual Noise with Variable Positional Arguments PARTIAL 12-design-patterns-python.md `*args` tidies call sites, but splatting a generator into it materialises the whole thing, and adding a positional later silently reinterprets existing calls. The two failure modes - unbounded memory from `*generator`, and the silent breakage when a new positional is prepended - are absent. MED
EP3 35 Provide Optional Behavior with Keyword Arguments OUT_OF_SCOPE Pass optional arguments by keyword so new behaviour can be added without touching callers. LOW
EP3 36 Use None and Docstrings to Specify Dynamic Default Arguments COVERED 04-python-core.md § Trap 1 - mutable default arguments (plus a Q&A drill); 12-design-patterns-python.md Defaults evaluate once at definition, so any mutable or time-dependent default must be `None` plus an in-body fallback. HIGH
EP3 37 Enforce Clarity with Keyword-Only and Positional-Only Arguments PARTIAL 12-design-patterns-python.md; 14-cheatsheets.md `*` forces keyword-only, `/` forces positional-only, and the span between them is the ordinary both-ways default. One mention each; the `/` marker in particular is common in CPython's own signatures and a reader will meet it in help() output without knowing what it means. MED
EP3 38 Define Function Decorators with functools.wraps COVERED 12-design-patterns-python.md § Decorators (four shapes, bottom-up stacking); 04-python-core.md A naive decorator destroys the wrapped function's identity and breaks introspection; `functools.wraps` restores it. HIGH
EP3 39 Prefer functools.partial over lambda Expressions for Glue Functions COVERED 12-design-patterns-python.md `partial` pins arguments declaratively; fall back to `lambda` only when you must reorder them. MED
EP3 40 Use Comprehensions Instead of map and filter PARTIAL 06-data-structures-python.md; 08-algorithm-patterns.md Comprehensions read better than `map`/`filter` and can skip items inline, but they materialise everything. MED
EP3 41 Avoid More Than Two Control Subexpressions in Comprehensions OUT_OF_SCOPE Beyond two `for`/`if` clauses a comprehension is less readable than the loop it replaced. LOW
EP3 42 Reduce Repetition in Comprehensions with Assignment Expressions MISSING A walrus inside a comprehension lets a computed value be reused in the same clause - and unlike the loop variable, it leaks to the enclosing scope. MED
EP3 43 Consider Generators Instead of Returning Lists COVERED 04-python-core.md § Generators and coroutines; 12-design-patterns-python.md Yielding keeps working memory flat and reads better than accumulating into a list. HIGH
EP3 44 Consider Generator Expressions for Large List Comprehensions COVERED 04-python-core.md; 06-data-structures-python.md Generator expressions compose by feeding one into the next, staying fast and constant-memory. MED
EP3 45 Compose Multiple Generators with yield from COVERED 04-python-core.md; 12-design-patterns-python.md `yield from` delegates to a sub-generator without the manual re-yield loop. MED
EP3 46 Pass Iterators into Generators as Arguments Instead of Calling send PARTIAL 04-python-core.md § Generators and coroutines; 12-design-patterns-python.md `send` injects a value into a paused `yield`, but mixing it with `yield from` produces stray `None`s; pass an input iterator instead. Guide documents what `send` does; the book's recommendation against it, and the `yield from` interaction that motivates that recommendation, are not there. MED
EP3 47 Manage Iterative State Transitions with a Class Instead of throw PARTIAL 12-design-patterns-python.md § match state machines `throw` re-raises inside a paused generator, but the resulting nesting is worse than an explicit stateful class. MED
EP3 48 Accept Functions Instead of Classes for Simple Interfaces COVERED 12-design-patterns-python.md (`__call__`, strategy) Functions are first-class, so a single-method interface should just be a function; use `__call__` when it needs state. MED
EP3 49 Prefer Object-Oriented Polymorphism over Functions with isinstance Checks COVERED 12-design-patterns-python.md § Protocol vs ABC Dispatch through subclass methods rather than an `isinstance` ladder. MED
EP3 50 Consider functools.singledispatch for Functional-Style Programming COVERED 12-design-patterns-python.md (18 mentions, incl. visitor discussion) `singledispatch` gives dynamic dispatch without class-centric layout, keeping one operation's cases together. MED
EP3 51 Prefer dataclasses for Defining Lightweight Classes COVERED 06-data-structures-python.md § tuple/namedtuple/NamedTuple/dataclass comparison table; 12-design-patterns-python.md `@dataclass` removes the boilerplate and the mistakes that come with hand-written `__init__`/`__eq__`/`__repr__`. HIGH
EP3 52 Use @classmethod Polymorphism to Construct Objects Generically COVERED 12-design-patterns-python.md § Factory; 04-python-core.md Python has one `__init__`, so alternative constructors are `@classmethod`s - and they can be polymorphic across subclasses. MED
EP3 53 Initialize Parent Classes with super COVERED 04-python-core.md § C3 MRO and cooperative super(); 12-design-patterns-python.md Zero-argument `super()` plus the C3 MRO is what makes diamond inheritance initialise each base exactly once. HIGH
EP3 54 Consider Composing Functionality with Mix-in Classes COVERED 12-design-patterns-python.md; 11-design-patterns-typescript.md Prefer stateless mix-ins to multiple inheritance with `__init__` and instance attributes. MED
EP3 55 Prefer Public Attributes over Private Ones MISSING Double-underscore attributes are only name-mangled to `_Class__attr`, not protected; use one underscore and documentation instead, reserving `__` for genuine subclass name collisions. MED
EP3 56 Prefer dataclasses for Creating Immutable Objects COVERED 04-python-core.md (`frozen=True` gives `__hash__`); 06-data-structures-python.md § hashability table; 12-design-patterns-python.md § structural sharing `frozen=True` yields value equality, a stable hash, and `replace()` for functional updates. MED
EP3 57 Inherit from collections.abc Classes for Custom Container Types COVERED 12-design-patterns-python.md; 04-python-core.md Subclassing a builtin is fine for simple cases; otherwise inherit from `collections.abc` so the missing methods are named for you. MED
EP3 58 Use Plain Attributes Instead of Setter and Getter Methods PARTIAL 04-python-core.md § Descriptors; 12-design-patterns-python.md Expose plain attributes and reach for `@property` only when access needs behaviour - and keep that behaviour fast and side-effect-free. `@property` gets a handful of mentions across the guide and no section of its own, despite being the mechanism this and the next two Items build on. MED
EP3 59 Consider @property Instead of Refactoring Attributes PARTIAL 04-python-core.md § Descriptors `@property` lets an existing attribute grow behaviour without breaking callers; heavy use is a signal to redesign instead. MED
EP3 60 Use Descriptors for Reusable @property Methods COVERED 04-python-core.md § Descriptors and attribute lookup order; 12-design-patterns-python.md § Descriptors When several attributes need the same validation, promote the `@property` to a descriptor class - storing per-instance state via `__set_name__` to avoid leaks. HIGH
EP3 61 Use __getattr__, __getattribute__, and __setattr__ for Lazy Attributes COVERED 04-python-core.md § Attribute lookup order; 12-design-patterns-python.md § Proxy `__getattr__` fires only on a miss, `__getattribute__` on every access; both need `super()` calls to avoid infinite recursion. HIGH
EP3 62 Validate Subclasses with __init_subclass__ COVERED 12-design-patterns-python.md § __init_subclass__ over metaclasses `__init_subclass__` validates a subclass at definition time, and cooperative `super()` calls make several layers compose. HIGH
EP3 63 Register Class Existence with __init_subclass__ COVERED 12-design-patterns-python.md § Registry Automatic registration at subclass definition removes the forgotten-registration bug. MED
EP3 64 Annotate Class Attributes with __set_name__ PARTIAL 12-design-patterns-python.md § Descriptors `__set_name__` tells a descriptor the attribute name it was bound to, which is what makes declarative field APIs possible. MED
EP3 65 Consider Class Body Definition Order to Establish Relationships Between Attributes MISSING A class body's definition order survives into `__dict__`, so declaration position can carry meaning - mapping fields to column indexes, for instance. MED
EP3 66 Prefer Class Decorators over Metaclasses for Composable Class Extensions COVERED 12-design-patterns-python.md § Class decorators; § __init_subclass__ over metaclasses A class decorator takes a class and returns one, composes with other decorators, and covers most of what people reach for metaclasses to do. MED
EP3 67 Use subprocess to Manage Child Processes MISSING 04-python-core.md (one passing mention) Child processes sidestep the GIL entirely; `run` for the simple case, `Popen` for pipelines, and always a `timeout` so a wedged child cannot hang the parent. MED
EP3 68 Use Threads for Blocking I/O; Avoid for Parallelism COVERED 04-python-core.md § The GIL, measured; 15-study-plan-and-flashcards.md The GIL prevents CPU parallelism across threads but not concurrency across blocking syscalls, which is what threads are actually for. HIGH
EP3 69 Use Lock to Prevent Data Races in Threads PARTIAL 11-design-patterns-typescript.md; 12-design-patterns-python.md The GIL does not make your invariants atomic; a read-modify-write across two objects still needs a mutex. Guide measures the GIL's effect on throughput but never makes the corollary point that GIL-protected bytecode is not the same as an atomic critical section - the misconception the Item exists to kill. HIGH
EP3 70 Use Queue to Coordinate Work Between Threads PARTIAL 04-python-core.md; 12-design-patterns-python.md § Producer-consumer A hand-rolled thread pipeline hits busy-waiting, shutdown signalling, completion detection, and unbounded queue growth; `Queue` solves all four. MED
EP3 71 Know How to Recognize When Concurrency Is Necessary PARTIAL 12-design-patterns-python.md Fan-out/fan-in pressure is the signal that a program needs concurrency rather than a faster loop. MED
EP3 72 Avoid Creating New Thread Instances for On-Demand Fan-out PARTIAL 04-python-core.md; 12-design-patterns-python.md A thread per unit of work costs memory and startup, and a bare `Thread` cannot propagate its exception back to the caller. The exception-swallowing property of raw `Thread` is the debugging-relevant half and it is absent. MED
EP3 73 Understand How Using Queue for Concurrency Requires Refactoring PARTIAL 12-design-patterns-python.md Fixed worker pools scale better than thread-per-task but cap total I/O parallelism and cost real restructuring per pipeline stage. LOW
EP3 74 Consider ThreadPoolExecutor When Threads Are Necessary PARTIAL 04-python-core.md `ThreadPoolExecutor` buys bounded memory and exception propagation for little refactoring, at the price of a fixed `max_workers`. MED
EP3 75 Achieve Highly Concurrent I/O with Coroutines COVERED 04-python-core.md § Generators and coroutines; 12-design-patterns-python.md (21 mentions) Coroutines scale to tens of thousands of in-flight operations, giving fan-out/fan-in without the thread costs. HIGH
EP3 76 Know How to Port Threaded I/O to asyncio COVERED 04-python-core.md; 12-design-patterns-python.md `async` variants exist for `for`, `with`, generators, comprehensions and iterators, so a threaded design ports largely mechanically. MED
EP3 77 Mix Threads and Coroutines to Ease the Transition to asyncio MISSING `run_in_executor` lets a coroutine await blocking code and `run_coroutine_threadsafe` lets sync code drive a coroutine, which together permit an incremental migration rather than a rewrite. MED
EP3 78 Maximize Responsiveness of asyncio Event Loops with async-Friendly Worker Threads PARTIAL 04-python-core.md; 02-javascript-core.md § The event loop A blocking syscall inside a coroutine stalls the whole loop; `asyncio.run(debug=True)` surfaces the offenders. Guide teaches the Node event loop in real depth including phase ordering, but never states the same don't-block-the-loop rule for asyncio, nor mentions `debug=True`. The cross-language parallel is the obvious teaching opportunity. HIGH
EP3 79 Consider concurrent.futures for True Parallelism COVERED 04-python-core.md § The GIL; 12-design-patterns-python.md `ProcessPoolExecutor` is the sane door into multiprocessing; the raw module's advanced surface rarely earns its complexity. MED
EP3 80 Take Advantage of Each Block in try/except/else/finally PARTIAL 04-python-core.md (one mention) All four blocks have distinct jobs: `else` keeps the `try` narrow and separates the success path from the handlers. Guide has essentially one line on the four-block form despite `else`-after-`try` being both widely misunderstood and a plausible interview question. MED
EP3 81 assert Internal Assumptions and raise Missed Expectations PARTIAL 04-python-core.md; 18-testing-strategy.md `raise` is for conditions callers are meant to handle and belongs in the documented interface; `assert` is for your own invariants and is not. MED
EP3 82 Consider contextlib and with Statements for Reusable try/finally Behavior COVERED 12-design-patterns-python.md § Context managers (`__exit__` truthy suppresses); 04-python-core.md `@contextmanager` turns a generator into a reusable `try/finally`, and the yielded value is what `as` binds. HIGH
EP3 83 Always Make try Blocks as Short as Possible PARTIAL 04-python-core.md A wide `try` catches exceptions you never meant to handle; push the extra code into `else` or a separate statement. MED
EP3 84 Beware of Exception Variables Disappearing MISSING `except E as e` unbinds `e` at the end of the block - deliberately, to break a reference cycle - so reaching for it afterwards raises `NameError`; copy it to another name first. HIGH
EP3 85 Beware of Catching the Exception Class PARTIAL 12-design-patterns-python.md A broad `except Exception` insulates a boundary but hides defects; if you catch broadly you must log. MED
EP3 86 Understand the Difference Between Exception and BaseException PARTIAL 04-python-core.md `KeyboardInterrupt` and `SystemExit` derive from `BaseException`, not `Exception`, so `except Exception` deliberately lets them through - while `finally` and `with` still run. HIGH
EP3 87 Use traceback for Enhanced Exception Reporting MISSING The `traceback` module gives programmatic access to stack frames, which matters most in concurrent programs where the default printout is lost or interleaved. MED
EP3 88 Consider Explicitly Chaining Exceptions to Clarify Tracebacks PARTIAL 04-python-core.md (`raise ... from`); 12-design-patterns-python.md Re-raising inside a handler always records the original in `__context__`; `raise ... from` sets `__cause__` and controls what the traceback shows. MED
EP3 89 Always Pass Resources into Generators and Have Callers Clean Them Up Outside PARTIAL 04-python-core.md (`GeneratorExit`) A generator's `finally` does not run until exhaustion or close, and an abandoned generator is cleaned up only when the collector injects `GeneratorExit` - so a generator is the wrong place to own a file or a lock. Guide mentions `GeneratorExit` in passing; the resource-ownership rule that follows from it, and the deferred-`finally` timing that motivates the rule, are not stated. HIGH
EP3 90 Never Set __debug__ to False MISSING `python -O` compiles out every `assert`, so asserts must never carry program logic - and leaving them in an un-optimised run is free diagnostic value. MED
EP3 91 Avoid exec and eval Unless You're Building a Developer Tool PARTIAL 16-testing-node-test.md (JS side only) `eval`/`exec` execute constructed source and reach into the surrounding scope; confine them to developer tooling. The guide discusses `eval` only on the JavaScript side; the Python `exec`/`eval` pair, and their scope-mutation behaviour, are not covered. MED
EP3 92 Profile Before Optimizing PARTIAL 04-python-core.md Bottlenecks are usually not where you think; use `cProfile` (not `profile`), and `Stats` to slice the output. The guide's own methodology is measurement-first and it reports many measured numbers, but it never teaches `cProfile`/`Stats` as tools the reader should use - a notable omission given the guide's whole stance. HIGH
EP3 93 Optimize Performance-Critical Code Using timeit Microbenchmarks PARTIAL 04-python-core.md; 01-complexity-and-big-o.md § The measured traps `timeit` with `setup=` excludes initialisation and normalises to comparable per-op numbers. The guide's ch01 already teaches empirical growth measurement and ch02 documents a benchmark that showed no difference because it was broken - so the pitfalls are covered better than the book's, but `timeit` itself as the standard tool is barely named. HIGH
EP3 94 Know When and How to Replace Python with Another Programming Language OUT_OF_SCOPE Exhaust in-Python optimisation before rewriting; moving hot paths to C works but costs correctness risk. LOW
EP3 95 Consider ctypes to Rapidly Integrate with Native Libraries OUT_OF_SCOPE 04-python-core.md (one mention) `ctypes` binds native libraries without a build step, at the cost of C-shaped APIs. LOW
EP3 96 Consider Extension Modules to Maximize Performance and Ergonomics OUT_OF_SCOPE A C extension runs at native speed and can use CPython's own protocols, but memory and error handling are easy to get wrong. LOW
EP3 97 Rely on Precompiled Bytecode and File System Caching to Improve Startup Time MISSING 04-python-core.md § Bytecode (disassembly only) Source compiles to bytecode cached in `__pycache__`, so cold-start cost is minimised when those files exist and are already in the OS page cache. MED
EP3 98 Lazy-Load Modules with Dynamic Imports to Reduce Startup Time MISSING `-X importtime` attributes startup cost per module, and an import moved inside a function costs about twenty additions on the warm path while removing the cold-start hit entirely. MED
EP3 99 Consider memoryview and bytearray for Zero-Copy Interactions with bytes COVERED 06-data-structures-python.md § memoryview/bytearray; 04-python-core.md `memoryview` slices the buffer protocol without copying and `bytearray` gives a mutable target, so received data can be spliced in place. MED
EP3 100 Sort by Complex Criteria Using the key Parameter COVERED 07-sorting-and-searching.md; 06-data-structures-python.md; 12-design-patterns-python.md A tuple-returning `key` composes criteria; where a field cannot be negated, sort repeatedly from least to most significant and lean on stability. HIGH
EP3 101 Know the Difference Between sort and sorted COVERED 07-sorting-and-searching.md; 14-cheatsheets.md `sort` mutates a list in place for less memory; `sorted` accepts any iterable and leaves the input alone. MED
EP3 102 Consider Searching Sorted Sequences with bisect COVERED 06-data-structures-python.md § bisect/SortedList; 07-sorting-and-searching.md; 01-complexity-and-big-o.md `bisect_left` is logarithmic where `index` and a scan are linear. HIGH
EP3 103 Prefer deque for Producer-Consumer Queues COVERED 06-data-structures-python.md § deque; 05-data-structures-typescript.md § ring buffer; 01-complexity-and-big-o.md `list.pop(0)` is linear so a list-as-FIFO degrades superlinearly; `deque` is O(1) at both ends. HIGH
EP3 104 Know How to Use heapq for Priority Queues COVERED 06-data-structures-python.md § binary heap (O(n) heapify, indexed decrease-key); 09-graphs-and-trees.md § Dijkstra `heapq` gives a real priority queue; items need a total order, so classes need `__lt__` or a tuple key. HIGH
EP3 105 Use datetime Instead of time for Local Clocks MISSING 12-design-patterns-python.md (injected-clock pattern only) Keep everything in UTC and convert with `datetime` plus `zoneinfo` only at the presentation edge; the `time` module is the wrong tool for zone conversion. MED
EP3 106 Use decimal when Precision Is Paramount PARTIAL 02-javascript-core.md § IEEE-754; 12-design-patterns-python.md `Decimal` gives exact arithmetic and explicit rounding for money - and must be constructed from a string, since a float argument imports the error you were avoiding. Guide explains IEEE-754 and its traps thoroughly on the JS side but never introduces Python's `Decimal`/`Fraction` as the remedy, and the construct-from-string rule is the detail people get wrong. HIGH
EP3 107 Make pickle Serialization Maintainable with copyreg PARTIAL 12-design-patterns-python.md (pickle in serialization discussion) `pickle` is only safe between trusted programs, and unpickling breaks when a class's fields change; `copyreg` pins the shape for forward compatibility. MED
EP3 108 Verify Related Behaviors in TestCase Subclasses COVERED 17-testing-python-unittest.md § TestCase, all 41 assertions, subTest; 19-testing-cheatsheet.md Subclass `TestCase`, one `test`-prefixed method per behaviour, the assertion helpers rather than bare `assert`, and `subTest` for table-driven cases. HIGH
EP3 109 Prefer Integration Tests over Unit Tests DIVERGENT 18-testing-strategy.md § pyramid/trophy/honeycomb; § functional core, imperative shell Because Python resolves almost everything at runtime, integration tests are argued to be the only reliable confidence, with unit tests reserved for edge-case-dense code. The book argues integration-first specifically from Python's dynamism. The guide presents the pyramid/trophy/honeycomb as a design choice and pushes a functional-core/imperative-shell split that makes unit tests load-bearing. These are different recommendations for the same language; the guide should state the book's position and the argument for it rather than quietly disagreeing. Settling it is empirical: take a Python module, build a unit-only suite and an integration-only suite at equal effort, and compare mutation scores - the guide already has the mutation harness to do exactly that. HIGH
EP3 110 Isolate Tests from Each Other with setUp, tearDown, setUpModule, tearDownModule COVERED 17-testing-python-unittest.md § fixtures, addCleanup/enterContext Per-test `setUp`/`tearDown` for isolation; module-level pair for harnesses shared by every case in the file. HIGH
EP3 111 Use Mocks to Test Code with Complex Dependencies COVERED 17-testing-python-unittest.md § unittest.mock (Mock/MagicMock/AsyncMock, 7 patch forms, autospec); 19-testing-cheatsheet.md `Mock` stands in for awkward dependencies; verify both the result and the calls, and inject via `patch` or keyword-only parameters. HIGH
EP3 112 Encapsulate Dependencies to Facilitate Mocking and Testing COVERED 18-testing-strategy.md § seams (parameterize/wrap/sprout/extract-pure-core); 17-testing-python-unittest.md When mock setup boilerplate piles up, wrap the dependency in a class, and add explicit seams for injection. HIGH
EP3 113 Use assertAlmostEqual to Control Precision in Floating Point Tests PARTIAL 17-testing-python-unittest.md (in the assertion table); 18-testing-strategy.md § flakiness taxonomy `assertEqual` on floats compares full precision and produces order-of-operations flakes; `assertAlmostEqual` with `places` or `delta` states the tolerance. The assertion is listed in ch17's table but the float-flakiness reasoning behind it is only glancingly connected to ch18's flakiness taxonomy, where it belongs as a named category. HIGH
EP3 114 Consider Interactive Debugging with pdb MISSING `breakpoint()` drops into `pdb` at a chosen point; `python -m pdb -c continue prog.py` and `pdb.pm()` do post-mortem inspection of an exception that already happened. MED
EP3 115 Use tracemalloc to Understand Memory Usage and Leaks COVERED 04-python-core.md § refcounting and generational GC `gc` tells you what exists, `tracemalloc` tells you where it was allocated - which is the question you actually have. MED
EP3 116 Know Where to Find Community-Built Modules OUT_OF_SCOPE PyPI plus `pip` is where community packages come from. LOW
EP3 117 Use Virtual Environments for Isolated and Reproducible Dependencies OUT_OF_SCOPE `python -m venv`, activate, `pip freeze` / `pip install -r` to reproduce. LOW
EP3 118 Write Docstrings for Every Function, Class, and Module OUT_OF_SCOPE 12-design-patterns-python.md Document every module, class and function, and do not restate what an annotation already says. LOW
EP3 119 Use Packages to Organize Modules and Provide Stable APIs PARTIAL 12-design-patterns-python.md; 17-testing-python-unittest.md (`__init__.py` in the test package) `__init__.py` makes a package, `__all__` declares its public surface, and a leading underscore marks internals. MED
EP3 120 Consider Module-Scoped Code to Configure Deployment Environments OUT_OF_SCOPE Module scope is ordinary code, so it can branch on host introspection to adapt to an environment. LOW
EP3 121 Define a Root Exception to Insulate Callers from APIs MISSING Give a module one root exception and raise only its subclasses: callers get a single thing to catch, you get a way to add specificity later without breaking them, and an unexpected `Exception` escaping means the bug is yours. HIGH
EP3 122 Know How to Break Circular Dependencies PARTIAL 02-javascript-core.md § CJS vs ESM live bindings; 12-design-patterns-python.md Mutual imports crash at startup; the real fix is extracting the shared part downward, and a function-scoped import is the cheap one. Guide covers module-cycle semantics on the JS side (CJS partial exports vs ESM live bindings) but not the Python import-cycle failure or its remedies - another natural cross-language pairing. MED
EP3 123 Consider warnings to Refactor and Migrate Usage PARTIAL 17-testing-python-unittest.md (assertWarns in the assertion table) `warnings` deprecates gently; `-W error` turns them fatal in CI, and warnings themselves deserve tests. MED
EP3 124 Consider Static Analysis via typing to Obviate Bugs PARTIAL 04-python-core.md; 12-design-patterns-python.md § Protocol vs ABC; 03-typescript-type-system.md Annotations plus a checker catch a large class of runtime bugs before they run. The guide goes extremely deep on the TypeScript type system and covers Python `Protocol` vs ABC, but never presents Python's own gradual-typing story as a subject - the one place the two halves of the guide should meet and do not. HIGH
EP3 125 Prefer Open Source Projects for Bundling over zipimport and zipapp OUT_OF_SCOPE Python can import from zips, many packages break when you do, and community tools handle single-file deployment better. LOW
ETS2 1 Understand the Relationship Between TypeScript and JavaScript PARTIAL 02-javascript-core.md; 03-typescript-type-system.md TypeScript is a syntactic superset of JavaScript whose checker deliberately goes beyond modelling runtime behaviour: it flags usage that is legal at runtime when an error is the likelier explanation. Guide teaches JS semantics and TS types as separate subjects and never states the superset relationship or the checker's willingness to reject working code - the framing the whole book rests on. HIGH
ETS2 2 Know Which TypeScript Options You're Using DIVERGENT 03-typescript-type-system.md § TS 6.0 to TS 7.0 Behaviour depends so heavily on compiler flags that the same source type-checks differently across projects; `noImplicitAny` and `strictNullChecks` are the two that matter most. The book (May 2024, TS ~5.4) tells readers to turn `strict` on. On TypeScript 6.0.3, installed here, `strict` is the default, and `baseUrl`, `node10` resolution, AMD and ES5 targets are gone along with `downlevelIteration`. Checkable: compile the book's opt-in examples with a bare `tsc` on 6.x and confirm the errors appear without any flag. HIGH
ETS2 3 Understand That Code Generation Is Independent of Types PARTIAL 03-typescript-type-system.md § erasableSyntaxOnly; 02-javascript-core.md Types are erased, so they cannot affect runtime behaviour, a file with type errors still emits, and no type check exists at runtime. Guide covers erasure via `erasableSyntaxOnly` and Node's type-stripping, but never states the consequence the book leads with: emit succeeds despite type errors, so a passing build is not a passing check. HIGH
ETS2 4 Get Comfortable with Structural Typing COVERED 03-typescript-type-system.md § Structural typing; 11-design-patterns-typescript.md; 12-design-patterns-python.md § Protocol TypeScript models JavaScript's duck typing structurally, so a value may satisfy your interface while carrying extra properties you never anticipated. HIGH
ETS2 5 Limit Use of the any Type COVERED 03-typescript-type-system.md (46 mentions of unknown/any); 14-cheatsheets.md `any` switches off checking for a symbol, and the damage spreads: broken contracts, dead editor support, unsafe refactors, hidden type errors. HIGH
ETS2 6 Use Your Editor to Interrogate and Explore the Type System OUT_OF_SCOPE 03-typescript-type-system.md (`// ^?` used throughout) Use the language service as the primary way to learn what the checker inferred. LOW
ETS2 7 Think of Types as Sets of Values MISSING A type is the set of values it admits: `never` is empty, a literal is a singleton, unions are unions and intersections are intersections, assignability is subset-hood, and `unknown` is the universe. HIGH
ETS2 8 Know How to Tell Whether a Symbol Is in the Type Space or Value Space MISSING The same identifier can name a type and a value independently, `typeof` means different things on each side of the divide, and `class` and `enum` introduce both at once - which is the root of most confusing TypeScript error messages. HIGH
ETS2 9 Prefer Type Annotations to Type Assertions COVERED 03-typescript-type-system.md § satisfies vs annotation vs assertion An annotation asks the checker to verify; an assertion tells it to stop asking. Prefer the former, and reserve assertions for facts the checker cannot know. HIGH
ETS2 10 Avoid Object Wrapper Types (String, Number, Boolean, Symbol, BigInt) PARTIAL 02-javascript-core.md § boxing and abstract equality Write the lowercase primitive types; the capitalised wrapper types describe boxed objects almost nobody means to require. Guide explains boxing on the JS side but does not connect it to the TS-level rule, and a reader who writes `String` instead of `string` gets no warning from the guide. MED
ETS2 11 Distinguish Excess Property Checking from Type Checking PARTIAL 03-typescript-type-system.md Object *literals* get an extra check that rejects unknown properties; assign through a variable first and structural typing lets the same object through. It is a distinct rule, not part of assignability. "Two passing mentions. This is a favourite interview question precisely because the ""why does it error inline but not via a variable?"" asymmetry looks like a bug, and the guide never explains it." HIGH
ETS2 12 Apply Types to Entire Function Expressions When Possible PARTIAL 03-typescript-type-system.md; 11-design-patterns-typescript.md § satisfies-typed strategy maps Annotate the whole function expression with a function type rather than each parameter and the return separately; it is shorter and it reuses. MED
ETS2 13 Know the Differences Between type and interface PARTIAL 03-typescript-type-system.md They are near-interchangeable; the differences that bite are declaration merging (interface only), unions and mapped types (type only), and the error messages you get. Three mentions. This is one of the most-asked TypeScript interview questions and the guide has no section on it. HIGH
ETS2 14 Use readonly to Avoid Errors Associated with Mutation COVERED 03-typescript-type-system.md (33 mentions); 11-design-patterns-typescript.md (52); § structural sharing Mark non-mutated parameters `readonly`/`Readonly<T>`: it documents the contract and stops accidental mutation inside the implementation. HIGH
ETS2 15 Use Type Operations and Generic Types to Avoid Repeating Yourself COVERED 03-typescript-type-system.md § 25 utility types, compiler-verified; § homomorphic mapped types DRY applies to types: name them, use `extends` to share interface fields, and derive with the utility types rather than restating shapes. HIGH
ETS2 16 Prefer More Precise Alternatives to Index Signatures MISSING 03-typescript-type-system.md (one passing mention) An index signature erodes safety much as `any` does - every key typechecks, including typos, and the editor stops helping; reach for an interface, `Record`, or `Map` instead. HIGH
ETS2 17 Avoid Numeric Index Signatures MISSING Array keys are strings at runtime; `number` as an index signature is a TypeScript-only fiction, so prefer `Array`, a tuple, `ArrayLike`, or `Iterable`. MED
ETS2 18 Avoid Cluttering Your Code with Inferable Types PARTIAL 03-typescript-type-system.md Annotate signatures, not local variables; a redundant annotation is noise that can also go stale. MED
ETS2 19 Use Different Variables for Different Types OUT_OF_SCOPE Introduce a second variable rather than reusing one for a differently typed value; names get better and inference gets simpler. LOW
ETS2 20 Understand How a Variable Gets Its Type PARTIAL 03-typescript-type-system.md § satisfies vs annotation vs assertion; § as const A declaration's type comes from widening its initialiser, and `const`, `let`, `as const` and an explicit annotation each widen differently. Guide covers `as const` and `satisfies` as tools but never lays out the widening rules they exist to control, so a reader knows the fix without knowing the mechanism. HIGH
ETS2 21 Create Objects All at Once PARTIAL 11-design-patterns-typescript.md Build an object in a single expression, using spread to compose, rather than assigning properties onto an empty literal and fighting the inferred type. MED
ETS2 22 Understand Type Narrowing COVERED 03-typescript-type-system.md § narrowing (25 mentions); 11-design-patterns-typescript.md § discriminated-union state machines Control flow refines types; discriminated unions and user-defined guards are what make the refinement reliable. HIGH
ETS2 23 Be Consistent in Your Use of Aliases PARTIAL 03-typescript-type-system.md Introducing an alias for a nested property helps readability but splits narrowing: refine the alias or the original consistently, never both. The narrowing-invalidation half - that a function call can silently discard a refinement - is the actionable part and is not covered. MED
ETS2 24 Understand How Context Is Used in Type Inference MISSING TypeScript infers from surrounding context, not just from the initialiser, so pulling a value out into a variable can change its type and break a call that worked inline. HIGH
ETS2 25 Understand Evolving Types MISSING Values initialised to `null`, `undefined` or `[]` get an implicit-any type that is allowed to *widen* as you assign to it, which is the one place TypeScript's types grow rather than narrow. MED
ETS2 26 Use Functional Constructs and Libraries to Help Types Flow PARTIAL 11-design-patterns-typescript.md; 05-data-structures-typescript.md Built-in and library combinators carry their types through a pipeline, where a hand-rolled loop loses them and needs annotations. MED
ETS2 27 Use async Functions Instead of Callbacks to Improve Type Flow COVERED 02-javascript-core.md § Promises/A+ and the event loop; 11-design-patterns-typescript.md § pMap/retry/circuit breaker Promises compose and carry types where callbacks do not; `async`/`await` is better still. HIGH
ETS2 28 Use Classes and Currying to Create New Inference Sites PARTIAL 03-typescript-type-system.md § NoInfer; 11-design-patterns-typescript.md Inference for a type-parameter list is all-or-nothing, so to get partial inference you split the call into two - via currying or a class - creating a second inference site. Guide covers `NoInfer` and inference control but not the all-or-nothing rule, which is the reason the currying workaround exists. MED
ETS2 29 Prefer Types That Always Represent Valid States COVERED 11-design-patterns-typescript.md § discriminated-union state machines (15 mentions); § Result/Either A type that can express invalid states will eventually hold one; model so the illegal combinations cannot be written. HIGH
ETS2 30 Be Liberal in What You Accept and Strict in What You Produce COVERED 11-design-patterns-typescript.md; 03-typescript-type-system.md § variance Parameter types should be broad and return types narrow; a union-typed return pushes the burden onto every caller. HIGH
ETS2 31 Don't Repeat Type Information in Documentation OUT_OF_SCOPE A comment or a name that restates a type will drift out of sync with it. LOW
ETS2 32 Avoid Including null or undefined in Type Aliases PARTIAL 11-design-patterns-typescript.md "Keep nullability out of the alias; let each use site decide, so `Foo` never secretly means ""maybe Foo""." MED
ETS2 33 Push Null Values to the Perimeter of Your Types PARTIAL 11-design-patterns-typescript.md § Result/Either Make a whole object nullable rather than sprinkling optional fields, so one check at the boundary settles it for the interior. Guide's `Result`/`Either` material solves the adjacent problem for errors; the all-or-nothing-nullability structuring rule for plain data is not stated. HIGH
ETS2 34 Prefer Unions of Interfaces to Interfaces with Unions COVERED 11-design-patterns-typescript.md § discriminated unions; 03-typescript-type-system.md An interface with several union-typed fields hides which combinations are legal; a union of interfaces names them. HIGH
ETS2 35 Prefer More Precise Alternatives to String Types COVERED 03-typescript-type-system.md § template-literal types; § branded types; 11-design-patterns-typescript.md Not every string is a possibility: use a union of literals, or a branded type, instead of `string`. HIGH
ETS2 36 Use a Distinct Type for Special Values PARTIAL 11-design-patterns-typescript.md; 02-javascript-core.md "A sentinel drawn from the value domain - `0`, `-1`, `""""` - is assignable where a real value is expected, so the checker cannot help; use `null`/`undefined` or a distinct type." HIGH
ETS2 37 Limit the Use of Optional Properties PARTIAL 11-design-patterns-typescript.md Optional properties multiply the states you must handle and scatter default-filling; prefer a separate input type normalised once into a fully-populated internal type. The two-type split (loose input, strict normalised internal) is a concrete pattern the guide's type-design material does not include. HIGH
ETS2 38 Avoid Repeated Parameters of the Same Type PARTIAL 11-design-patterns-typescript.md Consecutive same-typed parameters let callers swap them silently; take distinct types or a single object. MED
ETS2 39 Prefer Unifying Types to Modeling Differences PARTIAL 11-design-patterns-typescript.md Two near-identical types invite code that converts between them; unify unless the difference is real. MED
ETS2 40 Prefer Imprecise Types to Inaccurate Types MISSING There is an uncanny valley where a complex type is *more* wrong than a simple one; when you cannot model something accurately, model it loosely and say so rather than shipping a precise lie. HIGH
ETS2 41 Name Types Using the Language of Your Problem Domain OUT_OF_SCOPE Take type names from the domain vocabulary, not from their shape. LOW
ETS2 42 Avoid Types Based on Anecdotal Data PARTIAL 03-typescript-type-system.md Types hand-written from a few observed payloads get nullability and optionality wrong; generate them from the schema or take the official client's. MED
ETS2 43 Use the Narrowest Possible Scope for any Types COVERED 03-typescript-type-system.md Keep `any` to the smallest expression; never return it, or the hole propagates into every caller silently. HIGH
ETS2 44 Prefer More Precise Variants of any to Plain any COVERED 03-typescript-type-system.md `any[]`, `Record<string, any>`, `() => any` all preserve some checking that bare `any` throws away. MED
ETS2 45 Hide Unsafe Type Assertions in Well-Typed Functions COVERED 03-typescript-type-system.md § unsoundness; 05-data-structures-typescript.md Some correct implementations need an unsound assertion internally; confine it behind a signature that is honest. HIGH
ETS2 46 Use unknown Instead of any for Values with an Unknown Type COVERED 03-typescript-type-system.md (46 mentions) `unknown` accepts anything but permits nothing until narrowed, which is what you actually wanted from `any`. HIGH
ETS2 47 Prefer Type-Safe Approaches to Monkey Patching MISSING 03-typescript-type-system.md § module augmentation (mechanism only) Attaching data to built-ins or the DOM defeats the checker; if you must, use declaration merging or a custom interface assertion - and understand that `declare global` applies everywhere, not just your file. MED
ETS2 48 Avoid Soundness Traps COVERED 03-typescript-type-system.md § unsoundness; § array covariance is unsound; § method bivariance vs property contravariance Unsoundness is a value diverging from its static type at runtime; array covariance, method-parameter bivariance and non-null assertions are the usual doors in. HIGH
ETS2 49 Track Your Type Coverage to Prevent Regressions in Type Safety MISSING Measure the proportion of expressions with a non-`any` type so the `any`s you inherited cannot quietly multiply. MED
ETS2 50 Think of Generics as Functions Between Types COVERED 03-typescript-type-system.md § conditional types; § generics A generic is a function whose arguments and result are types; `extends` is how you constrain its domain. HIGH
ETS2 51 Avoid Unnecessary Type Parameters PARTIAL 03-typescript-type-system.md A type parameter that appears only once buys nothing and usually should be a concrete type or a constraint. The single-use-parameter smell is a crisp, memorable rule the guide's generics material does not name. MED
ETS2 52 Prefer Conditional Types to Overload Signatures COVERED 03-typescript-type-system.md § distributive conditional types A conditional type distributes over a union and so covers cases that would each need their own overload. HIGH
ETS2 53 Know How to Control the Distribution of Unions over Conditional Types COVERED 03-typescript-type-system.md § distributive conditional types; § Equals<X,Y> via deferred conditionals Distribution is automatic for a naked type parameter, and wrapping it in a tuple is how you switch it off. HIGH
ETS2 54 Use Template Literal Types to Model DSLs and Relationships Between Strings PARTIAL 03-typescript-type-system.md § template-literal types; 02-javascript-core.md Template literal types model structured string subsets, and combine with mapped and conditional types to express relations between key names. Only a couple of mentions on each side. The guide teaches the syntax but not the DSL-modelling use, nor the key-remapping combination which is where the technique earns its keep. HIGH
ETS2 55 Write Tests for Your Types COVERED 03-typescript-type-system.md § 25 utility + 8 semantic assertions, compiler-verified; § Equals<X,Y> Type-level tests must distinguish equality from assignability, and callback parameter types need testing too. Worth noting as a match rather than a gap: the guide independently arrived at the same `Equals<X,Y>` deferred-conditional harness the book recommends, and documents the `Expect<A,B>` alias failure that forces the inline form. HIGH
ETS2 56 Pay Attention to How Types Display PARTIAL 03-typescript-type-system.md Two equivalent types can display very differently; a `Resolve`/`Prettify` helper forces the readable form. MED
ETS2 57 Prefer Tail-Recursive Generic Types COVERED 03-typescript-type-system.md § recursion depth Accumulator-style tail-recursive conditional types are cheaper and hit a far higher depth limit than naively nested ones. MED
ETS2 58 Consider Codegen as an Alternative to Complex Types PARTIAL 03-typescript-type-system.md Past a point, generating declarations beats expressing the relationship in the type system. MED
ETS2 59 Use Never Types to Perform Exhaustiveness Checking COVERED 03-typescript-type-system.md § never (11 mentions); 11-design-patterns-typescript.md § discriminated-union state machines Assigning the fall-through value to `never` turns a newly added union member into a compile error. HIGH
ETS2 60 Know How to Iterate Over Objects PARTIAL 05-data-structures-typescript.md (`Object.entries`) `for...in` widens the key to `string` because the object may carry extra keys; `Object.entries` or an explicit key assertion is how you keep the narrow type. "The `for...in` key-widening surprise is a common source of ""why is this `string` and not my union?"" and the guide does not address it." HIGH
ETS2 61 Use Record Types to Keep Values in Sync PARTIAL 03-typescript-type-system.md § Record; 11-design-patterns-typescript.md § satisfies-typed strategy maps A `Record` keyed by a union forces every new member to be handled - fail-closed rather than fail-open when the union grows. Guide uses `satisfies`-typed strategy maps, which is the same fail-closed idea, but never names the fail-open/fail-closed choice that justifies it. HIGH
ETS2 62 Use Rest Parameters and Tuple Types to Model Variadic Functions COVERED 03-typescript-type-system.md § variadic tuples Variadic tuple types let one signature describe a function whose arity and parameter types co-vary. MED
ETS2 63 Use Optional Never Properties to Model Exclusive Or PARTIAL 03-typescript-type-system.md; 11-design-patterns-typescript.md § discriminated unions `A | B` is inclusive, so a value satisfying both is legal; `z?: never` on each arm is how you force exclusivity without a discriminant tag. The `?: never` trick and the inclusive-or point behind it are absent; the guide always reaches for a tag, which is not always available. HIGH
ETS2 64 Consider Brands for Nominal Typing COVERED 03-typescript-type-system.md § branded/nominal types (12 mentions); 11-design-patterns-typescript.md (15) A brand makes a type nominal: a value has it because you said so, not because the shape matched. HIGH
ETS2 65 Put TypeScript and @types in devDependencies OUT_OF_SCOPE Keep the compiler and type packages out of runtime dependencies, and never install the compiler globally. LOW
ETS2 66 Understand the Three Versions Involved in Type Declarations PARTIAL 03-typescript-type-system.md A library, its `@types` package, and the compiler version can each mismatch the other two, and each mismatch has its own symptom. MED
ETS2 67 Export All Types That Appear in Public APIs PARTIAL 11-design-patterns-typescript.md If a type appears anywhere in your public surface, consumers can extract it anyway, so export it deliberately. MED
ETS2 68 Use TSDoc for API Comments OUT_OF_SCOPE TSDoc comments surface in the editor where users need them; `@param`, `@returns`, Markdown. LOW
ETS2 69 Provide a Type for this in Callbacks if It's Part of Their API COVERED 02-javascript-core.md § [[HomeObject]] and this-binding; 03-typescript-type-system.md § ThisType If your callback API rebinds `this`, type it with a `this` parameter - and prefer not to design new APIs that way. MED
ETS2 70 Mirror Types to Sever Dependencies PARTIAL 11-design-patterns-typescript.md Copy the handful of fields you use instead of taking a transitive `@types` dependency; structural typing means the copy still fits. MED
ETS2 71 Use Module Augmentation to Improve Types COVERED 03-typescript-type-system.md § module augmentation Declaration merging lets you improve or extend someone else's types without forking them. MED
ETS2 72 Prefer ECMAScript Features to TypeScript Features COVERED 03-typescript-type-system.md § erasableSyntaxOnly; § TS 6.0/7.0; 16-testing-node-test.md § type stripping (the enum failure) Most TypeScript erases cleanly; enums, parameter properties, namespaces, experimental decorators and visibility modifiers do not, so prefer the ECMAScript equivalent. Reinforcing rather than diverging: the guide hit this empirically - a TS `enum` in a test file failed under Node's strip-only mode with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, needing `--experimental-transform-types`. That is the book's advice arriving as a runtime error. HIGH
ETS2 73 Use Source Maps to Debug TypeScript MISSING Debug the TypeScript you wrote, not the JavaScript that was emitted; source maps are what make the debugger line up. MED
ETS2 74 Know How to Reconstruct Types at Runtime PARTIAL 03-typescript-type-system.md (Zod mentioned); 19-testing-cheatsheet.md Types are erased, so runtime validation needs a separate mechanism: a schema library, generated validators, or hand-written guards. Guide names Zod in passing but never presents the general problem - that the type system stops at the I/O boundary - or the options for solving it, which is a routine design question. HIGH
ETS2 75 Understand the DOM Hierarchy OUT_OF_SCOPE 03-typescript-type-system.md (two mentions) `EventTarget`/`Node`/`Element`/`HTMLElement` and the `Event`/`MouseEvent` split matter in TypeScript in a way they do not in plain JavaScript. LOW
ETS2 76 Create an Accurate Model of Your Environment PARTIAL 03-typescript-type-system.md; 02-javascript-core.md § CJS vs ESM Declare the globals and scripts your code actually runs with, or the checker is validating against a fiction. MED
ETS2 77 Understand the Relationship Between Type Checking and Unit Testing COVERED 16-testing-node-test.md; 18-testing-strategy.md § coverage is not verification Types eliminate whole classes of error across all inputs; tests demonstrate behaviour on chosen inputs. You want both. Strong agreement worth citing: the guide's mutation-testing result - two suites at identical 100% line coverage scoring 0/5 and 4/5 - is independent evidence for the book's claim that these are complementary and neither subsumes the other. HIGH
ETS2 78 Pay Attention to Compiler Performance PARTIAL 03-typescript-type-system.md § TS 7.0 (tsgo, ~10x) Type-checking time is a real cost driven by project structure and by how expensive your types are to instantiate. The book's advice predates the Go port. The guide already covers TS 7.0/tsgo and its order-of-magnitude claim, so this is the one Item where the guide is ahead of the book rather than behind - worth measuring rather than repeating. MED
ETS2 79 Write Modern JavaScript PARTIAL 02-javascript-core.md § ES2026 iterator helpers; § optional chaining/nullish "Use the modern language and let the compiler target the old runtime; `??` rather than `||` for defaults, since `0` and `""""` are falsy." MED
ETS2 80 Use @ts-check and JSDoc to Experiment with TypeScript PARTIAL 03-typescript-type-system.md `// @ts-check` type-checks a JavaScript file in place, with JSDoc carrying the annotations - a migration on-ramp with no build change. MED
ETS2 81 Use allowJs to Mix TypeScript and JavaScript PARTIAL 03-typescript-type-system.md `allowJs` lets both languages coexist; get the build and tests green before converting anything. MED
ETS2 82 Convert Module by Module Up Your Dependency Graph PARTIAL 02-javascript-core.md § module graphs Start at the leaves and work up, adding `@types` for third-party edges first. MED
ETS2 83 Don't Consider Migration Complete Until You Enable noImplicitAny DIVERGENT 03-typescript-type-system.md § TS 6.0/7.0 (strict is now default) A migration is not finished while implicit `any` is still tolerated, because loose checking hides mistakes in the declarations themselves. The Item frames `noImplicitAny` as the finish line you opt into. On TypeScript 6.0.3 `strict` - and therefore `noImplicitAny` - is on by default, so for a new project the flag is the starting line and the migration advice inverts: you now opt *out* while migrating and remove the opt-out at the end. Checkable by running `tsc` with no config on a file with an unannotated parameter. HIGH
books/FINDINGS.md
# Verified findings (measured on this machine)
Environment: Python 3.11.15, Node 22.22.2, TypeScript 6.0.3, Linux x86-64.
## 1. TypeScript 6 is strict by default — the book's flag advice inverts
`Effective TypeScript` 2nd ed. (May 2024) targets TS ~5.4 and tells the reader to
turn `strict` on, treating `noImplicitAny` as the finish line of a migration
(Items 2 and 83). On **TypeScript 6.0.3**, with no `tsconfig.json` and no flags:
```
$ cat implicit.ts
export function greet(name) { return "hi " + name; }
$ tsc --noEmit implicit.ts
implicit.ts(1,23): error TS7006: Parameter 'name' implicitly has an 'any' type.
exit=2
$ tsc --noEmit --noImplicitAny false implicit.ts
exit=0
```
`strictNullChecks` likewise fires unprompted (`TS18047: 's' is possibly 'null'`).
So the advice inverts: `noImplicitAny` is now the *starting* line, and a migration
opts **out** and removes the opt-out at the end.
Five options the book's examples assume are now deprecated-and-erroring — not yet
removed, but each errors unless acknowledged, and each is documented as ceasing to
function in TypeScript 7.0:
| option | diagnostic on 6.0.3 |
|---|---|
| `--target es5` | `TS5107 ... deprecated and will stop functioning in TypeScript 7.0` |
| `--module amd` | `TS5107` |
| `--moduleResolution node10` | `TS5107` |
| `--downlevelIteration` | `TS5101` |
| `--baseUrl .` | `TS5101` |
Corroborating Item 72 (prefer ECMAScript features): `--erasableSyntaxOnly` rejects
`enum` outright with `TS1294: This syntax is not allowed when 'erasableSyntaxOnly'
is enabled`, which is the same wall the guide hit from the other side when a TS
`enum` in a `node:test` file failed with `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`.
## 2. Effective Python Item 109 — settled by measurement, and neither side wins
Item 109 argues that Python's dynamism makes integration tests the reliable way to
gain confidence, with unit tests reserved for edge-case-dense code. The guide's
ch.18 instead pushes a functional-core/imperative-shell split that makes unit tests
load-bearing. Rather than adjudicate by assertion, both were built and measured.
One module (`pricing.py`, 15 executable statements: a pure core of three functions
plus a shell that fetches prices and wires them together). Two suites at comparable
effort — **13 unit tests** with the dependency mocked, **12 integration tests**
through the entry point against a real in-memory collaborator, no mocks. 15 mutants.
**Both suites reach 100% statement coverage. Both score 11/15. They kill different mutants.**
```
mutant unit integration
------------------------------------------------------------------
line_total: qty <= 0 -> qty < 0 KILLED KILLED
line_total: * -> + KILLED KILLED
discount_pct: >= -> > KILLED KILLED
discount_pct: threshold 10000 -> 10001 KILLED KILLED
discount_pct: bonus 5 -> 6 KILLED KILLED
discount_pct: drop the bonus KILLED KILLED
discount_pct: cap 20 -> 25 survived survived
discount_pct: min -> max KILLED KILLED
apply_discount: 100 - pct -> 100 + pct KILLED KILLED
apply_discount: drop half-up rounding KILLED survived
apply_discount: round 50 -> 49 survived survived
price_order: ignore the discount survived KILLED
price_order: default tier none -> gold KILLED survived
price_order: subtotal += -> = KILLED KILLED
price_order: swap line_total arguments survived KILLED
------------------------------------------------------------------
mutation score 11/15 11/15
killed by both: 9/15 killed by neither: 2/15
unit-only kills: 2 integration-only kills: 2 union: 13/15
```
The blind spots are not random — each style fails in a way that follows from how it
is built:
**Mocks hide wiring.** `price_order: ignore the discount` replaces
`apply_discount(subtotal, pct)` with `apply_discount(subtotal, 0)`. The unit suite
tests `apply_discount` thoroughly *in isolation*, so mutating the call site is
invisible to it; its one wiring test used the default tier, where `pct` is already
0 and the mutant is behaviourally identical. This is exactly Item 109's argument,
and it is correct.
**Commutativity plus mocks hides argument order.** `swap line_total arguments`
survives the unit suite because `qty * unit_price` is commutative — no assertion on
a *value* can catch it. The integration suite kills it only through the error path:
with the arguments swapped, `line_total(100, 0)` no longer trips the `qty <= 0`
guard, so the `assertRaises` test fails. The kill comes from an exception test, not
an arithmetic one.
**Fixtures hide boundaries.** `drop half-up rounding` survives the integration
suite because none of its fixture arithmetic happens to land on a fractional cent;
the unit suite kills it because it deliberately constructs `1005 * 95 = 95475`.
**Helpers that fill in every field hide defaults.** `default tier none -> gold`
survives the integration suite because its `order()` helper always sets `tier`
explicitly, so `order.get("tier", "none")` never falls back. A convenience helper
that populates every field makes every default unreachable.
**Two mutants survive both, for different reasons.** `round 50 -> 49` needs an
exact half-cent case neither suite constructs — a real gap in both. `cap 20 -> 25`
is a true equivalent mutant: the maximum achievable discount is 15 (gold 10 plus
the 5-point threshold bonus), so `min(pct, 20)` is unreachable and no test can
distinguish 20 from 25.
Bonus finding from the same run: `test_unit.TestDiscountPct.test_cap` asserts 15 —
the test named after the cap never exercises the cap.
**Conclusion.** For this module, the book's claim and the guide's claim are both
half right. Neither style dominates; their blind spots are complementary and
predictable. The actionable rule is not "prefer integration" or "prefer unit" but:
mock-based tests cannot see wiring, and fixture-based tests cannot see the inputs
your fixtures do not construct — so choose the style per mutant class you care
about, and measure rather than argue.
## 3. A mutation harness that lied, because of `__pycache__`
The first run of the experiment above reported **12/15 for both suites**. That was
wrong, and the cause is worth keeping.
Several mutants preserve the source file's byte length exactly:
```
line_total(line["qty"], item) -> line_total(item, line["qty"])
both 17 characters inside the parens
pct += 5 -> pct += 6 min(pct, 20) -> max(pct, 20)
```
CPython validates a cached `.pyc` against the source's **(size, mtime)** only. A
harness that rewrites the file in a tight loop can produce a same-size rewrite
within the same mtime tick, at which point the interpreter runs **stale bytecode
from a different mutant** and every verdict after that is fiction. Symptom that
exposed it: `line_total(0, 100)` failing to raise, with `__pycache__` present.
```
$ rm -rf __pycache__ && python3 -B -c "..."
ValueError raised: qty must be positive # correct again
```
Fixed by clearing `__pycache__` and running each suite with `-B` /
`PYTHONDONTWRITEBYTECODE=1`, plus a `finally` that restores the pristine source so
a crash cannot leave a mutant on disk. The corrected scores are 11/15 and 11/15;
the stale cache had manufactured one false kill for each suite.
This is `Effective Python` Item 97 (precompiled bytecode and filesystem caching)
biting in practice — an Item the coverage matrix classifies as MISSING from the
guide. It earned its place.
## 4. PDF extraction hazard, observed
The Effective Python file was an **EPUB with a `.pdf` extension** (86 MB, 42 XHTML
files, code in real `<pre>` elements). That was lucky: structured markup gave clean
per-Item boundaries.
The Effective TypeScript file was a genuine PDF (calibre-produced, 570 pages, real
text layer). Its hazard showed up not in code indentation but in **section
alignment**: each Item's "Things to Remember" summary sits on the same physical page
as the *next* Item's heading, so slicing by page number attributes every summary to
the following Item. Three successive attempts to fix this by heading-matching failed
because the front table of contents, the back index, and in-body cross-references
all contain the literal string `Item N: Title`. Abandoned in favour of classifying
from the correctly-sliced item bodies. Worth knowing before trusting any
page-sliced extraction from a PDF.