Chapter 20

20. Effective Python (3rd ed.) — crosswalk and gap closure

All 125 Effective Python items mapped to this guide, with executed gap-fillers.

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

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

VerdictItemsShareWhat it means for you
covered4536%The guide already makes this point at comparable or greater depth. Skip.
partial4838%The guide states the rule but misses the why, the failure mode, or the framing. Read the Item.
gap1814%Genuinely absent from the guide. Closed in §5.
diverges11%The book and the guide disagree. Settled in §4.
skip1310%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

#ItemVerdictWhere it lives in the guidePri
1Know Which Version of Python You’re UsingskipL
2Follow the PEP 8 Style GuideskipL
3Never Expect Python to Detect Errors at Compile Timepartial04-python-core.md § The object modelM
4Write Helper Functions Instead of Complex ExpressionsskipL
5Prefer Multiple-Assignment Unpacking over Indexingpartial13-problem-sets.md; 15-study-plan-and-flashcards.mdM
6Always Surround Single-Element Tuples with ParenthesesgapM
7Consider Conditional Expressions for Simple Inline Logicpartial14-cheatsheets.md § TypeScript and JavaScript quick referenceL
8Prevent Repetition with Assignment Expressionspartial12-design-patterns-python.mdM
9Consider match for Destructuring in Flow Controlpartial12-design-patterns-python.md § match state machines; 11-design-patterns-typescript.mdH
10Know the Differences Between bytes and strpartial04-python-core.md; 05-data-structures-typescript.mdM
11Prefer Interpolated F-Strings over C-Style Format Stringspartial14-cheatsheets.mdL
12Understand the Difference Between repr and str when Printing Objectspartial04-python-core.md § Data model and dundersM
13Prefer Explicit String Concatenation over ImplicitgapM
14Know How to Slice Sequencespartial06-data-structures-python.mdM
15Avoid Striding and Slicing in a Single Expressionpartial06-data-structures-python.mdM
16Prefer Catch-All Unpacking over Slicingpartial15-study-plan-and-flashcards.mdM
17Prefer enumerate over rangecovered08-algorithm-patterns.mdL
18Use zip to Process Iterators in Parallelpartial14-cheatsheets.mdM
19Avoid else Blocks After for and while LoopsgapM
20Never Use for Loop Variables After the Loop EndsgapM
21Be Defensive when Iterating over Argumentscovered12-design-patterns-python.md (iter(g) is g, single-pass exhaustion); 04-python-core.md § Generators and coroutinesH
22Never Modify Containers While Iterating over Thempartial06-data-structures-python.mdH
23Pass Iterators to any and all for Efficient Short-Circuitingpartial02-javascript-core.md; 12-design-patterns-python.mdM
24Consider itertools for Working with Iterators and Generatorscovered06-data-structures-python.md; 12-design-patterns-python.md; 04-python-core.mdM
25Be Cautious when Relying on Dictionary Insertion Orderingcovered06-data-structures-python.md § dict compact layout; 04-python-core.mdM
26Prefer get over in and KeyError to Handle Missing Dictionary Keyscovered06-data-structures-python.mdM
27Prefer defaultdict over setdefault for Internal Statecovered06-data-structures-python.mdM
28Know How to Construct Key-Dependent Default Values with missinggap04-python-core.md (one passing mention)M
29Compose Classes Instead of Deeply Nesting Dictionaries, Lists, and Tuplescovered12-design-patterns-python.md; 06-data-structures-python.md § dataclass comparison tableM
30Know That Function Arguments Can Be Mutatedpartial04-python-core.md § The object model (rebinding vs mutation)H
31Return Dedicated Result Objects Instead of Unpacking More Than Three Variablescovered12-design-patterns-python.md § Result/Either; 06-data-structures-python.md § dataclass comparisonM
32Prefer Raising Exceptions to Returning Nonecovered12-design-patterns-python.md § Result/Either; 14-cheatsheets.md § trap listM
33Know How Closures Interact with Variable Scope and nonlocalpartial04-python-core.md (late-binding closures, the i=i fix)H
34Reduce Visual Noise with Variable Positional Argumentspartial12-design-patterns-python.mdM
35Provide Optional Behavior with Keyword ArgumentsskipL
36Use None and Docstrings to Specify Dynamic Default Argumentscovered04-python-core.md § Trap 1 - mutable default arguments (plus a Q&A drill); 12-design-patterns-python.mdH
37Enforce Clarity with Keyword-Only and Positional-Only Argumentspartial12-design-patterns-python.md; 14-cheatsheets.mdM
38Define Function Decorators with functools.wrapscovered12-design-patterns-python.md § Decorators (four shapes, bottom-up stacking); 04-python-core.mdH
39Prefer functools.partial over lambda Expressions for Glue Functionscovered12-design-patterns-python.mdM
40Use Comprehensions Instead of map and filterpartial06-data-structures-python.md; 08-algorithm-patterns.mdM
41Avoid More Than Two Control Subexpressions in ComprehensionsskipL
42Reduce Repetition in Comprehensions with Assignment ExpressionsgapM
43Consider Generators Instead of Returning Listscovered04-python-core.md § Generators and coroutines; 12-design-patterns-python.mdH
44Consider Generator Expressions for Large List Comprehensionscovered04-python-core.md; 06-data-structures-python.mdM
45Compose Multiple Generators with yield fromcovered04-python-core.md; 12-design-patterns-python.mdM
46Pass Iterators into Generators as Arguments Instead of Calling sendpartial04-python-core.md § Generators and coroutines; 12-design-patterns-python.mdM
47Manage Iterative State Transitions with a Class Instead of throwpartial12-design-patterns-python.md § match state machinesM
48Accept Functions Instead of Classes for Simple Interfacescovered12-design-patterns-python.md (__call__, strategy)M
49Prefer Object-Oriented Polymorphism over Functions with isinstance Checkscovered12-design-patterns-python.md § Protocol vs ABCM
50Consider functools.singledispatch for Functional-Style Programmingcovered12-design-patterns-python.md (18 mentions, incl. visitor discussion)M
51Prefer dataclasses for Defining Lightweight Classescovered06-data-structures-python.md § tuple/namedtuple/NamedTuple/dataclass comparison table; 12-design-patterns-python.mdH
52Use @classmethod Polymorphism to Construct Objects Genericallycovered12-design-patterns-python.md § Factory; 04-python-core.mdM
53Initialize Parent Classes with supercovered04-python-core.md § C3 MRO and cooperative super(); 12-design-patterns-python.mdH
54Consider Composing Functionality with Mix-in Classescovered12-design-patterns-python.md; 11-design-patterns-typescript.mdM
55Prefer Public Attributes over Private OnesgapM
56Prefer dataclasses for Creating Immutable Objectscovered04-python-core.md (frozen=True gives __hash__); 06-data-structures-python.md § hashability table; 12-design-patterns-python.md § structural sharingM
57Inherit from collections.abc Classes for Custom Container Typescovered12-design-patterns-python.md; 04-python-core.mdM
58Use Plain Attributes Instead of Setter and Getter Methodspartial04-python-core.md § Descriptors; 12-design-patterns-python.mdM
59Consider @property Instead of Refactoring Attributespartial04-python-core.md § DescriptorsM
60Use Descriptors for Reusable @property Methodscovered04-python-core.md § Descriptors and attribute lookup order; 12-design-patterns-python.md § DescriptorsH
61Use getattr, getattribute, and setattr for Lazy Attributescovered04-python-core.md § Attribute lookup order; 12-design-patterns-python.md § ProxyH
62Validate Subclasses with init_subclasscovered12-design-patterns-python.md § init_subclass over metaclassesH
63Register Class Existence with init_subclasscovered12-design-patterns-python.md § RegistryM
64Annotate Class Attributes with set_namepartial12-design-patterns-python.md § DescriptorsM
65Consider Class Body Definition Order to Establish Relationships Between AttributesgapM
66Prefer Class Decorators over Metaclasses for Composable Class Extensionscovered12-design-patterns-python.md § Class decorators; § init_subclass over metaclassesM
67Use subprocess to Manage Child Processesgap04-python-core.md (one passing mention)M
68Use Threads for Blocking I/O; Avoid for Parallelismcovered04-python-core.md § The GIL, measured; 15-study-plan-and-flashcards.mdH
69Use Lock to Prevent Data Races in Threadspartial11-design-patterns-typescript.md; 12-design-patterns-python.mdH
70Use Queue to Coordinate Work Between Threadspartial04-python-core.md; 12-design-patterns-python.md § Producer-consumerM
71Know How to Recognize When Concurrency Is Necessarypartial12-design-patterns-python.mdM
72Avoid Creating New Thread Instances for On-Demand Fan-outpartial04-python-core.md; 12-design-patterns-python.mdM
73Understand How Using Queue for Concurrency Requires Refactoringpartial12-design-patterns-python.mdL
74Consider ThreadPoolExecutor When Threads Are Necessarypartial04-python-core.mdM
75Achieve Highly Concurrent I/O with Coroutinescovered04-python-core.md § Generators and coroutines; 12-design-patterns-python.md (21 mentions)H
76Know How to Port Threaded I/O to asynciocovered04-python-core.md; 12-design-patterns-python.mdM
77Mix Threads and Coroutines to Ease the Transition to asynciogapM
78Maximize Responsiveness of asyncio Event Loops with async-Friendly Worker Threadspartial04-python-core.md; 02-javascript-core.md § The event loopH
79Consider concurrent.futures for True Parallelismcovered04-python-core.md § The GIL; 12-design-patterns-python.mdM
80Take Advantage of Each Block in try/except/else/finallypartial04-python-core.md (one mention)M
81assert Internal Assumptions and raise Missed Expectationspartial04-python-core.md; 18-testing-strategy.mdM
82Consider contextlib and with Statements for Reusable try/finally Behaviorcovered12-design-patterns-python.md § Context managers (__exit__ truthy suppresses); 04-python-core.mdH
83Always Make try Blocks as Short as Possiblepartial04-python-core.mdM
84Beware of Exception Variables DisappearinggapH
85Beware of Catching the Exception Classpartial12-design-patterns-python.mdM
86Understand the Difference Between Exception and BaseExceptionpartial04-python-core.mdH
87Use traceback for Enhanced Exception ReportinggapM
88Consider Explicitly Chaining Exceptions to Clarify Tracebackspartial04-python-core.md (raise ... from); 12-design-patterns-python.mdM
89Always Pass Resources into Generators and Have Callers Clean Them Up Outsidepartial04-python-core.md (GeneratorExit)H
90Never Set debug to FalsegapM
91Avoid exec and eval Unless You’re Building a Developer Toolpartial16-testing-node-test.md (JS side only)M
92Profile Before Optimizingpartial04-python-core.mdH
93Optimize Performance-Critical Code Using timeit Microbenchmarkspartial04-python-core.md; 01-complexity-and-big-o.md § The measured trapsH
94Know When and How to Replace Python with Another Programming LanguageskipL
95Consider ctypes to Rapidly Integrate with Native Librariesskip04-python-core.md (one mention)L
96Consider Extension Modules to Maximize Performance and ErgonomicsskipL
97Rely on Precompiled Bytecode and File System Caching to Improve Startup Timegap04-python-core.md § Bytecode (disassembly only)M
98Lazy-Load Modules with Dynamic Imports to Reduce Startup TimegapM
99Consider memoryview and bytearray for Zero-Copy Interactions with bytescovered06-data-structures-python.md § memoryview/bytearray; 04-python-core.mdM
100Sort by Complex Criteria Using the key Parametercovered07-sorting-and-searching.md; 06-data-structures-python.md; 12-design-patterns-python.mdH
101Know the Difference Between sort and sortedcovered07-sorting-and-searching.md; 14-cheatsheets.mdM
102Consider Searching Sorted Sequences with bisectcovered06-data-structures-python.md § bisect/SortedList; 07-sorting-and-searching.md; 01-complexity-and-big-o.mdH
103Prefer deque for Producer-Consumer Queuescovered06-data-structures-python.md § deque; 05-data-structures-typescript.md § ring buffer; 01-complexity-and-big-o.mdH
104Know How to Use heapq for Priority Queuescovered06-data-structures-python.md § binary heap (O(n) heapify, indexed decrease-key); 09-graphs-and-trees.md § DijkstraH
105Use datetime Instead of time for Local Clocksgap12-design-patterns-python.md (injected-clock pattern only)M
106Use decimal when Precision Is Paramountpartial02-javascript-core.md § IEEE-754; 12-design-patterns-python.mdH
107Make pickle Serialization Maintainable with copyregpartial12-design-patterns-python.md (pickle in serialization discussion)M
108Verify Related Behaviors in TestCase Subclassescovered17-testing-python-unittest.md § TestCase, all 41 assertions, subTest; 19-testing-cheatsheet.mdH
109Prefer Integration Tests over Unit Testsdiverges18-testing-strategy.md § pyramid/trophy/honeycomb; § functional core, imperative shellH
110Isolate Tests from Each Other with setUp, tearDown, setUpModule, tearDownModulecovered17-testing-python-unittest.md § fixtures, addCleanup/enterContextH
111Use Mocks to Test Code with Complex Dependenciescovered17-testing-python-unittest.md § unittest.mock (Mock/MagicMock/AsyncMock, 7 patch forms, autospec); 19-testing-cheatsheet.mdH
112Encapsulate Dependencies to Facilitate Mocking and Testingcovered18-testing-strategy.md § seams (parameterize/wrap/sprout/extract-pure-core); 17-testing-python-unittest.mdH
113Use assertAlmostEqual to Control Precision in Floating Point Testspartial17-testing-python-unittest.md (in the assertion table); 18-testing-strategy.md § flakiness taxonomyH
114Consider Interactive Debugging with pdbgapM
115Use tracemalloc to Understand Memory Usage and Leakscovered04-python-core.md § refcounting and generational GCM
116Know Where to Find Community-Built ModulesskipL
117Use Virtual Environments for Isolated and Reproducible DependenciesskipL
118Write Docstrings for Every Function, Class, and Moduleskip12-design-patterns-python.mdL
119Use Packages to Organize Modules and Provide Stable APIspartial12-design-patterns-python.md; 17-testing-python-unittest.md (__init__.py in the test package)M
120Consider Module-Scoped Code to Configure Deployment EnvironmentsskipL
121Define a Root Exception to Insulate Callers from APIsgapH
122Know How to Break Circular Dependenciespartial02-javascript-core.md § CJS vs ESM live bindings; 12-design-patterns-python.mdM
123Consider warnings to Refactor and Migrate Usagepartial17-testing-python-unittest.md (assertWarns in the assertion table)M
124Consider Static Analysis via typing to Obviate Bugspartial04-python-core.md; 12-design-patterns-python.md § Protocol vs ABC; 03-typescript-type-system.mdH
125Prefer Open Source Projects for Bundling over zipimport and zipappskipL

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_order tested with the repository replaced by a Mock and 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 .pyc against 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 was line_total(0, 100) quietly failing to raise. The fix is -B plus PYTHONDONTWRITEBYTECODE=1 plus clearing __pycache__ between runs, and a finally that 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.

ItemWhat to know
67 subprocessA 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 ↔ asyncioloop.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 tracebackGives 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 importspython -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 + zoneinfoKeep 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 pdbbreakpoint() 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 BaseException and GeneratorExit in 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.67

    Two 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 Protocol versus 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:

  1. 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.
  2. The high-priority partials (§6): Items 9, 22, 30, 33, 69, 78, 92, 93, 106, 113, 124.
  3. 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.
  4. Skim the covered 45 only if a topic feels rusty; the guide reference in the matrix will be at least as deep.
  5. 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.