diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..399843b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# `src/asset_manifest.py` is generated by `scripts/fingerprint_assets.py`. +# On merge/rebase, keep our side of the conflict — the post-merge and +# post-rewrite hooks regenerate the file deterministically afterwards. +# This works once `scripts/install-git-hooks.sh` has been run locally, +# which registers `merge.ours.driver = true` and points `core.hooksPath` +# at `.githooks/`. +src/asset_manifest.py merge=ours diff --git a/.githooks/post-merge b/.githooks/post-merge new file mode 100755 index 0000000..0dd8606 --- /dev/null +++ b/.githooks/post-merge @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Regenerate the asset manifest after a merge or pull so the digest +# reflects the merged tree, not whichever parent won the conflict. +set -e +cd "$(git rev-parse --show-toplevel)" +uv run python scripts/fingerprint_assets.py >/dev/null +if ! git diff --quiet src/asset_manifest.py public/_headers; then + echo "post-merge: asset manifest regenerated; stage and amend if needed" +fi diff --git a/.githooks/post-rewrite b/.githooks/post-rewrite new file mode 100755 index 0000000..04c37d4 --- /dev/null +++ b/.githooks/post-rewrite @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Regenerate the asset manifest after rebase/amend so the digest matches +# the rewritten history, not whichever commit happened to win each step. +set -e +cd "$(git rev-parse --show-toplevel)" +uv run python scripts/fingerprint_assets.py >/dev/null +if ! git diff --quiet src/asset_manifest.py public/_headers; then + echo "post-rewrite: asset manifest regenerated; stage and amend if needed" +fi diff --git a/.github/workflows/preview-viz.yml b/.github/workflows/preview-viz.yml new file mode 100644 index 0000000..9272e7f --- /dev/null +++ b/.github/workflows/preview-viz.yml @@ -0,0 +1,74 @@ +name: Preview viz + +on: + push: + branches: + - claude/tuftean-marginalia-viz-TB0fw + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: preview-viz + cancel-in-progress: true + +jobs: + upload-preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: false + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Install dependencies + run: uv sync --all-groups + - name: Build generated assets + run: make build + - name: Verify Cloudflare auth + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: npx --yes wrangler whoami + - name: Sync Python Workers vendor + run: uv run pywrangler sync + - name: Upload Cloudflare Preview + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: | + set -x + uv run pywrangler preview \ + --name viz \ + --message "${{ github.sha }}" \ + --json + - name: Smoke test deployed Preview + run: | + set -euo pipefail + base="https://viz-pythonbyexample.adewale-883.workers.dev" + for path in \ + "/" \ + "/examples/values" \ + "/prototyping/journey-figures-gestalt"; do + url="${base}${path}" + echo "Checking ${url}" + curl --fail --show-error --silent --location --output /tmp/preview-smoke.html --write-out "%{http_code} %{url_effective}\n" "${url}" + if grep -qiE "error code: 1101|PythonError|Traceback" /tmp/preview-smoke.html; then + echo "Preview rendered an exception for ${url}" + head -200 /tmp/preview-smoke.html + exit 1 + fi + done + - name: Dump wrangler logs on failure + if: failure() + run: | + find ~ /tmp /root -name "*.log" -path "*wrangler*" 2>/dev/null | while read f; do + echo "=== $f ===" + tail -300 "$f" || true + done diff --git a/README.md b/README.md index 02529b1..09f2e9b 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,12 @@ Install dependencies with `uv`, then run: python3 -m unittest discover -s tests -v ``` +After cloning, install the local git hooks once so merges and rebases regenerate `src/asset_manifest.py` instead of producing conflicts: + +```bash +./scripts/install-git-hooks.sh +``` + Run locally on Workers: ```bash diff --git a/docs/example-figure-rubric.md b/docs/example-figure-rubric.md new file mode 100644 index 0000000..c93692a --- /dev/null +++ b/docs/example-figure-rubric.md @@ -0,0 +1,209 @@ +# Example figure rubric + +Parallel to `docs/journey-visualisation-rubric.md`, but for the figures +that attach to **example pages** (literate-program lessons), not journey +sections. The journey rubric scores the figure beside a section heading; +this one scores the figure that sits between prose and code inside a +single cell of an example walkthrough. + +The two rubrics share craft criteria (palette, primitives, emphasis +scarcity) and diverge on content criteria, because the audience and +task differ. A journey-section figure depicts the *conceptual shift* +unifying multiple lessons; an example figure depicts the *single move* +the surrounding cell discusses. + +Score each example figure on a 10-point scale. Version 2 of this +rubric, applied 2026-05; see `docs/rubric-saturation.md` for the +reasoning that produced these upgrades. The previous criterion 2 +("match the running variables") and criterion 5 ("caption asserts") +have been replaced; a new page-level coherence rubric joins the +per-figure scoring. + +## Content (5.5) + +1. **Cell fidelity (0-1.5)** — the figure depicts the move the cell's + prose discusses, not the example's title. If the example is + "Mutability" but cell 1 is about immutable strings, a figure on + cell 1 must depict immutability, not aliasing. Wrong cell, wrong + figure. +2. **The figure earns its place (0-1.0)** — the figure surfaces + something the prose cannot show in the same word count: a + relationship, a before/after, a hidden mechanism, an invariant. + A figure that merely restates the prose in diagram form earns + 0.5; a figure that adds nothing the prose hasn't already said + earns 0. Generic placeholders (`a`, `b`, `xs`) are fine; what + matters is whether the figure carries pedagogical weight beyond + the prose. (Replaces v1's "match the running variables", which + punished honest reuse of library figures across multiple cells.) +3. **One conceptual move (0-1.0)** — exactly one shift, before-state + to after-state, or one mechanism. Squint test: a reader should + identify the figure's single point in two seconds. +4. **Mechanism over metaphor (0-1.0)** — the figure shows the actual + machinery (the cell, the binding, the dispatch, the iterator), + not a cartoon of it. Knuth's rule. +5. **Caption quality (0-1.0)** — `figcaption` declares what is true, + in the section summary's voice; it does not narrate what the + figure does. "Two names share one mutable list — appending + through one name changes the object visible through both." + earns 1.0. "The figure shows two names pointing at one list." + earns 0 (narration, not assertion). Mixed-voice captions earn + 0.5. The SVG itself contains no prose duplicating the caption; + only diagrammatic labels (`stdout`, `iter()`, panel tags, type + signatures). See pipeline invariant 2 in the spec. + +## Craft (3.0) + +6. **Grammar conformance (0-1.0)** — composed exclusively from + `Canvas` primitives in `src/marginalia_grammar.py`. No bespoke + SVG, no new colours, no stroke weights outside the locked set. +7. **Emphasis scarcity (0-1.0)** — at most one accent mark per + figure. The accent goes on the single element the cell prose + names (the live mutation, the captured cell, the dispatch arrow). + Three accent marks competing for attention is no emphasis at all. +8. **Restraint (0-1.0)** — no decoration that does not carry + information. No drop shadows, gradients, ornamental rules, + non-orthogonal tilts, or marks placed for "balance". + +## Context (1.5) + +9. **Banner-row fit (0-1.0)** — the figure's intrinsic width sits + comfortably inside `.cell-banner`'s auto-fit grid. Intrinsic widths + beyond ~360 px clamp to the column without growing past it; much + narrower viewBoxes leave whitespace either side of the centred + figure. Aim for an intrinsic viewBox between 200 and 360 px wide. +10. **Pairs with the surrounding cell (0-0.5)** — the banner sits + AFTER the named cell, so the eye reads cell-prose → cell-code → + banner. The figure should summarise the move the surrounding + cell just made, not stand alone as a generic illustration of the + example title. + +## Topic gates (cell-shape specific) + +- **Binding cells** (assignments, `=`) — show the name-arrow with the + type tag and the resulting value. The canonical Python picture. +- **Mutation cells** — show before-state and after-state with the + same object identity, OR rebinding with a new identity. The + difference is the lesson. +- **Iteration cells** — show the iterator advance: a caret moving, + or `iter()`+`next()` producing values one at a time. +- **Function-definition cells** — show the signature with parameter + separators (`/`, `*`) explicit when relevant, or the + caller→body→return shape. +- **Class cells** — show state and methods bundled, or the + instance→class→type triangle, or MRO chain. Pick one, not all. +- **Exception cells** — show the lanes (try/except/else/finally) + with a single traced path, or the exception-cause arrow (`__cause__` + vs `__context__`). +- **Async cells** — show two parallel lanes (loop · coroutine) with + await handoffs. + +## Release gates outside the score + +These are not scored; a figure that violates any of them does not +ship. The geometry, palette, font, stroke, emphasis, registration, +and caption gates are now enforced by automated contracts in +`tests/test_marginalia_geometry.py` (Contracts 1-9). CI fails before +the figure can merge. + +- **One figure per cell, at most.** Two figures on one cell signal + the cell is doing two things; split the cell instead. +- **figcaption present and declarative.** Captions in the form + "Two names share one mutable list — appending through one name + changes the object visible through both." Not "this shows X" or + "see how Y". +- **figcaption agrees with the cell's prose.** The cell's prose + paragraph in the markdown and the figure's figcaption assert the + same thing in different words. If they disagree, one is wrong. +- **figcaption is unique across slugs.** A reused figure can serve + multiple lessons (`iter-protocol` attaches to four), but each + lesson must frame the figure in its own voice. Verbatim caption + reuse copies the lesson voice the same way verbatim code reuse + copies the example. *Contract 5b — FigureCaptionContract.* +- **No clipping.** Every ``, ``, ``, ``, + `` lives inside the padded viewBox. Text width counts: a + long mono string in a too-narrow box clips even if the geometry + looks right at first glance. *Contract 1.* +- **No element collision.** Text that overlaps a rect must be + fully contained by that rect. A type tag sitting on top of the + box above it (the `/examples/values` STR-LIST-DICT bug) is the + canonical violation. *Contract 2.* +- **No text-text overlap.** Two text elements may not occupy + overlapping bounding boxes (the `itertools-chain` "ITER A" / + "1 · 2" collision in a too-narrow box). *Contract 3.* +- **Palette discipline.** Only `INK`, `INK_SOFT`, `EMPHASIS`, + `SOFT_FILL`, or `"none"` may appear as fill or stroke. *Contract + 5a — FigureGrammarContract.* +- **Font discipline.** Only `FONT_SERIF`, `FONT_MONO`, `FONT_SANS` + may appear as `font-family`. *Contract 5b.* +- **Stroke-weight discipline.** Only `W_HAIRLINE`, `W_STROKE`, + `W_EMPHASIS`, `W_GHOST`. *Contract 5c.* +- **Emphasis scarcity, enforced.** At most ONE accent mark + (`EMPHASIS`-coloured arrowhead, caret, dot, or rect stroke) per + figure. Was a soft v1 criterion; now hard. *Contract 9.* +- **Banner-fit, enforced.** Every figure's intrinsic width + (Canvas.w + 2 · PAD_X) must fit `.cell-banner--1`'s 440px max + ceiling. *Contract 8.* +- **Twin consistency.** When two figures depict parallel concepts + (`kw-only-separator` ↔ `positional-only-separator`, + `class-triangle` ↔ `metaclass-triangle`), their metrics must + match coordinate-for-coordinate where the concepts coincide. A + fix to one is a fix to both, in the same commit. +- **Geometric termination.** Lines that connect to dots, circles, + or rects must terminate AT the element's edge — not 1-2px short + (looks disconnected) and not inside the glyph (looks broken). + When in doubt, end the line at the centre and let the dot draw + on top. +- **Mono character alignment.** When a vertical divider marks a + position in mono text, its x must match the character's actual + centre. JetBrains Mono advances ~6px per char at fs=10. A + visually-similar `82` and `75` are not interchangeable. +- **Pipeline invariants** (see spec) hold: SVG renders at intrinsic + size; SVG contains no prose duplicating the caption. +- **Gestalt = production.** Review pages under `/prototyping/*` + must render the same paint code as the production attachments. + Parallel `e_*` paint functions for "gestalt versions" drift from + production and hide bugs; we eliminated 76 of them in May 2026. + +## Page-level coherence (per slug, multi-figure) + +A separate 0-1.0 score applied to slugs whose `ATTACHMENTS[slug]` +list contains more than one figure. Multi-figure pages must form a +coherent set, not three angles on the same point. + +- **1.0** — figures show distinct aspects of the lesson in a + natural reading order (intro picture, mid-walkthrough mechanism, + summary). Each banner earns its placement. +- **0.5** — figures are individually fine but redundant; one would + do the work of two. The page reads as cluttered. +- **0** — figures contradict each other, or one figure is on the + wrong cell, or the page has three figures where one would teach + better. + +For single-figure slugs (today, all 109 of them), page coherence is +trivially 1.0 and does not enter the per-figure score. As multi- +figure attachments grow this criterion will become the discriminator +that prevents the "more figures is better" failure mode. + +## Quality bands + +- **9.0-10.0** — depicts the cell's move in two seconds; the figcaption + could only describe this figure; reads pleasantly on return visits. +- **8.0-8.9** — depicts the right move but uses generic placeholders + where specific names would land harder, or the caption hedges, or + one secondary mark steals attention from the primary one. +- **7.0-7.9** — depicts the cell but loses something in scope: shows + the example title rather than the specific cell's move; or topic + gate not satisfied. +- **below 7.0** — wrong cell, wrong shape, multiple primary ideas + competing, or accent marks scattered rather than scarce. Redesign + before promoting. + +## Project gate + +A cell figure may ship to production once it scores **≥ 8.5**. The +example's figure average should exceed **8.7** so a multi-figure +example reads as a coherent set rather than independently authored +diagrams. + +The score is a guide, not a substitute for reading the cell beside +its surrounding prose. diff --git a/docs/example-graph-score-impact.md b/docs/example-graph-score-impact.md deleted file mode 100644 index 20d18a5..0000000 --- a/docs/example-graph-score-impact.md +++ /dev/null @@ -1,32 +0,0 @@ -# See also graph score impact - -The `see_also` implementation adds subtle graph behavior on top of the linear previous/next tour: - -- edge labels such as `contrast`, `prerequisite`, `alternative`, `builds on`, `shared mechanism`, and `next depth` -- validation with `scripts/audit_example_graph.py --check` -- lightweight recommendations on unknown `/examples/{slug}` pages -- a graph-aware score component in `scripts/score_examples.py` - -The score comparison below uses the same loaded catalog and the graph-aware scorer. “Before” subtracts the new graph component; “After” includes it. - -| Cohort | Count | Before avg | After avg | Delta | -|---|---:|---:|---:|---:| -| All examples | 69 | 9.37 | 9.44 | +0.07 | -| Examples with `see_also` | 17 | 9.31 | 9.61 | +0.30 | - -Lowest linked examples before/after: - -| Example | Before | After | Delta | -|---|---:|---:|---:| -| `import-aliases` | 8.56 | 8.86 | +0.30 | -| `metaclasses` | 8.89 | 9.19 | +0.30 | -| `async-iteration-and-context` | 9.08 | 9.38 | +0.30 | -| `assignment-expressions` | 9.34 | 9.64 | +0.30 | -| `break-and-continue` | 9.34 | 9.64 | +0.30 | -| `loop-else` | 9.34 | 9.64 | +0.30 | -| `positional-only-parameters` | 9.34 | 9.64 | +0.30 | -| `inheritance-and-super` | 9.34 | 9.64 | +0.30 | -| `exception-chaining` | 9.34 | 9.64 | +0.30 | -| `exception-groups` | 9.34 | 9.64 | +0.30 | - -Interpretation: the graph does not make weak prose strong by itself. It gives a modest rubric gain where links clarify alternatives, prerequisites, or next-depth concepts. The main catalog average moves only slightly because most examples intentionally remain unlinked unless there is a meaningful conceptual edge. diff --git a/docs/example-quality-new-vs-old.csv b/docs/example-quality-new-vs-old.csv deleted file mode 100644 index 9e323c6..0000000 --- a/docs/example-quality-new-vs-old.csv +++ /dev/null @@ -1,70 +0,0 @@ -cohort,slug,score -Existing examples,hello-world,9.12 -Existing examples,values,9.44 -Existing examples,numbers,9.42 -Existing examples,booleans,9.32 -New syntax/graph examples,operators-and-literals,9.44 -Existing examples,none,9.70 -Existing examples,variables,9.44 -Existing examples,constants,9.32 -Existing examples,truthiness,9.70 -Existing examples,equality-and-identity,9.56 -Existing examples,mutability,9.44 -Existing examples,strings,9.68 -Existing examples,string-formatting,9.44 -Existing examples,conditionals,9.54 -New syntax/graph examples,assignment-expressions,9.34 -Existing examples,for-loops,9.46 -New syntax/graph examples,break-and-continue,9.24 -New syntax/graph examples,loop-else,9.34 -Existing examples,iterating-over-iterables,9.70 -Existing examples,iterators,9.56 -Existing examples,match-statements,8.84 -New syntax/graph examples,advanced-match-patterns,9.44 -Existing examples,while-loops,9.46 -Existing examples,lists,9.56 -Existing examples,tuples,9.56 -Existing examples,unpacking,9.66 -Existing examples,dicts,9.56 -Existing examples,sets,9.68 -Existing examples,slices,9.46 -Existing examples,comprehensions,9.68 -New syntax/graph examples,comprehension-patterns,9.46 -Existing examples,sorting,9.68 -Existing examples,functions,9.44 -Existing examples,keyword-only-arguments,9.32 -New syntax/graph examples,positional-only-parameters,9.34 -Existing examples,args-and-kwargs,9.56 -Existing examples,multiple-return-values,9.46 -Existing examples,closures,9.22 -New syntax/graph examples,scope-global-nonlocal,9.46 -Existing examples,recursion,8.65 -Existing examples,lambdas,9.56 -Existing examples,generators,9.46 -New syntax/graph examples,yield-from,9.46 -Existing examples,generator-expressions,9.44 -Existing examples,itertools,9.68 -Existing examples,decorators,9.34 -Existing examples,classes,8.44 -New syntax/graph examples,inheritance-and-super,9.34 -Existing examples,dataclasses,9.44 -Existing examples,properties,9.56 -Existing examples,special-methods,8.52 -New syntax/graph examples,metaclasses,8.89 -Existing examples,context-managers,9.58 -New syntax/graph examples,delete-statements,9.68 -Existing examples,exceptions,8.56 -New syntax/graph examples,assertions,9.46 -New syntax/graph examples,exception-chaining,9.34 -New syntax/graph examples,exception-groups,9.24 -Existing examples,modules,8.84 -New syntax/graph examples,import-aliases,8.56 -Existing examples,type-hints,8.77 -Existing examples,enums,9.46 -Existing examples,regular-expressions,9.66 -Existing examples,number-parsing,9.22 -Existing examples,custom-exceptions,9.32 -Existing examples,json,9.34 -Existing examples,datetime,9.54 -Existing examples,async-await,8.42 -New syntax/graph examples,async-iteration-and-context,8.84 diff --git a/docs/example-quality-new-vs-old.md b/docs/example-quality-new-vs-old.md deleted file mode 100644 index 74c4483..0000000 --- a/docs/example-quality-new-vs-old.md +++ /dev/null @@ -1,23 +0,0 @@ -# Example quality: new vs existing examples - -![Dot plot comparing quality scores for existing and newly added examples](example-quality-new-vs-old.svg) - -Scores come from `scripts/score_examples.py`, so this is a heuristic audit rather than a human rubric review. The “new” cohort is the syntax-surface/graph expansion added after commit `f6019d5`. - -| Cohort | Count | Average | Median | Min | Max | -|---|---:|---:|---:|---:|---:| -| Existing examples | 52 | 9.36 | 9.46 | 8.42 | 9.70 | -| New syntax/graph examples | 17 | 9.29 | 9.34 | 8.56 | 9.68 | - -Lowest-scoring new examples under the heuristic: - -- `import-aliases` — 8.56 -- `async-iteration-and-context` — 8.84 -- `metaclasses` — 8.89 -- `break-and-continue` — 9.24 -- `exception-groups` — 9.24 -- `assignment-expressions` — 9.34 -- `loop-else` — 9.34 -- `positional-only-parameters` — 9.34 - -The new examples are slightly lower on average because they cover compact, advanced, or edge-case syntax. That suggests the next editorial pass should deepen the lowest-scoring new pages with stronger problem framing and contrast, not remove them. diff --git a/docs/example-quality-new-vs-old.svg b/docs/example-quality-new-vs-old.svg deleted file mode 100644 index 80db45b..0000000 --- a/docs/example-quality-new-vs-old.svg +++ /dev/null @@ -1,100 +0,0 @@ - - - -Example quality: new syntax coverage vs existing catalog -Heuristic score from scripts/score_examples.py; higher is better. Dots are individual examples. - -8.5 - -9.0 - -9.5 - - -8.5 gate -Existing examples -n=52 avg=9.36 median=9.46 min=8.42 - - -avg 9.36 -New syntax/graph examples -n=17 avg=9.29 median=9.34 min=8.56 - - -avg 9.29 -async-await 8.42 -classes 8.44 -special-methods 8.52 -exceptions 8.56 -recursion 8.65 -type-hints 8.77 -match-statements 8.84 -modules 8.84 -hello-world 9.12 -closures 9.22 -number-parsing 9.22 -booleans 9.32 -constants 9.32 -custom-exceptions 9.32 -keyword-only-arguments 9.32 -decorators 9.34 -json 9.34 -numbers 9.42 -dataclasses 9.44 -functions 9.44 -generator-expressions 9.44 -mutability 9.44 -string-formatting 9.44 -values 9.44 -variables 9.44 -enums 9.46 -for-loops 9.46 -generators 9.46 -multiple-return-values 9.46 -slices 9.46 -while-loops 9.46 -conditionals 9.54 -datetime 9.54 -args-and-kwargs 9.56 -dicts 9.56 -equality-and-identity 9.56 -iterators 9.56 -lambdas 9.56 -lists 9.56 -properties 9.56 -tuples 9.56 -context-managers 9.58 -regular-expressions 9.66 -unpacking 9.66 -comprehensions 9.68 -itertools 9.68 -sets 9.68 -sorting 9.68 -strings 9.68 -iterating-over-iterables 9.70 -none 9.70 -truthiness 9.70 -import-aliases 8.56 -async-iteration-and-context 8.84 -metaclasses 8.89 -break-and-continue 9.24 -exception-groups 9.24 -assignment-expressions 9.34 -exception-chaining 9.34 -inheritance-and-super 9.34 -loop-else 9.34 -positional-only-parameters 9.34 -advanced-match-patterns 9.44 -operators-and-literals 9.44 -assertions 9.46 -comprehension-patterns 9.46 -scope-global-nonlocal 9.46 -yield-from 9.46 -delete-statements 9.68 -import-aliases 8.56 -async-iteration-and-context 8.84 -metaclasses 8.89 -break-and-continue 9.24 -Score -Takeaway: new examples are slightly lower on average because many cover advanced/edge syntax compactly; none fall below the 8.5 gate under this heuristic. - \ No newline at end of file diff --git a/docs/journey-visualisation-rubric.md b/docs/journey-visualisation-rubric.md new file mode 100644 index 0000000..df42a06 --- /dev/null +++ b/docs/journey-visualisation-rubric.md @@ -0,0 +1,121 @@ +# Journey visualisation rubric + +This rubric scores the figure beside each journey section heading. +The example rubric (docs/example-quality-rubric.md) covers individual +lesson pages; this one covers the conceptual figures that introduce +each journey section. + +A journey section sits *above* individual lessons. It groups three to +five examples under a shared conceptual shift, e.g. "Recognise iteration +as a protocol" or "Bundle behavior with state". The figure beside that +heading should depict the shift the section asks the reader to make. +It is not a recycled lesson figure. + +Score each section figure on a 10-point scale. + +## Content (5.5) + +1. **Section fidelity (0-1.5)** — the figure depicts the conceptual + shift the section title and summary describe. It does not depict + one of the section's examples. A figure for "Make decisions + explicitly" must show *deciding*, not the body of any particular + `match` statement; a figure for "Bundle behavior with state" must + show *bundling*, not one specific class. +2. **Pedagogical scope (0-1.0)** — the figure captures the general + pattern that unifies the section's items. If the figure could be + replaced with the diagram from any single lesson it is too specific. +3. **One conceptual move (0-1.0)** — exactly one shift, before-state + to after-state, or the depiction of a single mechanism. Two ideas + compete for the reader's eye and both lose. Squint test: the + primary structure is identifiable within two seconds. +4. **Mechanism over metaphor (0-1.0)** — the figure shows the actual + machinery the section names — the iterator object, the cell, the + dispatch arrow — not a cartoon of it. Knuth's rule. +5. **Caption alignment (0-1.0)** — the `figcaption` names the + conceptual shift in plain language and matches the section + summary's voice. The caption is part of the figure, not optional. + +## Craft (3.0) + +6. **Grammar conformance (0-1.0)** — composed exclusively from + `Canvas` primitives in `src/marginalia_grammar.py`. No bespoke + SVG, no new colours, no stroke weights outside the locked set. +7. **Emphasis scarcity (0-1.0)** — at most one accent mark per + figure. The accent goes on the single element the section names + (the live yield, the dispatch arrow, the captured cell). If three + things are orange the figure has no emphasis at all. +8. **Restraint (0-1.0)** — no decoration that does not carry + information. No drop shadows, gradients, ornamental rules, + non-orthogonal tilts, or marks placed for "balance". + +## Context (1.5) + +9. **Independence from lesson figures (0-1.0)** — distinct framing + from any single lesson's diagram. If the section figure is + identical to the banner figure in one of the section's lessons, + one of them is wrong. Usually the section figure should be the + *more abstract* one. +10. **Layout fit (0-0.5)** — renders comfortably at the journey + page's ~280-320px section-figure column. Text inside the SVG + stays readable at that scale; the figure does not overflow. + +## Topic gates + +- **Decision sections** — depict the fork explicitly: a value flowing + through a predicate to one of several branches. A single linear + arrow does not satisfy this gate. +- **Loop sections** — show the back-edge that makes a loop a loop. + A linear sequence of cells without a return path is not a loop + picture, it is just a sequence. +- **Iteration sections** — show the `iter()` / `next()` protocol + explicitly: an iterable, an iterator, and one or more values + pulled out by `next()`. The figure must distinguish iterable + from iterator. +- **Type sections** — show annotations as ghost overlays on runtime + values, or show type relationships (union, generic, structural + matching) as containment / flow. Do not let a type figure devolve + into "a function with parameter names". +- **Resource and boundary sections** — show enter and exit as paired + events bracketing a body, with the failure path also routed + through exit. A one-way arrow is not a context manager. +- **Concurrency sections** — show two parallel lanes with handoffs + between them. A single timeline is not a concurrency picture. + +## Release gates outside the score + +- **Exactly one figure per section.** Section figures are not stacked. + If the section needs two figures the section is doing two things. +- **Caption present.** A figure without a `figcaption` is not allowed. +- **Section summary aligns with caption.** The summary in + `src/app.py`'s `JOURNEYS` list agrees with what the figure caption + asserts. Disagreement means one or the other is wrong. +- **Renders within `.journey-section`'s 2-column grid.** The figure + obeys the column the layout gives it (~280-320px); design at a + viewBox sized for that column, not at lesson-figure dimensions. +- **Uses only the four palette constants.** `INK`, `INK_SOFT`, + `EMPHASIS`, `SOFT_FILL`. Anything else is grounds for redesign. + +## Quality bands + +- **9.0-10.0** — captures the conceptual shift in two seconds; the + caption could only describe this figure; pleasant to look at on + return visits. +- **8.0-8.9** — depicts the right idea but shares too much framing + with a lesson figure, or the caption hedges instead of asserting, + or one secondary mark steals attention from the primary one. +- **7.0-7.9** — depicts the section but loses something in scope: + uses a specific predicate / iterable / type instead of the + general pattern; or topic gate not satisfied. +- **below 7.0** — recycled lesson figure, missing topic gate, + multiple primary ideas competing, or accent marks scattered + rather than scarce. Redesign before publishing. + +## Project gate + +Every section figure on a published journey page should score at +least **8.5**. The journey average across its three sections should +exceed **8.8** so the journey reads as a unified set rather than +three independently designed cards. + +The score is a guide, not a substitute for reading the page beside +its surrounding lessons. diff --git a/docs/lessons-learned.md b/docs/lessons-learned.md index bfb8721..89b60db 100644 --- a/docs/lessons-learned.md +++ b/docs/lessons-learned.md @@ -84,3 +84,31 @@ git diff --check - Keep `README.md` focused on how to understand, verify, and deploy the project. - Keep this lessons document updated when a bug reveals a general rule. - Record user-visible changes in `CHANGELOG.md` before significant commits or releases. + +## Visualisations and marginalia + +- **A diagram set needs a grammar, not a collection of one-off layouts.** Hand-drawn SVGs drift in stroke weight, cell size, type-tag placement, arrow style. A locked `Canvas` grammar (palette, tokens, words, phrases, metrics) makes drift structurally impossible. Cards declare figures by composing primitives; the library guarantees consistency. +- **Emit explicit `width`/`height` on every SVG; use `max-width: 100%` in CSS, never `width: 100%`.** Without `width`/`height` the browser stretches a small viewBox to fill its container, doubling text inside. This was the root cause of every "figure too big" report. The fix lives in `Canvas.to_svg()` and the CSS rules in `public/site.css`, `scripts/build_prototypes.py`, and `scripts/build_marginalia.py`. +- **A figure's diagrammatic content must not duplicate its figcaption.** SVGs may carry functional labels (`stdout`, `iter()`, panel tags like `before` / `after`, type signatures like `x: int | str | None`). Full sentences describing the figure as a whole are prose and belong in the figcaption. Captions are the canonical voice. The exception is review-only pages (`marginalia-gestalt`) where cards have no figcaption; figures destined for promotion to production must drop their inline prose first. +- **Emphasis is scarce.** With site `--accent` saturated for UI use, a coral arrow on every line reads as no emphasis at all. `closed_arrow` defaults to `emphasis=False`; figures opt in only for the single element the prose names. Same rule for accent dots, gates, and ring highlights. +- **Soft fills should be neutral, not accent-tinted.** A 5% warm-brown tint reads as a quiet container. An accent-tinted soft fill makes every object box look highlighted, which breaks the scarcity rule a second way. +- **Two rubrics, one craft section.** Journey-section figures depict a *conceptual shift* across multiple lessons; example-cell figures depict the *single move* the surrounding cell discusses. `docs/journey-visualisation-rubric.md` and `docs/example-figure-rubric.md` score each on 10 points: content fidelity, craft, context. Topic gates per kind of section / cell shape. +- **Constraint-shaped sections resist mechanism figures.** Workers' "Preserve the lesson while respecting the runtime" is a principle, not a mechanism. Forcing figures on such sections scores below the 8.5 gate. Either reframe the section around a mechanism or accept the gap deliberately. +- **Authoring stays on the contributor; figures stay on the curator.** Example markdown does not include figure references. `src/marginalia.py` holds `FIGURES` (paint functions) and `ATTACHMENTS` (slug → cell → figure → caption). Curating figures is a single-file edit that contributors never see. +- **Inline between prose and code is the production layout; banners between cells is the prototyped richer grammar.** Cells with figures drop to single-column stacking (prose, figure, code) via `.lp-cell.has-figure { grid-template-columns: 1fr }`. Cells without figures keep today's `prose | code` 2-column grid bit-for-bit. The banner-between approach (`/prototyping/layout-banner-*`) supports multi-figure small-multiples between cells when one inline figure isn't enough. +- **Centralised gestalt pages catch drift that page-by-page review misses.** `/prototyping/marginalia-gestalt`, `/prototyping/journey-figures-gestalt`, and `/prototyping/production-figures-gestalt` show every figure in three different framings. Seeing all section figures of a journey in one 3-up row exposes inconsistencies invisible across six tabs. +- **Mapping reuses existing figures; promoting moves design to production.** Half of example coverage came from attaching existing FIGURES to new examples (no paint code). The other half from new paint code copied or designed from gestalt cards. Both paths must pass the rubric. +- **Tests against the cell layout must allow the `has-figure` class.** When the renderer adds `has-figure` to cells with attached figures, assertions on the literal string `class="lesson-step lp-cell"` fail. Change those tests to check the substring `lesson-step lp-cell` so both variants match. +- **Score what's shipping, not what was designed.** A scoring dict on the gestalt is design-time review. Production figures live in `src/marginalia.py` `FIGURES` and may have been redesigned during promotion. Scoring should track the production version with the gestalt as separate history. +- **Some examples should never have figures.** Constraint-shaped, infrastructure-shaped, and aggregator-shaped slugs lack a single mechanism to depict. Force-fitting figures on them scores below the gate. Leave them figure-less and document why rather than ship weak figures. +- **Audits without contracts rot; bug classes need automated gates.** When you find a bug class — clipping, collision, off-palette colour, drifting twin coordinates, duplicate caption — write a unit test that asserts the invariant across every figure. The geometry contracts in `tests/test_marginalia_geometry.py` started as ad-hoc scripts and were promoted to CI gates after the same bug class recurred. 54 tests today cover 9 contract families; new figures pass them automatically because each test iterates `FIGURES`. +- **Clipping ≠ collision; padding the viewBox fixes one but not the other.** The `value-types` bug had two components: the first `INT` tag was clipped above the viewBox (geometry escapes its frame), and the `STR`/`LIST`/`DICT` tags overlapped the boxes above them (geometry collides internally). Padding the viewBox solved (1) and disguised (2). Element-element collision needs its own audit that walks every text-rect, text-text, and (for label-on-edge cases) text-line bounding-box pair. +- **Heuristic audits over-flag; trust the design, not the regex.** Probes for "prose duplication" (SVG text matching caption substring), "text crossing a line" (label bbox bisected by a hairline), and "text overlapping a circle" each surfaced ~3-10 hits — all false positives. Diagrammatic labels naturally appear in captions (`__getattr__`, `yield from inner`); dashed strikes through `.append` are deliberate; text inside node circles is the design. Don't promote heuristic findings to contracts without confirming each is a real bug. +- **Structural twins must share coordinates exactly.** When two figures depict parallel concepts — `kw-only-separator` and `positional-only-separator`, `class-triangle` and `metaclass-triangle` — they read as a pair. A single-pixel drift in one breaks the visual rhyme. Treat a coordinate change in one as a forced change in both, in the same commit. The audit caught `kw-only-separator` at the old `x=82` after `positional-only-separator` had moved to the corrected `x=75`. +- **Reused figure → bespoke caption per slug.** The 8 library figures (`iter-protocol` across 4 iteration slugs, `aliasing-mutation` across mutability + copying-collections, etc.) shouldn't all carry the same caption. Each slug is a separate lesson; the figure stays, the framing changes. `FigureCaptionContract` enforces uniqueness across slugs after one verbatim duplicate slipped through. +- **One paint registry, not two.** The marginalia-gestalt review page once rendered its own `e_*` paint functions parallel to `src/marginalia.py FIGURES`. Reviewers saw a different picture than readers, and the gestalt's bugs (overlapping labels, misaligned dashed lines) didn't ship. The gestalt is now a thin view over production: same paint, same drift surface, same audit coverage. 862 lines of duplicated paint code went away. +- **Tag-above vs tag-inside is a layout decision driven by stacking.** `object_box(tag_position="above")` is natural for an isolated box; `tag_position="inside"` is required when boxes stack vertically with less than ~13px of gap (the tag's footprint). Defaults to `"above"` for the common isolated case; stacked callers opt in. The grammar carries the choice, not the caller's hand-positioned `tag()` call. +- **Mono character alignment uses the font's advance, not eyeballed pixels.** JetBrains Mono advances ~6px per char at fs=10. A dashed line marking the `/` at index 12 of `def f(a, b, /, c, d): …` lives at `x=12*6+3=75`, not `x=82`. Hand-tuned positions drift; computed positions match the rendered glyph. +- **Lines must terminate AT elements, not in their gaps or interiors.** A 1.5px gap between a tree edge and a leaf dot reads as "the tree is disconnected" (the `exception-group-peel` bug). A line endpoint 2px inside a circle reads as "the arrow pierces the node" (the `context-bowtie` bug). When connecting to a dot, end the line at the dot's centre and let the dot draw on top — the visual termination is the circumference, with zero gap or overshoot. +- **Journey pages render section figures inline.** `SECTION_FIGURES` lives in `src/marginalia.py` (single source of truth, keyed by section title) and `render_for_section(title)` is invoked from `render_journey_page` between each section's meta and its example list. The same paint code that produces the `/prototyping/journey-figures-gestalt` review page renders on production journey pages; drift between the two is structurally impossible. Contract 10 asserts every section in `JOURNEYS` has a figure and every figure name resolves. +- **An explicit comparison loop should iterate over the topic's whole spectrum.** When a cell teaches by doing `for label, value in [(...), (...)]: print(...)`, the bracketed list IS the lesson. Two items is a binary contrast; three items reads as a progression. The strings example presented English (pure ASCII, 1 byte/char) against Thai (3 bytes/char) but skipped the Latin-extended middle (French `café`: 4 code points, 5 bytes — `é` is 2 UTF-8 bytes). Adding the middle row turned the cell from "ASCII vs non-Latin" into "1-byte / 2-byte / 3-byte progression." The rule is narrow — most examples spread categories across cells, which is also a valid pattern — but when a comparison loop exists, fill it with the topic's actual spectrum, not just the endpoints. diff --git a/docs/markdown-cell-migration-investigation.md b/docs/markdown-cell-migration-investigation.md deleted file mode 100644 index 0cf7347..0000000 --- a/docs/markdown-cell-migration-investigation.md +++ /dev/null @@ -1,288 +0,0 @@ -# Markdown cell migration investigation - -The rollback showed that the migration problem is not Markdown itself. The problem is coupling three different things into one source block: - -1. the full editable program; -2. the literate source fragment shown beside prose; -3. the executable unit used by the verifier. - -The first migration derived the full editor program by joining cells. That made non-executable explanatory fragments impossible and caused several examples to collapse into one large cell. The better model is: - -- `:::program` owns the full editable source and must match the current `src/examples.py` code byte-for-byte during migration; -- `:::cell` owns a verified teaching fragment with nearby output; -- cells may restate earlier definitions to stay executable; -- cells are not concatenated to make the editor program. - -## Examples that lost fine-grained cells - -These examples collapsed during the attempted migration: - -- `match-statements` -- `recursion` -- `classes` -- `properties` -- `special-methods` -- `type-hints` - -## Proposed executable cell rewrites - -These sketches preserve fine-grained teaching while keeping cells executable. - -### `match-statements` - -Use separate complete `match` statements with different command values: - -```python -command = {"action": "move", "x": 3, "y": 4} - -match command: - case {"action": "move", "x": x, "y": y}: - print(f"move to {x},{y}") -``` - -```output -move to 3,4 -``` - -```python -command = {"action": "quit"} - -match command: - case {"action": "move", "x": x, "y": y}: - print(f"move to {x},{y}") - case {"action": "quit"}: - print("quit") -``` - -```output -quit -``` - -```python -command = {"action": "jump"} - -match command: - case {"action": "move", "x": x, "y": y}: - print(f"move to {x},{y}") - case {"action": "quit"}: - print("quit") - case {"action": action}: - print(f"unknown action: {action}") - case _: - print("invalid command") -``` - -```output -unknown action: jump -``` - -### `recursion` - -Show the base case first, then the recursive case: - -```python -def factorial(n): - if n == 0: - return 1 - -print(factorial(0)) -``` - -```output -1 -``` - -```python -def factorial(n): - if n == 0: - return 1 - return n * factorial(n - 1) - -print(factorial(5)) -``` - -```output -120 -``` - -### `classes` - -Restate the class as it gains behavior: - -```python -class Counter: - def __init__(self, start=0): - self.value = start - -first = Counter() -second = Counter(10) -print(first.value) -print(second.value) -``` - -```output -0 -10 -``` - -```python -class Counter: - def __init__(self, start=0): - self.value = start - - def increment(self, amount=1): - self.value += amount - return self.value - -first = Counter() -second = Counter(10) -print(first.increment()) -print(second.increment(5)) -``` - -```output -1 -15 -``` - -### `properties` - -Show stored attributes first, then the computed property: - -```python -class Rectangle: - def __init__(self, width, height): - self.width = width - self.height = height - -box = Rectangle(3, 4) -print(box.width) -print(box.height) -``` - -```output -3 -4 -``` - -```python -class Rectangle: - def __init__(self, width, height): - self.width = width - self.height = height - - @property - def area(self): - return self.width * self.height - -box = Rectangle(3, 4) -print(box.area) -``` - -```output -12 -``` - -### `special-methods` - -Restate the class as each protocol method is added: - -```python -class Bag: - def __init__(self, items): - self.items = list(items) - -bag = Bag(["a", "b"]) -print(bag.items) -``` - -```output -['a', 'b'] -``` - -```python -class Bag: - def __init__(self, items): - self.items = list(items) - - def __len__(self): - return len(self.items) - -bag = Bag(["a", "b"]) -print(len(bag)) -``` - -```output -2 -``` - -```python -class Bag: - def __init__(self, items): - self.items = list(items) - - def __len__(self): - return len(self.items) - - def __iter__(self): - return iter(self.items) - -bag = Bag(["a", "b"]) -print(list(bag)) -``` - -```output -['a', 'b'] -``` - -```python -class Bag: - def __init__(self, items): - self.items = list(items) - - def __len__(self): - return len(self.items) - - def __iter__(self): - return iter(self.items) - - def __repr__(self): - return f"Bag({self.items!r})" - -bag = Bag(["a", "b"]) -print(bag) -``` - -```output -Bag(['a', 'b']) -``` - -### `type-hints` - -Show runtime behavior separately from stored annotations: - -```python -def total(numbers: list[int]) -> int: - return sum(numbers) - -print(total([1, 2, 3])) -``` - -```output -6 -``` - -```python -def total(numbers: list[int]) -> int: - return sum(numbers) - -print(total.__annotations__) -``` - -```output -{'numbers': list[int], 'return': } -``` - -## Recommendation - -Before another Markdown migration, first update the current examples or golden fixture to use executable fine-grained teaching cells. Then migrate to Markdown with `:::program` preserving the editor source exactly and `:::cell` preserving the literate teaching structure. diff --git a/docs/rubric-saturation.md b/docs/rubric-saturation.md new file mode 100644 index 0000000..3352113 --- /dev/null +++ b/docs/rubric-saturation.md @@ -0,0 +1,142 @@ +# Rubric saturation analysis + +After six iteration passes, the figure system has 109 examples +attached (one per slug on `main`) and 109 figures in +`src/marginalia.py FIGURES`. Coverage is 100%. Distribution against +`docs/example-figure-rubric.md`: + +| band | count | composition | +|---|---:|---| +| 9.5 | 3 | the canonical pictures (`variables`, `mutability`, `copying-collections`) | +| 9.0 | ~35 | strong mechanism, single move, runs match cell | +| 8.5 | ~55 | strong but honest reuse, or generic placeholders | +| 8.0 | ~16 | binding pictures, abstract pictures, weak reuses | + +Mean ≈ 8.7. **No figure scores below 8.0.** No figure exceeds 9.5. +Pushing further requires changes to the rubric itself, because the +remaining drag comes from criteria that are structurally over-strict +for a library this size. + +## Why every figure cannot reach 9.0 under the current rubric + +Two criteria in `docs/example-figure-rubric.md` cap most figures +at 8.5 by design: + +### Criterion 2 — "Match the running variables (0–1.0)" + +A figure loses up to 1.0 when its placeholders (`a`, `b`, `xs`) do +not match the cell's specific names (`first`, `second`, `factor`, +`numbers`). For a library of 109 figures across 109 cells, matching +running variables one-for-one would require 109 bespoke paint +functions; reuse becomes impossible. Today 12 figures are reused +across multiple slugs precisely because they capture a *general* +mechanism (`iter-protocol` covers `iterators`, +`iterator-vs-iterable`, `iterating-over-iterables`, +`container-protocols`). Every reuse pays a tax against this +criterion. + +The criterion was written for a small boutique catalogue where one +figure per lesson is the norm. At 109 figures the cost of strict +matching is unbounded; the criterion's *intent* — "make the figure +recognisably about this cell, not a different lesson" — is satisfied +already by criterion 1 (cell fidelity) plus criterion 4 (mechanism). + +### Criterion 9 — "Independence from lesson figures (0–1.0)" + +A journey-section figure scoring 9 elsewhere loses up to 1.0 when +attached to a related lesson. `iter-protocol` is the section figure +for *Iteration · See the protocol behind `for`* and the cell figure +for four iteration-adjacent lessons. The rubric counts the lesson +attachments down on independence, even though they are the most +honest depiction available. + +The intent was to prevent a journey-section figure from being +literally re-rendered as the only diagram on its constituent lesson +pages — that *would* read as redundant. But in our flow, the +journey-section figure already sits at `/journeys/`, and the +lesson appears alone at `/examples/`; readers don't see both +beside each other. The "independence" penalty fires regardless. + +## What the rubric needs + +Four upgrades would let further iteration produce visible quality +gains rather than just shuffling the same band. + +### 1. Tier figures into **library** and **canonical** + +A *library* figure is a primitive of the system: meant for reuse, +generic by design (e.g. `iter-protocol`, `branch-fork`, +`class-triangle`). A *canonical* figure is unique to one cell, with +that cell's specific running variables baked in (e.g. +`aliasing-mutation`, `mutability`'s three-state strip). + +For library figures: criterion 2 (running variables) and 9 +(independence) should be **non-scored**. Score them once at +registration; cap their attached score at 9.0 (not 10). + +For canonical figures: criteria 2 and 9 stay as written. Cap at +9.5 only if the figure is *the* picture for that mechanism — the +9.5 floor is supposed to be rare and definitive. + +Result: ~70 library figures (today reuse-shaped) all reach 9.0; +~30 canonical figures reach 9.0–9.5 by being slug-specific. + +### 2. Replace criterion 2 with **"the figure earns its place"** + +Strict variable-matching loses information value at scale. The +better question is "does swapping in this figure improve the cell +versus showing no figure?" If yes, full credit. If the figure +contains marks the cell's prose doesn't motivate, deduct. + +Practical rewrite of criterion 2 (0–1.0): + +> The figure adds something the prose cannot show in the same word +> count: a relationship, a before/after, a hidden mechanism. A +> figure that merely restates the prose in diagram form earns 0.5; +> a figure that surfaces a relationship invisible in the prose +> earns 1.0. + +This rewards genuine pedagogical value and accepts honest reuse. + +### 3. Add **caption rubric** + +Captions today are scored only as "present" (criterion 5). +Quality varies: some assert ("Two names share one mutable list — appending through one name changes the object visible through both."); others hedge ("The figure shows..."). A separate 0–1.0: + +> Caption declares what is true, in the section summary's voice; +> does not narrate what the figure does. "Two names share one list" +> earns 1.0; "Here we see two names" earns 0. + +Captions written under this criterion will pull weak figures up by +~0.5 points. + +### 4. Add **page-level coherence** + +Currently a slug with three attached figures scores three figures +independently. A page that ships three 8.5 figures is *worse* than +one 9.0 figure on the same page (cognitive load, redundancy). A +page-level rubric (0–1.0) would score: + +> When multiple figures attach to one slug, they form a coherent +> set — different aspects of the same lesson, not three angles on +> the same point. + +Today this is a manual judgement; codifying it would prevent the +inevitable "too many figures" failure mode as coverage grows. + +## What this turn changed + +- Fixed the layout regression: cells stay 2-col always; figures live + in banner rows BETWEEN cells. `hello-world` now matches production. +- Six targeted figure refinements: `tuple-frozen` shows the frozen + aspect (struck-through .append); `literal-forms` shows specific + literal spellings per type; `function-with-body` shows a specific + function with its return value; spec/rubric docs updated to reflect + banner-between in production. +- Documented the rubric saturation: 9.0 floor isn't reachable for + every figure under the current rubric without designing slug- + specific paint code for ~70 reusable library figures, which sells + reuse for marginal score gain. + +The rubric upgrades above are what would make the next pass produce +visible quality gains rather than re-shuffling the same 8.5 band. diff --git a/docs/shallow-example-audit.md b/docs/shallow-example-audit.md deleted file mode 100644 index e58426a..0000000 --- a/docs/shallow-example-audit.md +++ /dev/null @@ -1,265 +0,0 @@ -# Shallow example audit - -This audit looks for examples that may surprise readers by being narrower than their title, summary, or journey placement suggests. The main smell is the **broad surface tour problem**: a page title sounds like a tour of a feature family, but the example demonstrates one small slice without enough scoping, contrast, or links to neighbors. - -## Method - -Signals used: - -- broad titles such as `Operators`, `Literals`, `Testing`, `Packages`, `Regular Expressions`, `Type Hints`, `Async Await`, `Special Methods`, `Modules`, `JSON`, `Numbers` -- one-cell or two-cell examples for multi-part concepts -- very short programs under broad titles -- prose that scopes the page poorly or uses generic boilerplate -- missing nearby syntax that a learner would reasonably expect on the page -- journey order that hides prerequisites - -This is a qualitative audit. Passing verification proves examples run; it does not prove they are comprehensive enough for their title. - -## Recently fixed broad-surface issue - -### Former combined `Operators and Literals` - -Status: **split into `Operators` and `Literals`**. - -Why it surprised: - -- The title promised a large surface: operators plus literals. -- The page sampled raw strings, bytes, complex numbers, bit operators, set symmetric difference, `@`, `...`, and `:=`. -- That was useful surface coverage, but it read like a grab bag rather than a coherent map of Python's operator/literal families. - -What changed: - -- `Operators` now focuses on expression syntax that combines, compares, and tests values. -- `Literals` now focuses on source-code forms that create values directly. -- Runtime and Control Flow journeys now point to the narrower pages. -- `Operators` links to `Assignment Expressions`; `Literals` links to focused value/container/text pages. - -Expected rubric impact: - -- `Operators`: approximately **8.8-9.2** if it keeps its surface-map framing and avoids becoming an all-operators reference page. -- `Literals`: approximately **8.7-9.1** if it remains a value-syntax map and keeps links to focused pages. - -Remaining watch point: - -- `Operators` still covers a lot of syntax. If it starts to feel like a table of punctuation, split out `operator-precedence` or keep precedence as a note only. - -## Recently fixed shallow examples - -### `bytes-and-bytearray` - -Status: **fixed**. - -What changed: - -- Split one compressed cell into encode, decode, byte indexing, and `bytearray` mutation cells. -- Added the important boundary that indexing `bytes` returns integers. -- Linked the page to `strings`, `literals`, and `networking`. - -### `type-aliases` - -Status: **fixed**. - -What changed: - -- Replaced generic boilerplate with a sharper static-shape explanation. -- Added separate cells for the `type` statement, runtime alias names, and assignment-style aliases. -- Explicitly contrasted aliases with `NewType`. - -### Type-system cluster single-cell examples - -Affected pages: - -- `runtime-type-checks` -- `union-and-optional-types` -- `typed-dicts` -- `callable-types` -- `generics-and-typevar` -- `casts-and-any` -- `newtype` - -Status: **fixed for the first pass**. - -What changed: - -- Added static-vs-runtime or boundary contrast cells across the cluster. -- `runtime-type-checks` now separates exact type, inheritance-aware checks, and class relationships. -- `union-and-optional-types` now shows narrowing and visible runtime annotations. -- `typed-dicts` now shows ordinary dict runtime behavior and optional keys. -- `callable-types` now contrasts functions with callable objects. -- `generics-and-typevar` now shows input/output type relationships and runtime annotations. -- `casts-and-any` now shows that `cast()` returns the same object and contrasts with runtime narrowing. -- `newtype` now contrasts static identity with runtime integer behavior. - -## Medium-priority findings - -### `numbers` - -Status: **improved, still introductory**. - -What was fixed: - -- Added complex numbers to the numeric story instead of leaving them only in `Literals`. -- Added visible floating-point approximation output. -- Linked to `Literals` and `Operators`. - -Remaining gap: - -- Decimal and fraction arithmetic remain separate future topics. - -### `modules` - -Status: **improved, still introductory**. - -What was fixed: - -- Added explicit scoping: this page is about import forms and namespaces. -- Added `sys.modules` cache evidence. -- Linked to `Import Aliases` and `Packages`. - -Remaining gap: - -- Module search paths, side effects, entry points, reloads, and relative imports remain separate future topics. - -### `packages` - -Status: **improved, still introductory**. - -What was fixed: - -- Replaced generic boilerplate. -- Split package-as-module, dotted imports, and dynamic import into separate cells. - -Remaining gap: - -- Does not show real on-disk package layout because examples cannot create a project tree naturally in the runner. - -Recommendation: - -- Add an unsupported/illustrative layout snippet if needed: - `project/pkg/__init__.py`, `project/pkg/models.py`. - -### `regular-expressions` - -Status: **improved, still first-pass**. - -What was fixed: - -- Split repeated extraction, first-match groups, and string-method alternative. - -Remaining gap: - -- No substitution, flags, compiled patterns, anchors, or greedy/non-greedy behavior. - -Recommendation: - -- Keep current page scoped as first pass. -- Add future `regex-substitution` or `regex-flags` only if the catalog needs deeper text coverage. - -### `testing` - -Status: **improved, still minimal**. - -What was fixed: - -- Replaced generic boilerplate. -- Shows named test methods, assertions, suite, runner result. - -Remaining gap: - -- No failure output, fixtures, parameterization, mocks, or pytest comparison. - -Recommendation: - -- Add a deliberate failing-test cell only if the page can keep deterministic output readable. -- Consider future `test-fixtures` or `pytest-style-tests` example. - -### `json` - -Status: **improved, mostly acceptable**. - -What was fixed: - -- Added `indent=2` formatting evidence. -- Added invalid JSON boundary with `JSONDecodeError`. -- Linked to dictionaries, `TypedDict`, and strings. - -Remaining gap: - -- File helpers (`dump`/`load`) and custom encoders remain separate advanced topics. - -## Lower-priority findings - -### `decorators` - -Status: **improved**. - -What was fixed: - -- Added `functools.wraps` to the example and showed preserved metadata. -- Linked to closures, functions, and callable types. - -### `context-managers` - -Status: **improved**. - -What was fixed: - -- Added a class-based `__enter__` / `__exit__` cell. -- Kept `contextlib.contextmanager` as the concise neighboring form. -- Added the exception propagation/suppression boundary. - -### `exceptions` - -Status: **adequate first pass**. - -Gap: - -- Does not show `else`/`finally`; those may be expected under exceptions. - -Recommendation: - -- Consider an `exception-cleanup` example if reliability coverage needs it. - -### `async-await` - -Status: **acceptable because scoped**. - -Why acceptable: - -- It clearly focuses on coroutine creation, `await`, and `gather`. -- `async-iteration-and-context` covers neighboring async protocols. - -Gap: - -- No cancellation, tasks, timeouts, or task groups. - -Recommendation: - -- Do not expand this page into all async. Add focused pages if needed. - -### `type-hints` - -Status: **acceptable because scoped and journey-supported**. - -Why acceptable: - -- It draws the important static-vs-runtime boundary. -- The Types journey has focused neighbors for unions, aliases, protocols, callables, generics, `Any`, and `NewType`. - -Gap: - -- Could link more explicitly to those neighbors. - -## Action list - -Completed: - -- Split `Operators and Literals` into `Operators` and `Literals`. -- Expanded `bytes-and-bytearray` into multiple boundary cells. -- Rewrote `type-aliases` with contrast and multiple cells. -- Added static-vs-runtime contrast cells to the shallow type-system cluster. - -Remaining: - -1. Consider focused follow-up examples such as `decimal-and-fractions`, `module-entry-points`, `import-system`, `operator-precedence`, and `exception-cleanup`. -2. Continue auditing broad pages after each new journey change; broadness can return when examples are moved or renamed. diff --git a/docs/visual-explainer-spec.md b/docs/visual-explainer-spec.md new file mode 100644 index 0000000..1bebafa --- /dev/null +++ b/docs/visual-explainer-spec.md @@ -0,0 +1,283 @@ +# Visual explainer spec + +This spec describes how figures attach to existing Python By Example pages +without changing what contributors author. The earlier draft of this spec +relied on absolute-positioned figures escaping into the page's implicit +outer margin; that approach was rolled back in favour of inline placement +between prose and code, which works at every viewport. + +## Goals + +- **One column model per page type, fixed.** Example pages keep cells in + the prose|code 2-col grid; journey pages keep section heading + figure + in a 2-col grid. Figures never reflow the surrounding columns. +- **Universal, not viewport-conditional.** A reader at any width sees the + same figure in the same place. No `@media` breakpoints for figure + positioning; no overlay layer. +- **Multiple figures supported.** Banners hold one, two, or three + figures via an auto-fit grid; small multiples are first-class. +- **No contributor burden.** Example markdown stays as it is. Figures are + curated separately by the project owner. +- **Quiet by default.** A page with no figures attached renders + bit-for-bit identical to today. +- **Grammar reuse.** Figures are composed from the locked vocabulary in + `src/marginalia_grammar.py`. No bespoke SVG. + +## Layout strategy + +Two patterns, one for each page type. Both keep their underlying column +model fixed; figures slot into defined positions without disrupting it. + +### Example pages — banners between cells + +Each `.lp-cell` stays `grid-template-columns: minmax(17rem, .85fr) +minmax(0, 1fr)` — prose left, code right — **always**. Figures live in +banner rows that sit between cells, not inside them. The banner spans +the full content width and uses an auto-fit grid so it can hold one, +two, or three figures as small multiples. + +``` +[ cell 0 · prose | code ] +─────── banner row ─────── +[ figure ] optional caption +───────────────────────── +[ cell 1 · prose | code ] +``` + +This matches the union of three influences: +- *Knuth* — cells preserve the literate-program rhythm of prose and code + side by side, uninterrupted. +- *Tufte* — the banner slot accepts a small-multiple of related figures + so contrasts and progressions read as one composition. +- *Algebrica* — each banner figure carries a quiet italic caption beneath, + in the muted text colour, with generous whitespace above and below. + +Banner positions: + +| key | renders | +|--------------------|--------------------------------------| +| `before` | once, before the first cell | +| `after-cell-N` | once, after cell N (zero-indexed) | +| `after-walkthrough`| once, after the last cell | + +Each position holds **one or more** figures via `cell-banner` markup. +Captions are per-figure. + +```html +
+
prose | code
+
+
+ +
Mutable: change visible through any alias.
+
+
+ +
Immutable: aliases share a frozen value.
+
+
+
prose | code
+
+``` + +```css +.cell-banner { + margin: var(--space-5) 0; + padding: var(--space-4) 0; + border-block: 1px dashed var(--hairline-soft); + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: var(--space-4); + justify-items: center; +} +.cell-banner figure { margin: 0; padding: 0; max-width: 360px; } +.cell-banner svg { width: 100%; height: auto; display: block; } +.cell-banner figcaption { + margin-top: var(--space-2); + color: var(--muted); + font-size: .92rem; + font-style: italic; + max-width: 44ch; +} +.cell-banner--1 figure { max-width: 440px; } +``` + +The cell never reflows: cells without banners around them and cells +between banners look identical to today's layout. + +### Journey pages — figure beside section heading + +Journey pages are not literate code; they are linear lists of items +grouped under section headings. Here the figure lives **beside** the +section heading in a 2-column row (heading-and-list on the left, figure +on the right). The column model is fixed for the whole journey page: +each section is a 2-col grid, every section the same shape. + +```css +.journey-section { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--space-4); +} +@media (min-width: 900px) { + .journey-section { + grid-template-columns: minmax(0, 1.4fr) minmax(220px, 320px); + align-items: start; + } +} +``` + +One figure per section, faithful to the section's conceptual shift, +scored against `docs/journey-visualisation-rubric.md`. The same template +is reused for every journey; figures are mapped via +`JOURNEY_SECTION_FIGURES` in `scripts/build_prototypes.py`. Sections +without a figure render as a heading + list with no figure column, +which is the intended state for journeys still pending design (today, +all three sections of the Workers journey). + +### Why these two, not five + +The earlier prototypes that mixed inline-above, inline-between, +after-output, and prose-aside all switched the cell's column model +when a figure was present. That created the perils of randomly +oscillating between 1, 2, and 3 columns within the same page. The +banner-between grammar fixes the column model and lets figure count +vary instead. Adding a second or third figure changes the banner's +internal grid (auto-fit handles 1/2/3+), but the cells around it +remain unchanged — no reflow, no cognitive context-switch. + +## Anchors and attachments + +`src/marginalia.py` declares which figures attach where. The data shape +will move from per-cell injection toward per-position banners: + +```python +# proposed shape — banners keyed by position, each holding 1+ figures +BANNERS = { + "mutability": { + "after-cell-0": [ + ("aliasing-mutation", + "Two names share one mutable list — appending through one " + "name changes the object visible through both."), + ("tuple-no-mutation", + "By contrast, a tuple is frozen — aliases share a value " + "no method can change in place."), + ], + }, +} +``` + +Banner positions: + +| position | renders | +|--------------------|--------------------------------------| +| `before` | once, before the first cell | +| `after-cell-0`, … | once, after cell N (zero-indexed) | +| `after-walkthrough`| once, after the last cell | + +Each position is a list, not a single figure: the same banner may hold +multiple figures as a small multiple. Most slugs will start empty. +Adding a banner is a one-line edit in `src/marginalia.py`. + +## Authoring model + +### What contributors do + +Nothing new. Example markdown stays: + +``` +:::cell +prose… +```python +… +``` +```output +… +``` +::: +``` + +There is no `:::figure` block, no frontmatter key, no caption alongside +the prose. Contributors merge cell content; the figure layer is composed +independently. + +### What the project owner does + +Edit `ATTACHMENTS` in `src/marginalia.py`. Add a paint function (composed +from grammar primitives) and register it in `FIGURES`. Append a tuple of +`(anchor, figure_name, caption)` to `ATTACHMENTS[slug]`. Done. + +## Pipeline invariants (root-cause rules) + +These rules exist because we hit, and fixed, both failure modes +explicitly. Re-introducing either is a defect. + +1. **The SVG element renders at intrinsic CSS-pixel size.** + `Canvas.to_svg()` emits `width="W"` and `height="H"` matching the + `viewBox`. CSS that displays a figure must use `max-width: 100%`, + never `width: 100%`. With `width: 100%` a small viewBox is stretched + to fill the container, which doubles or triples the apparent text + size inside; with `max-width: 100%` the figure renders at its + designed size and only shrinks when the container is narrower. + +2. **A figure's diagrammatic content does not duplicate its figcaption.** + Where a figure is rendered with a `
` (production cell + pages, prototype journey pages, the journey-figures gestalt) the + SVG must not contain an inline `` that repeats the caption's + sentence. Captions are the canonical prose; the SVG is diagrammatic. + Functional labels inside the SVG (`stdout`, `iter()`, `next()`, + `await`, panel tags like `before` / `after`, type-signature + annotations like `x: int | str | None`) are diagrammatic — they + name a part of the figure, not the figure as a whole. A full + sentence describing the figure is prose and belongs in the + figcaption. + + The `marginalia-gestalt` review page is an exception: cards there + have no figcaption, so inline prose can stand in as the only + explanation. Figures destined for promotion to the production + registry must drop their inline prose first. + +## Files + +- `src/marginalia_grammar.py` — palette, tokens, words, phrases, metrics. + Aligned with `public/site.css` design tokens; figures use the four + palette constants and never pick colours directly. +- `src/marginalia.py` — figure registry (`FIGURES`) and attachment map. + Exports `render_for_anchor(slug, anchor)` for the current cell-inline + layout; banner-rendering helpers will land alongside. +- `src/app.py` — `_render_walkthrough_cell` is the current rendering + helper; the banner-between rollout will rename or replace it with a + walkthrough-level renderer that interleaves cells and banners. +- `public/site.css` — `.cell-banner` rules. Production uses the + banner-between grammar; cells always render with the prose|code + 2-column grid and never receive a `has-figure` class. +- `scripts/build_prototypes.py` — already implements the banner grammar + and journey-section grammar so prototypes can validate it before + production migration. + +## Edge cases + +- **Many figures in one banner.** Auto-fit grid handles 1 (centered, + larger), 2 (small-multiple pair), 3+ (wraps as content allows). More + than 3 in one banner is usually a signal that two adjacent banners + are clearer. +- **No figures attached.** The page renders bit-for-bit identical to + today. +- **Print.** Banners and cells both flow naturally in single-column + print contexts. +- **Very narrow viewports (≤340px).** Banner figures stack via the + auto-fit grid; SVGs scale via `max-width: 100%`. Cells keep their + existing 980px breakpoint for collapsing the 2-col grid. + +## Non-goals + +- **No JavaScript-driven layout.** No scroll listener, no resize observer, + no popup affordance. Pure CSS + server-side rendering. +- **No viewport-conditional layout.** The earlier margin-overlay approach + required a 1440px+ viewport to work; that complexity is gone. +- **No contributor surface.** Contributors do not author figures or + preview placement. +- **No chromatic decoration.** Figures use only the locked palette + (`--text`, `--muted`, `--accent`, `--accent-soft`-equivalent neutral). + Emphasis is scarce: at most one accent mark per figure, used only for + the single element the prose names. diff --git a/public/_headers b/public/_headers index 29969bb..d75a1d8 100644 --- a/public/_headers +++ b/public/_headers @@ -9,3 +9,6 @@ /favicon.svg Cache-Control: public, max-age=31536000, immutable + +/prototyping/* + Cache-Control: no-cache, must-revalidate diff --git a/public/prototyping/index.html b/public/prototyping/index.html new file mode 100644 index 0000000..09a77c3 --- /dev/null +++ b/public/prototyping/index.html @@ -0,0 +1,41 @@ + + + + +Visual explainer prototypes · Prototype + + + + +
Prototype · All prototypes · all prototypes
+ +
+
+

Prototypes · cache: no-cache, must-revalidate

+

Visual explainer prototypes

+

Real example pages with their attached figures, plus the design-review pages. The example pages all use the production layout: a cell with an attached figure stacks prose, figure, and code vertically; cells without figures keep today's prose|code grid.

+
+
  • Marginalia gestalt

    Every journey and example as a card, drawn from the shared grammar. Pure design review.

  • Journey-figures gestalt

    All journey section figures on one page, grouped by journey, for uniform rubric review.

  • Production figures gestalt

    Every figure currently registered in src/marginalia.py FIGURES, with a tag showing where it renders (example attachment, journey section, or unattached).

  • Layout · banner between cells

    The grammar: cells stay 2-column always; figures live in banner rows BETWEEN cells. Holds one figure here. The intended union of Tufte/Knuth/algebrica.

  • Layout · banner with small-multiples pair

    Same grammar with two figures in the banner — a Tufte small-multiple. The mutable list and the immutable tuple side by side, captioned, between the same pair of cells.

  • Layout · multiple banners across the walkthrough

    The grammar at scale: a single-figure banner before the walkthrough, a pair-banner between two cells, a single-figure summary after the last cell. Multiple diagrams; cells never displaced.

+
+ + + diff --git a/public/prototyping/journey-figures-gestalt.html b/public/prototyping/journey-figures-gestalt.html new file mode 100644 index 0000000..ad5b975 --- /dev/null +++ b/public/prototyping/journey-figures-gestalt.html @@ -0,0 +1,59 @@ + + + + +Journey-figures gestalt · Prototype + + + + +
Prototype · All 18 journey section figures grouped by journey, for uniform rubric review. · all prototypes
+ +
+
+

Journeys · 18 section figures

+

Journey-figures gestalt

+

Every journey section's figure on one page so the set can be reasoned about as a whole. Score against the rubric; redesign anything below the 8.5 gate before shipping to /journeys/<slug>.

+
+

Runtime

This journey builds the smallest coherent model of Python at runtime: programs run statements, names refer to objects, objects have types, and operations ask those objects to do work.

Start with executable evidence.

print("…")stdouthello world
Every page is a runnable program. The smallest mental model: source produces visible output.

Separate value, identity, and absence.

IS + ==ab[1,2]== ONLYab[1,2][1,2]
Two names can share one object (left, both `is` and `==` true) or hold two equal-but-distinct objects (right, only `==` true).

Read expressions as object operations.

a + bdispatchesa.__add__(b)
Operators are method calls. `a + b` dispatches to `a.__add__(b)`; the data model exposes the syntax.

Control Flow

This journey follows how a Python program chooses which path runs, names facts at decision points, and exits early when the remaining work no longer applies.

Choose between paths.

value?case Acase B
A value flows through a predicate to one of several branches.

Name and shape decisions.

len(xs):=nNAMEvaluen > 10
The walrus binds a name while the surrounding expression uses its value: one expression, two outputs.

Stop as soon as the answer is known.

abcdefound · breakfirst match
The loop exits at the first match — break short-circuits the rest of the sequence.

Iteration

This journey follows repeated work from ordinary loops to the iterator protocol: consume values, stop deliberately, and produce lazy streams only when they help.

Choose the right loop shape.

abcdbody
Walk the sequence, run the body, return; the shape behind for and while.

See the protocol behind `for`.

ITERABLE[a,b,c]iter()ITERATORnext()abc
iter() exposes the iterator behind for; next() pulls one value at a time until exhausted.

Compose lazy value streams.

SOURCE[a,b,c]FILTERx>0MAPx*2next()
Filters and maps compose without materialising intermediate lists; values flow through the pipeline only when next() pulls them.

Shapes

This journey teaches the core Python habit of choosing a data shape, transforming it directly, and making the result visible.

Pick the container that matches the question.

LIST[a,b]orderedTUPLE(a,b)fixedDICT{k:v}lookupSET{a,b}unique
Each container answers a different question: ordered, fixed, lookup, unique.

Move between shapes deliberately.

[3,1,4]sorted[1,3,4]
Most everyday code reshapes data: one input, one transform, one new value.

Cross text and data boundaries.

"42"TEXTparseINT42
Programs receive text and produce structured data; parsing makes the boundary explicit.

Interfaces

This journey shows how Python grows from simple functions to callable APIs, object interfaces, protocols, and metaclasses.

Start with functions as named behavior.

argsDEF F(...)return
A function is the first abstraction boundary: arguments in, body, return value out.

Use functions as values.

FNdef fg = fn
Functions are first-class values. A second name binds to the same function object.

Bundle behavior with state.

CLASS BOXSTATEx · yMETHODSmove(...)
Classes group fields and methods so data and behavior move together behind one interface.

Types

This journey maps Python's runtime object model to optional static annotations so learners know what types can and cannot promise.

Keep runtime and static analysis separate.

def f(x: int, y: str) -> bool: …
Annotations describe expected types for tools; the runtime accepts any object regardless.

Describe realistic data shapes.

X: INT|STR|NONExintstrNone
A typed slot can accept one of several shapes — `int | str | None` covers expected absence and alternatives.

Scale annotations for reusable libraries.

TFN[T]T
A generic type variable preserves shape across a call: the same T flows in and out.

Reliability

This journey follows the boundaries where programs fail, clean up, split into modules, communicate with the outside world, and run concurrent work.

Make failure explicit.

TRYEXCEPTELSEFINALLY
try, except, else, and finally as parallel lanes; the path traced through them is the actual control flow.

Control resource and module boundaries.

inbodyout
A context manager pairs setup with reliable cleanup; the raise path still routes through __exit__.

Handle operations that outlive one expression.

LOOPCOROawaitresume
On await, the coroutine yields to the loop; the loop runs other work and resumes when the awaitable is ready.

Workers

This journey explains the examples that were adapted so they can teach operating-system boundaries while still running inside Cloudflare Dynamic Workers.

Replace unavailable process boundaries with portable evidence.

UNAVAILABLEmultiprocessing.Process()PORTABLE EVIDENCEvalueasserted in-process
Worker isolation breaks the usual cross-process pathways; the lesson preserves a captured value as portable evidence instead.

Keep network lessons local to the protocol boundary.

REQUEST SHAPEGET /resourceRESPONSE SHAPE · ASSERTED LOCALLY200 OK · { … }
Demonstrate the protocol shape (request and response) rather than calling out over the network.

Preserve the lesson while respecting the runtime.

lesson questionprocess APIcaptured output
The lesson's evidence survives across the boundary that the worker runtime enforces.
+
+ + + diff --git a/public/prototyping/layout-banner-pair.html b/public/prototyping/layout-banner-pair.html new file mode 100644 index 0000000..6a269fd --- /dev/null +++ b/public/prototyping/layout-banner-pair.html @@ -0,0 +1,109 @@ + + + + +Mutability · banner with paired small-multiples · Prototype + + + + +
Prototype · One banner with two figures — list mutates, tuple does not. Same grammar, just more figures in the slot. · all prototypes
+ +
+ +
+

Data Model

+

Mutability

+

Some objects change in place, while others return new values.

+
+

Mutable objects can change in place. first and second point to the same list, so appending through one name changes the object seen through both names.

Source

first = ["python"]
+second = first
+second.append("workers")
+print(first)
+print(second)

Output

['python', 'workers']
+['python', 'workers']
BEFOREfirstsecond["python"]AFTER APPENDfirstsecond["python","workers"]
Two names share one mutable list — appending through one name changes the object visible through both.
TUPLE — FROZENfirstsecond("python",)NO .APPENDfirstsecond("python",)tuples raise AttributeError
By contrast, a tuple is frozen — its contents cannot change in place, so aliasing carries no mutation hazard.

Immutable objects do not change in place. String methods such as upper() return a new string, leaving the original string unchanged.

Source

text = "python"
+upper_text = text.upper()
+print(text)
+print(upper_text)

Output

python
+PYTHON

Some APIs make the boundary explicit. sorted() returns a new list, while methods such as append() and list.sort() mutate an existing list.

Source

numbers = [3, 1, 2]
+ordered = sorted(numbers)
+print(ordered)
+print(numbers)

Output

[1, 2, 3]
+[3, 1, 2]
+

Notes

+
  • Lists and dictionaries are mutable; strings and tuples are immutable.
  • Aliasing is useful, but copy mutable containers when independent changes are needed.
  • Pay attention to whether an operation mutates in place or returns a new value.
+
+

Run the complete example

+
+
+

Example code

+
first = ["python"]
+second = first
+second.append("workers")
+print(first)
+print(second)
+
+text = "python"
+upper_text = text.upper()
+print(text)
+print(upper_text)
+
+numbers = [3, 1, 2]
+ordered = sorted(numbers)
+print(ordered)
+print(numbers)
+
+
+

Expected output

['python', 'workers']
+['python', 'workers']
+python
+PYTHON
+[1, 2, 3]
+[3, 1, 2]
+
+
+
+
+ + + diff --git a/public/prototyping/layout-banner-single.html b/public/prototyping/layout-banner-single.html new file mode 100644 index 0000000..787b2e5 --- /dev/null +++ b/public/prototyping/layout-banner-single.html @@ -0,0 +1,109 @@ + + + + +Mutability · banner between cells (single figure) · Prototype + + + + +
Prototype · Cells keep their prose|code grid; one figure sits in a banner row between cell 0 and cell 1. · all prototypes
+ +
+ +
+

Data Model

+

Mutability

+

Some objects change in place, while others return new values.

+
+

Mutable objects can change in place. first and second point to the same list, so appending through one name changes the object seen through both names.

Source

first = ["python"]
+second = first
+second.append("workers")
+print(first)
+print(second)

Output

['python', 'workers']
+['python', 'workers']
BEFOREfirstsecond["python"]AFTER APPENDfirstsecond["python","workers"]
Two names share one mutable list — appending through one name changes the object visible through both.

Immutable objects do not change in place. String methods such as upper() return a new string, leaving the original string unchanged.

Source

text = "python"
+upper_text = text.upper()
+print(text)
+print(upper_text)

Output

python
+PYTHON

Some APIs make the boundary explicit. sorted() returns a new list, while methods such as append() and list.sort() mutate an existing list.

Source

numbers = [3, 1, 2]
+ordered = sorted(numbers)
+print(ordered)
+print(numbers)

Output

[1, 2, 3]
+[3, 1, 2]
+

Notes

+
  • Lists and dictionaries are mutable; strings and tuples are immutable.
  • Aliasing is useful, but copy mutable containers when independent changes are needed.
  • Pay attention to whether an operation mutates in place or returns a new value.
+
+

Run the complete example

+
+
+

Example code

+
first = ["python"]
+second = first
+second.append("workers")
+print(first)
+print(second)
+
+text = "python"
+upper_text = text.upper()
+print(text)
+print(upper_text)
+
+numbers = [3, 1, 2]
+ordered = sorted(numbers)
+print(ordered)
+print(numbers)
+
+
+

Expected output

['python', 'workers']
+['python', 'workers']
+python
+PYTHON
+[1, 2, 3]
+[3, 1, 2]
+
+
+
+
+ + + diff --git a/public/prototyping/layout-banner-trio.html b/public/prototyping/layout-banner-trio.html new file mode 100644 index 0000000..c3d7390 --- /dev/null +++ b/public/prototyping/layout-banner-trio.html @@ -0,0 +1,109 @@ + + + + +Mutability · banners across the whole walkthrough · Prototype + + + + +
Prototype · Demonstrates that the grammar accepts multiple banners at any position: lead-in figure, mid-walkthrough small-multiple pair, summary figure. Cells never reflow. · all prototypes
+ +
+ +
+

Data Model

+

Mutability

+

Some objects change in place, while others return new values.

+
+
BEFOREfirstsecond["python"]AFTER APPENDfirstsecond["python","workers"]
Two names share one mutable list — appending through one name changes the object visible through both.

Mutable objects can change in place. first and second point to the same list, so appending through one name changes the object seen through both names.

Source

first = ["python"]
+second = first
+second.append("workers")
+print(first)
+print(second)

Output

['python', 'workers']
+['python', 'workers']

Immutable objects do not change in place. String methods such as upper() return a new string, leaving the original string unchanged.

Source

text = "python"
+upper_text = text.upper()
+print(text)
+print(upper_text)

Output

python
+PYTHON
BEFOREfirstsecond["python"]AFTER APPENDfirstsecond["python","workers"]
Mutable: change visible through any alias.
TUPLE — FROZENfirstsecond("python",)NO .APPENDfirstsecond("python",)tuples raise AttributeError
Immutable: aliases share a frozen value.

Some APIs make the boundary explicit. sorted() returns a new list, while methods such as append() and list.sort() mutate an existing list.

Source

numbers = [3, 1, 2]
+ordered = sorted(numbers)
+print(ordered)
+print(numbers)

Output

[1, 2, 3]
+[3, 1, 2]
TUPLE — FROZENfirstsecond("python",)NO .APPENDfirstsecond("python",)tuples raise AttributeError
When in doubt, reach for the immutable shape.
+

Notes

+
  • Lists and dictionaries are mutable; strings and tuples are immutable.
  • Aliasing is useful, but copy mutable containers when independent changes are needed.
  • Pay attention to whether an operation mutates in place or returns a new value.
+
+

Run the complete example

+
+
+

Example code

+
first = ["python"]
+second = first
+second.append("workers")
+print(first)
+print(second)
+
+text = "python"
+upper_text = text.upper()
+print(text)
+print(upper_text)
+
+numbers = [3, 1, 2]
+ordered = sorted(numbers)
+print(ordered)
+print(numbers)
+
+
+

Expected output

['python', 'workers']
+['python', 'workers']
+python
+PYTHON
+[1, 2, 3]
+[3, 1, 2]
+
+
+
+
+ + + diff --git a/public/prototyping/marginalia-gestalt.html b/public/prototyping/marginalia-gestalt.html new file mode 100644 index 0000000..ed49f89 --- /dev/null +++ b/public/prototyping/marginalia-gestalt.html @@ -0,0 +1,719 @@ + + + + +Marginalia gestalt — Python By Example + + + +
+

Marginalia gestalt

+

Every example figure rendered from the same paint code that ships on /examples/<slug>.

+

Locked metrics, locked palette, locked typography. Cards compose words; words compose tokens; nothing is bespoke.

+
+ +

Examples

+
+
+

Basics · 01

+

Hello World

+ print("…")stdouthello world +

9.0 · program → output, smallest mechanism

+
+
+

Basics · 02

+

Values

+ INT42STR"hi"LIST[1,2,3]DICT{k:v} +

9.0 · every literal is a typed object

+
+
+

Basics · 03

+

Literals

+ int42 · 0x2a · 0b101float3.14 · 1e-3str"hi" · 'hi'list[1, 2, 3]dict{k: v}set{1, 2, 3} +

9.0 · literal spellings per type

+
+
+

Basics · 04

+

Numbers

+ INT · UNBOUNDEDFLOAT · REPRESENTABLE SPACING WIDENS +

9.0 · int unbounded vs float thinning, both registers

+
+
+

Basics · 05

+

Booleans

+ A AND BTFTFTFFF +

9.0 · 2×2 truth table

+
+
+

Basics · 06

+

Operators

+ *+423 +

9.0 · expression tree mechanism

+
+
+

Basics · 07

+

None

+ abcNONETYPENone +

9.0 · three names converging on one None

+
+
+

Basics · 08

+

Variables

+ xINT42 +

9.5 · the canonical name → object picture

+
+
+

Basics · 09

+

Constants

+ xINT42 +

9.0 · name binding; UPPER_CASE is convention

+
+
+

Basics · 10

+

Truthiness

+ xboolTrue or FalseFALSY VALUES00.0""[]{}NoneFalse +

9.0 · bool(x) with the falsy set as a strip

+
+
+

Data Model · 11

+

Equality and Identity

+ IS + ==ab[1,2]== ONLYab[1,2][1,2] +

9.0 · shared vs separate object, side-by-side

+
+
+

Data Model · 12

+

Mutability

+ BEFOREfirstsecond["python"]AFTER APPENDfirstsecond["python","workers"] +

9.5 · three-state small multiple of aliased mutation

+
+
+

Basics · 13

+

Object Lifecycle

+ __init__live · refcount > 0__del__ +

9.0 · __init__ → live → __del__

+
+
+

Text · 14

+

Strings

+ CODEPOINTScaféUTF-8 BYTES636166c3a9 +

9.0 · codepoints + bytes registers

+
+
+

Basics · 15

+

Bytes and Bytearray

+ BYTES — FROZENb'\\x63\\x61\\x66'BYTEARRAY — MUTABLEbytearray(b'\\x63\\x61').append(0x66) +

9.0 · frozen vs mutable contrast

+
+
+

Text · 16

+

String Formatting

+ FORMAT SPECalignsignwidth,.prectype{:>6,.2f} +

9.0 · format-spec railroad

+
+
+

Control Flow · 17

+

Conditionals

+ value?case Acase B +

9.0 · predicate forks value to branch

+
+
+

Control Flow · 18

+

Guard Clauses

+ if not valid: returnif missing: return Noneif special_case: return Xmain work …exit +

9.0 · early returns, main body at the tail

+
+
+

Control Flow · 19

+

Assignment Expressions

+ len(xs):=nNAMEvaluen > 10 +

9.0 · walrus binds while comparing

+
+
+

Control Flow · 20

+

For Loops

+ abcdnext()abcdnext()abcdnext()abcdnext() — last +

9.0 · 4-row caret advance

+
+
+

Control Flow · 21

+

Break and Continue

+ abcdefound · breakfirst match +

9.0 · early exit at first match

+
+
+

Control Flow · 22

+

Loop Else

+ loop bodyfell through · else runsbroke · else skipped +

9.0 · fell-through vs broke, two outcomes

+
+
+

Iteration · 23

+

Iterating over Iterables

+ ITERABLE[a,b,c]iter()ITERATORnext()abc +

9.0 · iter() exposes the iterator

+
+
+

Iteration · 24

+

Iterators

+ ITERABLE[a,b,c]iter()ITERATORnext()abc +

9.0 · three-state machine

+
+
+

Iteration · 25

+

Iterator vs Iterable

+ ITERABLE[a,b,c]iter()ITERATORnext()abc +

9.0 · the protocol exposed

+
+
+

Iteration · 26

+

Sentinel Iteration

+ iter(read, '')valuevaluevalue''sentinel · stop +

9.0 · iter(callable, sentinel) stop condition

+
+
+

Control Flow · 27

+

Match Statements

+ match valuecase 0:case [x, y]:case Point(0, _):case _:first match +

9.0 · dispatch ladder; first match wins

+
+
+

Control Flow · 28

+

Advanced Match Patterns

+ capture[x, y]alternativeP() | Q()guard[x] if x > 0classPoint(x=0, y=_) +

9.0 · four pattern variants

+
+
+

Control Flow · 29

+

While Loops

+ abcdbody +

9.0 · back-edge mechanism

+
+
+

Collections · 30

+

Lists

+ 314+1.append +

9.0 · cells with append mechanism

+
+
+

Collections · 31

+

Tuples

+ FROZEN SEQUENCE(3, 1, 4, 1).append +

9.0 · frozen sequence with struck-through .append

+
+
+

Collections · 32

+

Unpacking

+ 12345a*restb +

9.0 · binding-line mechanism with *rest

+
+
+

Collections · 33

+

Dictionaries

+ HASH → BUCKET0"a" → 11"b" → 22"c" → 3"d" → 4collision +

9.0 · hash buckets with collision chain

+
+
+

Collections · 34

+

Sets

+ KEYS ONLYabcx in sO(1) +

9.0 · hash buckets without values

+
+
+

Collections · 35

+

Slices

+ abcdef0123456[:3][3:] +

9.0 · ruler with bracket overlay

+
+
+

Collections · 36

+

Comprehensions

+ [x*2 for x in xs if x > 0]out = []for x in xs: if x > 0: out.append(x*2) +

9.0 · comprehension over equivalent for-loop

+
+
+

Collections · 37

+

Comprehension Patterns

+ [x*2 for x in xs if x > 0]out = []for x in xs: if x > 0: out.append(x*2) +

9.0 · nested clauses compose

+
+
+

Collections · 38

+

Sorting

+ INPUTSTABLE SORT BY KEY2 · Ada1 · Bo1 · Bo1 · Cy2 · Eve2 · Ada1 · Cy2 · Eve +

9.0 · stability ribbons preserved across keys

+
+
+

Collections · 39

+

Collections Module

+ dequefast appends both endsCounterkey → countdefaultdictmissing key defaultnamedtupletuple with names +

9.0 · deque / Counter / defaultdict / namedtuple

+
+
+

Collections · 40

+

Copying Collections

+ BEFOREfirstsecond["python"]AFTER APPENDfirstsecond["python","workers"] +

9.5 · same picture as mutability, perfect match

+
+
+

Functions · 41

+

Functions

+ nameDEF GREET(NAME):"Hello, " + name"Hello, Ada" +

9.0 · specific call: greet('Ada') → 'Hello, Ada'

+
+
+

Functions · 42

+

Keyword-only Arguments

+ def f(a, b, *, c, d): …positional or kwkeyword only +

9.0 · signature with explicit `*` separator

+
+
+

Functions · 43

+

Positional-only Parameters

+ def f(a, b, /, c, d): …positional onlypositional or kw +

9.0 · signature with explicit `/` separator

+
+
+

Functions · 44

+

Args and Kwargs

+ def f(*args, **kwargs): …→ tuple→ dict +

9.0 · *args tuple, **kwargs dict regions

+
+
+

Functions · 45

+

Multiple Return Values

+ def f(): return a, b(a, b)x, y +

9.0 · function returns tuple; caller unpacks

+
+
+

Functions · 46

+

Closures

+ MAKE_MULTIPLIERCELLfactor=2MULTIPLYuses cell +

9.0 · captured cell reference

+
+
+

Functions · 47

+

Partial Functions

+ f(a, b, c)partial(f, 1)g(b, c) +

9.0 · f → partial(f, 1) → g

+
+
+

Functions · 48

+

Global and Nonlocal

+ B · BUILT-ING · GLOBALE · ENCLOSINGL · LOCAL +

9.0 · LEGB nested rings

+
+
+

Functions · 49

+

Recursion

+ factorial(3)factorial(2)factorial(1)factorial(0) ← base +

9.0 · stacked frames with same name, different argument

+
+
+

Functions · 50

+

Lambdas

+ lambda x: x + 1paramsexpression +

9.0 · function literal: params / expression

+
+
+

Iteration · 51

+

Generators

+ PAUSED BETWEEN YIELDS · RESUMED BY NEXT()yieldyield +

9.0 · ribbon cut by yield gates

+
+
+

Iteration · 52

+

Yield From

+ OUTERyield from innerINNER +

9.0 · stitched ribbons; delegation

+
+
+

Iteration · 53

+

Generator Expressions

+ SOURCE[a,b,c]FILTERx>0MAPx*2next() +

9.0 · lazy filter→map pipeline

+
+
+

Iteration · 54

+

Itertools

+ ITER A1 · 2ITER B3 · 4CHAIN1 · 2 · 3 · 4 +

9.0 · chain joins two iterables into one stream

+
+
+

Functions · 55

+

Decorators

+ BEFOREfFNf₀AFTER @DECfwrapperf₀ +

9.0 · before/after rebinding through cell

+
+
+

Classes · 56

+

Classes

+ instanceCLASSClassTYPEtype +

9.0 · instance/class/type triangle

+
+
+

Classes · 57

+

Inheritance and Super

+ ABCDMRODBCAobject +

9.0 · MRO chain with diamond ghost

+
+
+

Classes · 58

+

Classmethods and Staticmethods

+ @classmethodfirst arg · Cls@staticmethodfirst arg · (none)instancefirst arg · self +

9.0 · three method kinds, three first-arg conventions

+
+
+

Classes · 59

+

Dataclasses

+ DECLARATIONname : strage : inttags : list__init__(name, age, tags) +

9.0 · fields → generated __init__ signature

+
+
+

Classes · 60

+

Properties

+ obj.xfget / fset__dict__ +

9.0 · obj.x routes through fget instead of __dict__

+
+
+

Data Model · 61

+

Special Methods

+ a + bdispatchesa.__add__(b) +

9.0 · syntax → method dispatch

+
+
+

Data Model · 62

+

Truth and Size

+ x__bool__()__len__() != 0default: True +

9.0 · __bool__ → __len__ → True fallback chain

+
+
+

Data Model · 63

+

Container Protocols

+ ITERABLE[a,b,c]iter()ITERATORnext()abc +

9.0 · iter/next backbone

+
+
+

Data Model · 64

+

Callable Objects

+ OBJECT__call__obj(...) +

9.0 · __call__ makes any object callable

+
+
+

Data Model · 65

+

Operator Overloading

+ a + bdispatchesa.__add__(b) +

9.0 · dispatch arrow

+
+
+

Data Model · 66

+

Attribute Access

+ obj.xinstance __dict__class __dict____getattr__ +

9.0 · instance __dict__ → class __dict__ → __getattr__

+
+
+

Data Model · 67

+

Bound and Unbound Methods

+ obj.methodbound · self filledClass.methodfunction · self required +

9.0 · instance.method bound vs Class.method unbound

+
+
+

Data Model · 68

+

Descriptors

+ obj.attrDESCRIPTOR__get____set____delete__ +

9.0 · get/set/delete protocol routed through descriptor

+
+
+

Classes · 69

+

Metaclasses

+ instanceCLASSClassMETACLASStype +

9.0 · extended triangle to metaclass

+
+
+

Data Model · 70

+

Context Managers

+ inbodyout +

9.0 · enter / body / exit bowtie

+
+
+

Data Model · 71

+

Delete Statements

+ BEFOREx[1, 2, 3]AFTER DEL Xx[1, 2, 3] +

9.0 · name erased; object survives if referenced

+
+
+

Errors · 72

+

Exceptions

+ TRYEXCEPTELSEFINALLY +

9.0 · try/except/else/finally lanes with traced path

+
+
+

Errors · 73

+

Assertions

+ assert condTrue · passFalse · AssertionError +

9.0 · True passes, False raises

+
+
+

Errors · 74

+

Exception Chaining

+ ValueError__cause____context__RuntimeError +

9.0 · __cause__ vs __context__ distinguished

+
+
+

Errors · 75

+

Exception Groups

+ BEFOREexcept*AFTER +

9.0 · except* peels matching leaves

+
+
+

Errors · 76

+

Warnings

+ code pathDeprecationWarningexecution continues +

9.0 · soft signal; execution continues

+
+
+

Modules · 77

+

Modules

+ SYS.PATHcwdsite-packagesstdlibfirst hitmymod.py +

9.0 · sys.path resolution; first hit wins

+
+
+

Modules · 78

+

Import Aliases

+ import numpy as npnpnumpy module +

9.0 · two names bind to the same module

+
+
+

Modules · 79

+

Packages

+ MYPACKAGE__init__.pya.pyb.pysub/ +

9.0 · __init__.py + nested submodules

+
+
+

Modules · 80

+

Virtual Environments

+ PROJECTcoderequirementsVENVpythonsite-packages +

9.0 · project / venv boundary

+
+
+

Types · 81

+

Type Hints

+ def f(x: int, y: str) -> bool: … +

9.0 · ghost annotations over runtime values

+
+
+

Types · 82

+

Runtime Type Checks

+ isinstance(x, T)TrueFalse +

9.0 · isinstance returns bool

+
+
+

Types · 83

+

Union and Optional Types

+ X: INT|STR|NONExintstrNone +

9.0 · type fork to several shapes

+
+
+

Types · 84

+

Type Aliases

+ dict[str, list[tuple[int, str]]]type Index = …Index +

9.0 · complex annotation collapses to a name

+
+
+

Types · 85

+

TypedDict

+ USER TYPEDDICTid: intname: stractive: bool +

9.0 · keys with declared value types

+
+
+

Classes · 86

+

Structured Data Shapes

+ USER TYPEDDICTid: intname: stractive: bool +

9.0 · TypedDict named keys with value types

+
+
+

Types · 87

+

Literal and Final

+ LITERAL[…]x'red''green''blue' +

9.0 · slot narrows to a fixed set

+
+
+

Types · 88

+

Callable Types

+ CALLABLE[[INT, STR], BOOL](int, str)bool +

8.5 · Callable[[A, B], R] shape; static-only

+
+
+

Types · 89

+

Generics and TypeVar

+ TFN[T]T +

9.0 · the same T flows in and out

+
+
+

Types · 90

+

ParamSpec

+ f(P)@DECP preservedwrapper(P) +

9.0 · P preserved through decorator

+
+
+

Types · 91

+

Overloads

+ @OVERLOADdef f(x: int) -> strdef f(x: str) -> intone impl +

8.5 · multiple signatures → one impl; abstract

+
+
+

Types · 92

+

Casts and Any

+ Anycast(T, x)T +

9.0 · Any → cast(T, x) → T, runtime unchanged

+
+
+

Types · 93

+

NewType

+ RUNTIME: INT42STATIC: USERIDUserId(42) +

9.0 · same runtime, distinct static identity

+
+
+

Types · 94

+

Protocols

+ OBJECTread()write()close()structuralPROTOCOLread()close() +

9.0 · structural duck check

+
+
+

Classes · 95

+

Abstract Base Classes

+ instanceCLASSClassTYPEtype +

9.0 · same triangle as concrete classes

+
+
+

Types · 96

+

Enums

+ COLOR · CLOSED SETREDGREENBLUEno more +

9.0 · closed set of symbolic values

+
+
+

Text · 97

+

Regular Expressions

+ PATTERN^\d{2}-\d{2}$INPUT12-34 +

9.0 · pattern ruler with anchors

+
+
+

Standard Library · 98

+

Number Parsing

+ "42"int()42ValueError +

9.0 · int() success path vs ValueError

+
+
+

Errors · 99

+

Custom Exceptions

+ BaseExceptionExceptionValueErrorMyDomainError +

9.0 · subclass chain to a domain name

+
+
+

Standard Library · 100

+

JSON

+ JSONPYTHONobjectdictarrayliststringstrnumberint / floattrue / falseTrue / FalsenullNone +

9.0 · two-column type mapping

+
+
+

Standard Library · 101

+

Logging

+ CRITICAL50ERROR40WARNING30INFO20DEBUG10 +

9.0 · five thresholded levels

+
+
+

Standard Library · 102

+

Testing

+ arrangeset up stateactperform behaviorassertcompare result +

9.0 · arrange-act-assert three-row pattern

+
+
+

Standard Library · 103

+

Subprocesses

+ parentspawnchild processoutput +

9.0 · spawn → child → captured output

+
+
+

Standard Library · 104

+

Threads and Processes

+ GILTHREAD ATHREAD B +

8.5 · GIL lanes; abstract concurrency model

+
+
+

Standard Library · 105

+

Networking

+ STR"ping"encodeBYTESb'ping'SOCKETBYTESb'ping'decodeSTR"ping" +

9.0 · text ↔ bytes across the socket boundary

+
+
+

Standard Library · 106

+

Dates and Times

+ one instant−5h+0h +

9.0 · one instant, two clock offsets

+
+
+

Standard Library · 107

+

CSV Data

+ ROWS · RECORDSidnamescore1Ada972Bo883Cy76 +

9.0 · rows × columns; same shape per line

+
+
+

Async · 108

+

Async Await

+ LOOPCOROawaitresume +

9.0 · loop/coro swimlane with await handoffs

+
+
+

Async · 109

+

Async Iteration and Context

+ LOOPCOROawaitresume +

9.0 · loop/coro lanes with await yields

+
+
+ + diff --git a/public/prototyping/production-figures-gestalt.html b/public/prototyping/production-figures-gestalt.html new file mode 100644 index 0000000..d27d9d6 --- /dev/null +++ b/public/prototyping/production-figures-gestalt.html @@ -0,0 +1,59 @@ + + + + +Production figures gestalt · Prototype + + + + +
Prototype · All 109 figures currently registered in src/marginalia.py FIGURES; each card names where it renders. · all prototypes
+ +
+
+

Production figure registry · 109 figures

+

Production figures gestalt

+

Every figure currently registered in src/marginalia.py FIGURES. Each card names the figure, where it renders today (an example attachment, a journey section, or "not yet attached"), and the intrinsic viewBox dimensions. Use this page beside the example-figure rubric to triage which figures are shipping, which are journey-only, and which are sitting in the registry waiting for an example attachment.

+
+

aliasing-mutation

BEFOREfirstsecond["python"]AFTER APPENDfirstsecond["python","workers"]
attached to /examples/mutability, copying-collections · viewBox 220×175

v2 scores: mutability 9.5 · copying-collections 9.5

tuple-no-mutation

TUPLE — FROZENfirstsecond("python",)NO .APPENDfirstsecond("python",)tuples raise AttributeError
registered, not yet attached · viewBox 220×185

iterator-unroll

abcdnext()abcdnext()abcdnext()abcdnext() — last
attached to /examples/for-loops · viewBox 220×130

v2 scores: for-loops 9.0

scope-rings

B · BUILT-ING · GLOBALE · ENCLOSINGL · LOCAL
attached to /examples/scope-global-nonlocal · viewBox 216×116

v2 scores: scope-global-nonlocal 9.0

closure-cell

MAKE_MULTIPLIERCELLfactor=2MULTIPLYuses cell
attached to /examples/closures · viewBox 240×120

v2 scores: closures 9.0

slice-ruler

abcdef0123456[:3][3:]
attached to /examples/slices · viewBox 232×120

v2 scores: slices 9.0

branch-fork

value?case Acase B
attached to /examples/conditionals · attached to a journey section · viewBox 232×100

v2 scores: conditionals 9.0

loop-repetition

abcdbody
attached to /examples/while-loops · attached to a journey section · viewBox 204×90

v2 scores: while-loops 9.0

iter-protocol

ITERABLE[a,b,c]iter()ITERATORnext()abc
attached to /examples/iterators, iterator-vs-iterable, iterating-over-iterables, container-protocols · attached to a journey section · viewBox 304×70

v2 scores: iterators 9.0 · iterator-vs-iterable 9.0 · iterating-over-iterables 9.0 · container-protocols 9.0

program-output

print("…")stdouthello world
attached to /examples/hello-world · attached to a journey section · viewBox 240×80

v2 scores: hello-world 9.0

identity-and-equality

IS + ==ab[1,2]== ONLYab[1,2][1,2]
attached to /examples/equality-and-identity · attached to a journey section · viewBox 304×96

v2 scores: equality-and-identity 9.0

operator-dispatch

a + bdispatchesa.__add__(b)
attached to /examples/special-methods, operator-overloading · attached to a journey section · viewBox 260×70

v2 scores: special-methods 9.0 · operator-overloading 9.0

container-questions

LIST[a,b]orderedTUPLE(a,b)fixedDICT{k:v}lookupSET{a,b}unique
attached to a journey section · viewBox 280×88

reshape-pipeline

[3,1,4]sorted[1,3,4]
attached to a journey section · viewBox 204×80

text-data-boundary

"42"TEXTparseINT42
attached to a journey section · viewBox 172×70

function-signature

argsDEF F(...)return
attached to a journey section · viewBox 188×80

function-as-value

FNdef fg = fn
attached to a journey section · viewBox 200×66

class-with-state

CLASS BOXSTATEx · yMETHODSmove(...)
attached to a journey section · viewBox 152×108

annotation-ghost

def f(x: int, y: str) -> bool: …
attached to /examples/type-hints · attached to a journey section · viewBox 220×52

v2 scores: type-hints 9.0

union-types

X: INT|STR|NONExintstrNone
attached to /examples/union-and-optional-types · attached to a journey section · viewBox 166×80

v2 scores: union-and-optional-types 9.0

generic-preservation

TFN[T]T
attached to /examples/generics-and-typevar · attached to a journey section · viewBox 250×70

v2 scores: generics-and-typevar 9.0

exception-lanes

TRYEXCEPTELSEFINALLY
attached to /examples/exceptions · attached to a journey section · viewBox 320×100

v2 scores: exceptions 9.0

context-bowtie

inbodyout
attached to /examples/context-managers · attached to a journey section · viewBox 244×76

v2 scores: context-managers 9.0

async-swimlane

LOOPCOROawaitresume
attached to /examples/async-await, async-iteration-and-context · attached to a journey section · viewBox 280×84

v2 scores: async-await 9.0 · async-iteration-and-context 9.0

naming-decisions

len(xs):=nNAMEvaluen > 10
attached to /examples/assignment-expressions · attached to a journey section · viewBox 274×80

v2 scores: assignment-expressions 9.0

early-exit

abcdefound · breakfirst match
attached to /examples/break-and-continue · attached to a journey section · viewBox 144×116

v2 scores: break-and-continue 9.0

lazy-stream

SOURCE[a,b,c]FILTERx>0MAPx*2next()
attached to /examples/generator-expressions · attached to a journey section · viewBox 300×56

v2 scores: generator-expressions 9.0

variables-bind

xINT42
attached to /examples/variables, constants · viewBox 180×44

v2 scores: variables 9.5 · constants 9.0

call-stack

factorial(3)factorial(2)factorial(1)factorial(0) ← base
attached to /examples/recursion · viewBox 200×100

v2 scores: recursion 9.0

decorator-rebind

BEFOREfFNf₀AFTER @DECfwrapperf₀
attached to /examples/decorators · viewBox 232×110

v2 scores: decorators 9.0

mro-chain

ABCDMRODBCAobject
attached to /examples/inheritance-and-super · viewBox 200×152

v2 scores: inheritance-and-super 9.0

dataclass-fields

DECLARATIONname : strage : inttags : list__init__(name, age, tags)
attached to /examples/dataclasses · viewBox 312×76

v2 scores: dataclasses 9.0

class-triangle

instanceCLASSClassTYPEtype
attached to /examples/classes, abstract-base-classes · viewBox 274×60

v2 scores: classes 9.0 · abstract-base-classes 9.0

exception-cause-context

ValueError__cause____context__RuntimeError
attached to /examples/exception-chaining · viewBox 282×70

v2 scores: exception-chaining 9.0

unpacking-bind

12345a*restb
attached to /examples/unpacking · viewBox 152×80

v2 scores: unpacking 9.0

comprehension-equivalence

[x*2 for x in xs if x > 0]out = []for x in xs: if x > 0: out.append(x*2)
attached to /examples/comprehensions, comprehension-patterns · viewBox 280×76

v2 scores: comprehensions 9.0 · comprehension-patterns 9.0

list-append

314+1.append
attached to /examples/lists · viewBox 220×36

v2 scores: lists 9.0

dict-buckets

HASH → BUCKET0"a" → 11"b" → 22"c" → 3"d" → 4collision
attached to /examples/dicts · viewBox 270×88

v2 scores: dicts 9.0

workers-portable-evidence

UNAVAILABLEmultiprocessing.Process()PORTABLE EVIDENCEvalueasserted in-process
attached to a journey section · viewBox 222×84

workers-protocol-local

REQUEST SHAPEGET /resourceRESPONSE SHAPE · ASSERTED LOCALLY200 OK · { … }
attached to a journey section · viewBox 200×110

workers-lesson-runtime

lesson questionprocess APIcaptured output
attached to a journey section · viewBox 300×80

number-lines

INT · UNBOUNDEDFLOAT · REPRESENTABLE SPACING WIDENS
attached to /examples/numbers · viewBox 260×78

v2 scores: numbers 9.0

expression-tree

*+423
attached to /examples/operators · viewBox 220×92

v2 scores: operators 9.0

none-singleton

abcNONETYPENone
attached to /examples/none · viewBox 240×84

v2 scores: none 9.0

codepoints-bytes

CODEPOINTScaféUTF-8 BYTES636166c3a9
attached to /examples/strings · viewBox 200×84

v2 scores: strings 9.0

sort-stability

INPUTSTABLE SORT BY KEY2 · Ada1 · Bo1 · Bo1 · Cy2 · Eve2 · Ada1 · Cy2 · Eve
attached to /examples/sorting · viewBox 270×100

v2 scores: sorting 9.0

kw-only-separator

def f(a, b, *, c, d): …positional or kwkeyword only
attached to /examples/keyword-only-arguments · viewBox 200×56

v2 scores: keyword-only-arguments 9.0

positional-only-separator

def f(a, b, /, c, d): …positional onlypositional or kw
attached to /examples/positional-only-parameters · viewBox 200×56

v2 scores: positional-only-parameters 9.0

generator-ribbon

PAUSED BETWEEN YIELDS · RESUMED BY NEXT()yieldyield
attached to /examples/generators · viewBox 260×50

v2 scores: generators 9.0

truth-and-size

x__bool__()__len__() != 0default: True
attached to /examples/truth-and-size · viewBox 232×70

v2 scores: truth-and-size 9.0

descriptor-protocol

obj.attrDESCRIPTOR__get____set____delete__
attached to /examples/descriptors · viewBox 222×76

v2 scores: descriptors 9.0

bound-unbound

obj.methodbound · self filledClass.methodfunction · self required
attached to /examples/bound-and-unbound-methods · viewBox 296×56

v2 scores: bound-and-unbound-methods 9.0

method-kinds

@classmethodfirst arg · Cls@staticmethodfirst arg · (none)instancefirst arg · self
attached to /examples/classmethods-and-staticmethods · viewBox 272×70

v2 scores: classmethods-and-staticmethods 9.0

callable-objects

OBJECT__call__obj(...)
attached to /examples/callable-objects · viewBox 220×44

v2 scores: callable-objects 9.0

attribute-lookup

obj.xinstance __dict__class __dict____getattr__
attached to /examples/attribute-access · viewBox 242×70

v2 scores: attribute-access 9.0

guard-clauses

if not valid: returnif missing: return Noneif special_case: return Xmain work …exit
attached to /examples/guard-clauses · viewBox 264×104

v2 scores: guard-clauses 9.0

bytes-vs-bytearray

BYTES — FROZENb'\\x63\\x61\\x66'BYTEARRAY — MUTABLEbytearray(b'\\x63\\x61').append(0x66)
attached to /examples/bytes-and-bytearray · viewBox 308×86

v2 scores: bytes-and-bytearray 9.0

sentinel-iteration

iter(read, '')valuevaluevalue''sentinel · stop
attached to /examples/sentinel-iteration · viewBox 320×92

v2 scores: sentinel-iteration 9.0

partial-functions

f(a, b, c)partial(f, 1)g(b, c)
attached to /examples/partial-functions · viewBox 334×36

v2 scores: partial-functions 9.0

args-kwargs

def f(*args, **kwargs): …→ tuple→ dict
attached to /examples/args-and-kwargs · viewBox 280×68

v2 scores: args-and-kwargs 9.0

multiple-return

def f(): return a, b(a, b)x, y
attached to /examples/multiple-return-values · viewBox 180×110

v2 scores: multiple-return-values 9.0

lambda-expression

lambda x: x + 1paramsexpression
attached to /examples/lambdas · viewBox 170×76

v2 scores: lambdas 9.0

property-fork

obj.xfget / fset__dict__
attached to /examples/properties · viewBox 232×72

v2 scores: properties 9.0

metaclass-triangle

instanceCLASSClassMETACLASStype
attached to /examples/metaclasses · viewBox 300×60

v2 scores: metaclasses 9.0

sys-path-resolution

SYS.PATHcwdsite-packagesstdlibfirst hitmymod.py
attached to /examples/modules · viewBox 258×100

v2 scores: modules 9.0

import-alias

import numpy as npnpnumpy module
attached to /examples/import-aliases · viewBox 212×56

v2 scores: import-aliases 9.0

protocol-check

OBJECTread()write()close()structuralPROTOCOLread()close()
attached to /examples/protocols · viewBox 220×78

v2 scores: protocols 9.0

enum-members

COLOR · CLOSED SETREDGREENBLUEno more
attached to /examples/enums · viewBox 280×60

v2 scores: enums 9.0

datetime-instant

one instant−5h+0h
attached to /examples/datetime · viewBox 280×88

v2 scores: datetime 9.0

json-python-mapping

JSONPYTHONobjectdictarrayliststringstrnumberint / floattrue / falseTrue / FalsenullNone
attached to /examples/json · viewBox 220×116

v2 scores: json 9.0

regex-anchors

PATTERN^\d{2}-\d{2}$INPUT12-34
attached to /examples/regular-expressions · viewBox 200×92

v2 scores: regular-expressions 9.0

number-parse

"42"int()42ValueError
attached to /examples/number-parsing · viewBox 204×64

v2 scores: number-parsing 9.0

format-spec

FORMAT SPECalignsignwidth,.prectype{:>6,.2f}
attached to /examples/string-formatting · viewBox 220×64

v2 scores: string-formatting 9.0

truthy-check

xboolTrue or FalseFALSY VALUES00.0""[]{}NoneFalse
attached to /examples/truthiness · viewBox 240×70

v2 scores: truthiness 9.0

boolean-truth-table

A AND BTFTFTFFF
attached to /examples/booleans · viewBox 132×64

v2 scores: booleans 9.0

set-buckets

KEYS ONLYabcx in sO(1)
attached to /examples/sets · viewBox 156×90

v2 scores: sets 9.0

tuple-frozen

FROZEN SEQUENCE(3, 1, 4, 1).append
attached to /examples/tuples · viewBox 280×48

v2 scores: tuples 9.0

value-types

INT42STR"hi"LIST[1,2,3]DICT{k:v}
attached to /examples/values · viewBox 160×116

v2 scores: values 9.0

yield-delegation

OUTERyield from innerINNER
attached to /examples/yield-from · viewBox 240×84

v2 scores: yield-from 9.0

itertools-chain

ITER A1 · 2ITER B3 · 4CHAIN1 · 2 · 3 · 4
attached to /examples/itertools · viewBox 246×82

v2 scores: itertools 9.0

assertion-check

assert condTrue · passFalse · AssertionError
attached to /examples/assertions · viewBox 304×76

v2 scores: assertions 9.0

custom-exception-chain

BaseExceptionExceptionValueErrorMyDomainError
attached to /examples/custom-exceptions · viewBox 220×90

v2 scores: custom-exceptions 9.0

exception-group-peel

BEFOREexcept*AFTER
attached to /examples/exception-groups · viewBox 240×50

v2 scores: exception-groups 9.0

delete-name-erased

BEFOREx[1, 2, 3]AFTER DEL Xx[1, 2, 3]
attached to /examples/delete-statements · viewBox 200×84

v2 scores: delete-statements 9.0

package-tree

MYPACKAGE__init__.pya.pyb.pysub/
attached to /examples/packages · viewBox 240×76

v2 scores: packages 9.0

venv-boundary

PROJECTcoderequirementsVENVpythonsite-packages
attached to /examples/virtual-environments · viewBox 274×76

v2 scores: virtual-environments 9.0

subprocess-spawn

parentspawnchild processoutput
attached to /examples/subprocesses · viewBox 324×60

v2 scores: subprocesses 9.0

logging-levels

CRITICAL50ERROR40WARNING30INFO20DEBUG10
attached to /examples/logging · viewBox 164×124

v2 scores: logging 9.0

aaa-pattern

arrangeset up stateactperform behaviorassertcompare result
attached to /examples/testing · viewBox 250×80

v2 scores: testing 9.0

socket-byte-boundary

STR"ping"encodeBYTESb'ping'SOCKETBYTESb'ping'decodeSTR"ping"
attached to /examples/networking · viewBox 364×46

v2 scores: networking 9.0

gil-lanes

GILTHREAD ATHREAD B
attached to /examples/threads-and-processes · viewBox 300×100

v2 scores: threads-and-processes 8.5

cast-escape

Anycast(T, x)T
attached to /examples/casts-and-any · viewBox 184×56

v2 scores: casts-and-any 9.0

newtype-phantom

RUNTIME: INT42STATIC: USERIDUserId(42)
attached to /examples/newtype · viewBox 96×92

v2 scores: newtype 9.0

overload-signatures

@OVERLOADdef f(x: int) -> strdef f(x: str) -> intone impl
attached to /examples/overloads · viewBox 304×64

v2 scores: overloads 8.5

paramspec-preserve

f(P)@DECP preservedwrapper(P)
attached to /examples/paramspec · viewBox 294×60

v2 scores: paramspec 9.0

literal-constrained

LITERAL[…]x'red''green''blue'
attached to /examples/literal-and-final · viewBox 144×76

v2 scores: literal-and-final 9.0

callable-type

CALLABLE[[INT, STR], BOOL](int, str)bool
attached to /examples/callable-types · viewBox 196×40

v2 scores: callable-types 8.5

isinstance-check

isinstance(x, T)TrueFalse
attached to /examples/runtime-type-checks · viewBox 232×76

v2 scores: runtime-type-checks 9.0

collections-containers

dequefast appends both endsCounterkey → countdefaultdictmissing key defaultnamedtupletuple with names
attached to /examples/collections-module · viewBox 284×92

v2 scores: collections-module 9.0

typed-dict-shape

USER TYPEDDICTid: intname: stractive: bool
attached to /examples/typed-dicts, structured-data-shapes · viewBox 200×92

v2 scores: typed-dicts 9.0 · structured-data-shapes 9.0

csv-records

ROWS · RECORDSidnamescore1Ada972Bo883Cy76
attached to /examples/csv-data · viewBox 212×96

v2 scores: csv-data 9.0

warning-signal

code pathDeprecationWarningexecution continues
attached to /examples/warnings · viewBox 292×80

v2 scores: warnings 9.0

object-lifecycle

__init__live · refcount > 0__del__
attached to /examples/object-lifecycle · viewBox 366×60

v2 scores: object-lifecycle 9.0

type-alias-name

dict[str, list[tuple[int, str]]]type Index = …Index
attached to /examples/type-aliases · viewBox 240×104

v2 scores: type-aliases 9.0

match-dispatch-ladder

match valuecase 0:case [x, y]:case Point(0, _):case _:first match
attached to /examples/match-statements · viewBox 260×130

v2 scores: match-statements 9.0

match-pattern-variants

capture[x, y]alternativeP() | Q()guard[x] if x > 0classPoint(x=0, y=_)
attached to /examples/advanced-match-patterns · viewBox 272×96

v2 scores: advanced-match-patterns 9.0

loop-else-gate

loop bodyfell through · else runsbroke · else skipped
attached to /examples/loop-else · viewBox 312×76

v2 scores: loop-else 9.0

literal-forms

int42 · 0x2a · 0b101float3.14 · 1e-3str"hi" · 'hi'list[1, 2, 3]dict{k: v}set{1, 2, 3}
attached to /examples/literals · viewBox 252×132

v2 scores: literals 9.0

function-with-body

nameDEF GREET(NAME):"Hello, " + name"Hello, Ada"
attached to /examples/functions · viewBox 334×68

v2 scores: functions 9.0

+
+ + + diff --git a/public/site.5f6f7da7c305.css b/public/site.1452cc5609f2.css similarity index 83% rename from public/site.5f6f7da7c305.css rename to public/site.1452cc5609f2.css index 4538e5c..588fc11 100644 --- a/public/site.5f6f7da7c305.css +++ b/public/site.1452cc5609f2.css @@ -1,4 +1,4 @@ -:root { color-scheme: light; --accent: #FF4801; --accent-hover: #FF7038; --accent-soft: rgba(255, 72, 1, 0.08); --text: #521000; --muted: rgba(82, 16, 0, 0.7); --subtle: rgba(82, 16, 0, 0.4); --page: #F5F1EB; --surface: #FFFBF5; --surface-2: #FFFDFB; --surface-3: #FEF7ED; --hairline: #EBD5C1; --hairline-soft: rgba(235, 213, 193, 0.5); --space-1: .5rem; --space-2: .75rem; --space-3: 1rem; --space-4: 1.5rem; --space-5: 2rem; --space-6: 3rem; } +:root { color-scheme: light; --accent: #FF4801; --accent-hover: #FF7038; --accent-soft: rgba(255, 72, 1, 0.08); --text: #521000; --muted: rgba(82, 16, 0, 0.7); --page: #F5F1EB; --surface: #FFFBF5; --surface-2: #FFFDFB; --surface-3: #FEF7ED; --hairline: #EBD5C1; --hairline-soft: rgba(235, 213, 193, 0.5); --space-1: .5rem; --space-2: .75rem; --space-3: 1rem; --space-4: 1.5rem; --space-5: 2rem; --space-6: 3rem; } * { box-sizing: border-box; } html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } body { max-width: 1040px; margin: 0 auto; padding: var(--space-4); color: var(--text); font: 16px/1.6 FT Kunst Grotesk, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; background: radial-gradient(circle at top left, rgba(255, 72, 1, 0.10), transparent 34rem), var(--page); } @@ -8,7 +8,7 @@ nav a:hover { color: var(--accent); text-decoration-color: var(--accent); } .button:active { transform: scale(0.96); } h1, h2 { letter-spacing: -0.04em; line-height: 1.05; text-wrap: balance; } - h1 { font-size: clamp(2.4rem, 7vw, 5.75rem); margin: 0 0 1rem; } + h1 { font-size: clamp(2.4rem, 4.5vw, 3.75rem); margin: 0 0 1rem; } h2 { margin-top: 0; } p, li { text-wrap: pretty; } pre { overflow: auto; padding: 1rem; border-radius: 1rem; background: #0b1020; color: #f9fafb; box-shadow: 0 1px 1px rgba(0,0,0,.12), 0 12px 42px rgba(0,0,0,.18); } @@ -66,13 +66,20 @@ .lp-prose p:last-child { margin-bottom: 0; } .cell-code-stack { min-width: 0; padding-left: var(--space-3); border-left: 2px solid var(--accent); } .cell-code-stack code { white-space: pre-wrap; } - .cell-source pre, .cell-source .shiki-block, .cell-output pre { margin: 0; padding: 0; border-left: 0; border-radius: 0; background: transparent !important; color: var(--text); box-shadow: none; white-space: pre-wrap; overflow-x: visible; } + .cell-source pre, .cell-source .shiki-block, .cell-output pre { margin: 0; padding: 0; border-left: 0; border-radius: 0; background: transparent !important; color: var(--text); box-shadow: none; white-space: pre-wrap; overflow-wrap: anywhere; overflow-x: visible; } .cell-source .shiki-block .line { display: inline; } .cell-output { margin-top: var(--space-3); padding: var(--space-3) 0 0; border-top: 1px solid var(--hairline-soft); background: transparent; } .unsupported-cell .cell-code-stack { border-left-style: dashed; } .notebook-notes { margin-top: var(--space-5); } - @media (max-width: 980px) { .lp-cell { grid-template-columns: 1fr; } .cell-output { max-width: none; } } + @media (max-width: 780px) { + .lp-cell, .lesson-step, .runner-grid { grid-template-columns: 1fr; } + .lp-cell .cell-code-stack { max-width: 72ch; } + .cell-output { max-width: none; } + body { padding: .875rem; } + header { margin-inline: -.875rem; padding-inline: .875rem; } + } .playground { margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--hairline); } + .playground > h2 { font-size: clamp(1.5rem, 2.5vw, 2rem); letter-spacing: -0.03em; margin-bottom: var(--space-3); } .runner-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(18rem, .75fr); gap: var(--space-4); align-items: stretch; } .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } @@ -91,6 +98,9 @@ .journey-section { margin-block: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--hairline); } .journey-section h2 { margin-bottom: var(--space-2); } .journey-section > .meta { max-width: 58ch; } + .journey-section-figure { margin: var(--space-4) auto; padding: 0; width: 100%; max-width: clamp(280px, 70vw, 640px); } + .journey-section-figure svg { max-width: 100%; height: auto; display: block; } + .journey-section-figure figcaption { margin-top: var(--space-2); color: var(--muted); font-size: .92rem; font-style: italic; max-width: 44ch; } .journey-list { display: grid; gap: 0; margin-top: var(--space-3); padding-left: 0; list-style: none; } .journey-list li { position: relative; margin-left: .45rem; padding: var(--space-3) 0 var(--space-3) var(--space-4); border-left: 2px solid var(--hairline); } .journey-list li::before { content: ""; position: absolute; left: -.42rem; top: 1.45rem; width: .7rem; height: .7rem; border: 1px solid var(--accent); border-radius: 999px; background: radial-gradient(circle, var(--accent) 0 .16rem, var(--surface) .17rem); box-shadow: 0 0 0 5px var(--page); } @@ -99,9 +109,21 @@ .journey-item-title { font-weight: 750; } .journey-gap, .journey-gap-label { color: var(--muted); font-weight: 750; } .journey-gap-label { margin: 0; color: var(--muted); font-size: .86rem; letter-spacing: 0; text-transform: none; } + .notes-list { margin: 0 0 var(--space-5); padding-left: 1.2rem; color: var(--muted); max-width: 58ch; } + .notes-list li { margin-bottom: var(--space-1); text-wrap: pretty; } .example-top, .example-nav { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; } .example-nav { margin-top: var(--space-5); padding-top: var(--space-3); border-top: 1px solid var(--hairline); } footer { margin-block: 2rem; color: var(--muted); } .site-footer-note { font-size: .82rem; } - @media (max-width: 860px) { .lesson-step, .runner-grid { grid-template-columns: 1fr; } body { padding: .875rem; } header { margin-inline: -.875rem; padding-inline: .875rem; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 1ms !important; } } + /* Cell banner: a figure attached to a cell renders in a banner row + AFTER that cell, spanning the full content width. Cells always + keep their prose|code 2-column grid; banners between cells hold + one or many figures (small multiples). See docs/visual-explainer-spec.md. */ + .cell-banner { margin: var(--space-5) 0; padding: var(--space-4) 0; border-block: 1px dashed var(--hairline-soft); display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--space-4); justify-items: center; } + .cell-banner figure { margin: 0; padding: 0; width: 100%; max-width: clamp(240px, 45vw, 480px); } + .cell-banner svg { max-width: 100%; height: auto; display: block; } + .cell-banner figcaption { margin-top: var(--space-2); color: var(--muted); font-size: .92rem; font-style: italic; max-width: 44ch; } + .cell-banner--1 { margin-block: var(--space-6); } + .cell-banner--1 figure { width: 100%; max-width: clamp(280px, 65vw, 640px); } + .cell-banner--1 figcaption { max-width: 42ch; } diff --git a/public/site.css b/public/site.css index 4538e5c..588fc11 100644 --- a/public/site.css +++ b/public/site.css @@ -1,4 +1,4 @@ -:root { color-scheme: light; --accent: #FF4801; --accent-hover: #FF7038; --accent-soft: rgba(255, 72, 1, 0.08); --text: #521000; --muted: rgba(82, 16, 0, 0.7); --subtle: rgba(82, 16, 0, 0.4); --page: #F5F1EB; --surface: #FFFBF5; --surface-2: #FFFDFB; --surface-3: #FEF7ED; --hairline: #EBD5C1; --hairline-soft: rgba(235, 213, 193, 0.5); --space-1: .5rem; --space-2: .75rem; --space-3: 1rem; --space-4: 1.5rem; --space-5: 2rem; --space-6: 3rem; } +:root { color-scheme: light; --accent: #FF4801; --accent-hover: #FF7038; --accent-soft: rgba(255, 72, 1, 0.08); --text: #521000; --muted: rgba(82, 16, 0, 0.7); --page: #F5F1EB; --surface: #FFFBF5; --surface-2: #FFFDFB; --surface-3: #FEF7ED; --hairline: #EBD5C1; --hairline-soft: rgba(235, 213, 193, 0.5); --space-1: .5rem; --space-2: .75rem; --space-3: 1rem; --space-4: 1.5rem; --space-5: 2rem; --space-6: 3rem; } * { box-sizing: border-box; } html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } body { max-width: 1040px; margin: 0 auto; padding: var(--space-4); color: var(--text); font: 16px/1.6 FT Kunst Grotesk, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; background: radial-gradient(circle at top left, rgba(255, 72, 1, 0.10), transparent 34rem), var(--page); } @@ -8,7 +8,7 @@ nav a:hover { color: var(--accent); text-decoration-color: var(--accent); } .button:active { transform: scale(0.96); } h1, h2 { letter-spacing: -0.04em; line-height: 1.05; text-wrap: balance; } - h1 { font-size: clamp(2.4rem, 7vw, 5.75rem); margin: 0 0 1rem; } + h1 { font-size: clamp(2.4rem, 4.5vw, 3.75rem); margin: 0 0 1rem; } h2 { margin-top: 0; } p, li { text-wrap: pretty; } pre { overflow: auto; padding: 1rem; border-radius: 1rem; background: #0b1020; color: #f9fafb; box-shadow: 0 1px 1px rgba(0,0,0,.12), 0 12px 42px rgba(0,0,0,.18); } @@ -66,13 +66,20 @@ .lp-prose p:last-child { margin-bottom: 0; } .cell-code-stack { min-width: 0; padding-left: var(--space-3); border-left: 2px solid var(--accent); } .cell-code-stack code { white-space: pre-wrap; } - .cell-source pre, .cell-source .shiki-block, .cell-output pre { margin: 0; padding: 0; border-left: 0; border-radius: 0; background: transparent !important; color: var(--text); box-shadow: none; white-space: pre-wrap; overflow-x: visible; } + .cell-source pre, .cell-source .shiki-block, .cell-output pre { margin: 0; padding: 0; border-left: 0; border-radius: 0; background: transparent !important; color: var(--text); box-shadow: none; white-space: pre-wrap; overflow-wrap: anywhere; overflow-x: visible; } .cell-source .shiki-block .line { display: inline; } .cell-output { margin-top: var(--space-3); padding: var(--space-3) 0 0; border-top: 1px solid var(--hairline-soft); background: transparent; } .unsupported-cell .cell-code-stack { border-left-style: dashed; } .notebook-notes { margin-top: var(--space-5); } - @media (max-width: 980px) { .lp-cell { grid-template-columns: 1fr; } .cell-output { max-width: none; } } + @media (max-width: 780px) { + .lp-cell, .lesson-step, .runner-grid { grid-template-columns: 1fr; } + .lp-cell .cell-code-stack { max-width: 72ch; } + .cell-output { max-width: none; } + body { padding: .875rem; } + header { margin-inline: -.875rem; padding-inline: .875rem; } + } .playground { margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--hairline); } + .playground > h2 { font-size: clamp(1.5rem, 2.5vw, 2rem); letter-spacing: -0.03em; margin-bottom: var(--space-3); } .runner-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(18rem, .75fr); gap: var(--space-4); align-items: stretch; } .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } @@ -91,6 +98,9 @@ .journey-section { margin-block: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--hairline); } .journey-section h2 { margin-bottom: var(--space-2); } .journey-section > .meta { max-width: 58ch; } + .journey-section-figure { margin: var(--space-4) auto; padding: 0; width: 100%; max-width: clamp(280px, 70vw, 640px); } + .journey-section-figure svg { max-width: 100%; height: auto; display: block; } + .journey-section-figure figcaption { margin-top: var(--space-2); color: var(--muted); font-size: .92rem; font-style: italic; max-width: 44ch; } .journey-list { display: grid; gap: 0; margin-top: var(--space-3); padding-left: 0; list-style: none; } .journey-list li { position: relative; margin-left: .45rem; padding: var(--space-3) 0 var(--space-3) var(--space-4); border-left: 2px solid var(--hairline); } .journey-list li::before { content: ""; position: absolute; left: -.42rem; top: 1.45rem; width: .7rem; height: .7rem; border: 1px solid var(--accent); border-radius: 999px; background: radial-gradient(circle, var(--accent) 0 .16rem, var(--surface) .17rem); box-shadow: 0 0 0 5px var(--page); } @@ -99,9 +109,21 @@ .journey-item-title { font-weight: 750; } .journey-gap, .journey-gap-label { color: var(--muted); font-weight: 750; } .journey-gap-label { margin: 0; color: var(--muted); font-size: .86rem; letter-spacing: 0; text-transform: none; } + .notes-list { margin: 0 0 var(--space-5); padding-left: 1.2rem; color: var(--muted); max-width: 58ch; } + .notes-list li { margin-bottom: var(--space-1); text-wrap: pretty; } .example-top, .example-nav { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; } .example-nav { margin-top: var(--space-5); padding-top: var(--space-3); border-top: 1px solid var(--hairline); } footer { margin-block: 2rem; color: var(--muted); } .site-footer-note { font-size: .82rem; } - @media (max-width: 860px) { .lesson-step, .runner-grid { grid-template-columns: 1fr; } body { padding: .875rem; } header { margin-inline: -.875rem; padding-inline: .875rem; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 1ms !important; } } + /* Cell banner: a figure attached to a cell renders in a banner row + AFTER that cell, spanning the full content width. Cells always + keep their prose|code 2-column grid; banners between cells hold + one or many figures (small multiples). See docs/visual-explainer-spec.md. */ + .cell-banner { margin: var(--space-5) 0; padding: var(--space-4) 0; border-block: 1px dashed var(--hairline-soft); display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--space-4); justify-items: center; } + .cell-banner figure { margin: 0; padding: 0; width: 100%; max-width: clamp(240px, 45vw, 480px); } + .cell-banner svg { max-width: 100%; height: auto; display: block; } + .cell-banner figcaption { margin-top: var(--space-2); color: var(--muted); font-size: .92rem; font-style: italic; max-width: 44ch; } + .cell-banner--1 { margin-block: var(--space-6); } + .cell-banner--1 figure { width: 100%; max-width: clamp(280px, 65vw, 640px); } + .cell-banner--1 figcaption { max-width: 42ch; } diff --git a/scripts/build_marginalia.py b/scripts/build_marginalia.py new file mode 100644 index 0000000..00f8e63 --- /dev/null +++ b/scripts/build_marginalia.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Generate public/prototyping/marginalia-gestalt.html — every attached +production figure rendered as a small card so the whole library can be +reviewed at a glance. + +The gestalt is a thin view over `src/marginalia.py`: every card pulls +its paint function, dimensions, score, and commentary directly from +the production registries (FIGURES, ATTACHMENTS, SCORES). No bespoke +paint code lives here — drift between "what readers see on +/examples/X" and "what reviewers see on the gestalt page" is +structurally impossible. + +(Journey overview thumbnails are not rendered on this page; the +per-section journey figures are reviewed on +journey-figures-gestalt.html instead.) +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "src")) + +from marginalia import ATTACHMENTS, FIGURES, SCORES # noqa: E402 (sys.path set above) +from marginalia_grammar import Card # noqa: E402 +from example_loader import load_examples # noqa: E402 + +OUT = ROOT / "public" / "prototyping" / "marginalia-gestalt.html" + + +def _example_cards() -> list[Card]: + """One Card per example slug that has a production attachment. + + The paint function, intrinsic width, and intrinsic height all come + from FIGURES[ATTACHMENTS[slug][0][1]]. Slugs without attachments + (or without scores) are skipped. + """ + _, examples = load_examples() + cards: list[Card] = [] + for i, ex in enumerate(examples, start=1): + slug = ex["slug"] + attachments = ATTACHMENTS.get(slug) + if not attachments: + continue + # Single-figure banner per slug today; future multi-figure + # banners would extend this to one card per figure. + _, figure_name, _caption = attachments[0] + paint, w, h = FIGURES[figure_name] + card = Card( + slug=slug, + title=ex["title"], + section=ex["section"], + order=i, + figure=paint, + width=w, + height=h, + ) + score = SCORES.get(slug) + if score is not None: + card.score, card.score_note = score + cards.append(card) + return cards + + +EXAMPLES: list[Card] = _example_cards() + + +# ─── Page scaffold ───────────────────────────────────────────────────── + + +HEAD = """ + + + +Marginalia gestalt — Python By Example + + + +
+

Marginalia gestalt

+

Every example figure rendered from the same paint code that ships on /examples/<slug>.

+

Locked metrics, locked palette, locked typography. Cards compose words; words compose tokens; nothing is bespoke.

+
+""" + + +def render() -> str: + out = [HEAD] + out.append('

Examples

\n
') + for card in EXAMPLES: + out.append(card.render_html()) + out.append("
\n\n\n") + return "\n".join(out) + + +def main() -> None: + OUT.write_text(render()) + print(f"wrote {OUT.relative_to(ROOT)} — {len(EXAMPLES)} examples (from production FIGURES)") + + +if __name__ == "__main__": + main() diff --git a/scripts/build_prototypes.py b/scripts/build_prototypes.py new file mode 100644 index 0000000..2406792 --- /dev/null +++ b/scripts/build_prototypes.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python3 +"""Generate exploratory prototypes under public/prototyping/. + +The canonical layout is "figure between prose and code", with the cell +dropping to single-column when a figure is attached. These prototypes +demonstrate that layout on representative examples and journeys, plus +keep the marginalia-gestalt and operators-comparison review pages. +""" + +from __future__ import annotations + +import html +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "src")) + +from app import ( # noqa: E402 (sys.path set above) + JOURNEYS_BY_SLUG, + _walkthrough_cells, + get_example, + render_inline, +) +from marginalia import SECTION_FIGURES as JOURNEY_SECTION_FIGURES # noqa: E402 +from marginalia import _render_svg # noqa: E402 + +OUT_DIR = ROOT / "public" / "prototyping" + + +# ─── Page scaffolding ────────────────────────────────────────────────── + + +def page(title: str, banner: str, style_extras: str, body: str) -> str: + return f""" + + + +{html.escape(title)} · Prototype + + + + +
Prototype · {banner} · all prototypes
+{body} + + +""" + + +def render_cell(step: dict) -> str: + """Render one literate cell — always prose | code in a 2-column grid. + + Figures live in banner rows between cells, never inside them. + """ + prose_html = "".join(f"

{render_inline(p)}

" for p in step["prose"]) + code = html.escape(step["code"]) + output = html.escape(step["output"]) + code_stack = ( + '
' + f'

Source

{code}
' + f'

Output

{output}
' + "
" + ) + return f'
{prose_html}
{code_stack}
' + + +def render_article(example: dict, *, banners: dict[str, str] | None = None) -> str: + """Render a full example article with optional banner rows between cells. + + banners keys: "before" | "after-cell-N" | "after-walkthrough". + Each value is the HTML of one banner (use the banner() helper). + """ + cells = _walkthrough_cells(example) + banners = banners or {} + parts: list[str] = [] + if "before" in banners: + parts.append(banners["before"]) + for i, step in enumerate(cells): + parts.append(render_cell(step)) + key = f"after-cell-{i}" + if key in banners: + parts.append(banners[key]) + if "after-walkthrough" in banners: + parts.append(banners["after-walkthrough"]) + walkthrough = "".join(parts) + notes = "".join(f"
  • {render_inline(n)}
  • " for n in example.get("notes", [])) + code = html.escape(example["code"]) + output = html.escape(example.get("expected_output", "")) + return f""" +
    + +
    +

    {html.escape(example['section'])}

    +

    {html.escape(example['title'])}

    +

    {html.escape(example['summary'])}

    +
    +
    {walkthrough}
    +

    Notes

    +
      {notes}
    +
    +

    Run the complete example

    +
    +
    +

    Example code

    +
    {code}
    +
    +

    Expected output

    {output}
    +
    +
    +
    +""" + + +def banner(*items: tuple[str, str | None]) -> str: + """A banner row holding 1+ figures spanning the full cell width. + + items: tuples of (figure_name, caption_or_None). The banner uses an + auto-fit grid so a single figure centers, two pair as small multiples, + three or more wrap as the cell allows. + """ + figures_html = "".join( + f'
    {_render_svg(name)}' + f'{("
    " + html.escape(cap) + "
    ") if cap else ""}' + "
    " + for name, cap in items + ) + count_class = f" cell-banner--{len(items)}" + return f'
    {figures_html}
    ' + + +# ─── Index ───────────────────────────────────────────────────────────── + + +PROTOTYPES = [ + ("marginalia-gestalt.html", "Marginalia gestalt", + "Every journey and example as a card, drawn from the shared grammar. Pure design review."), + ("journey-figures-gestalt.html", "Journey-figures gestalt", + "All journey section figures on one page, grouped by journey, for uniform rubric review."), + ("production-figures-gestalt.html", "Production figures gestalt", + "Every figure currently registered in src/marginalia.py FIGURES, with a tag showing where it renders (example attachment, journey section, or unattached)."), + ("layout-banner-single.html", "Layout · banner between cells", + "The grammar: cells stay 2-column always; figures live in banner rows BETWEEN cells. Holds one figure here. The intended union of Tufte/Knuth/algebrica."), + ("layout-banner-pair.html", "Layout · banner with small-multiples pair", + "Same grammar with two figures in the banner — a Tufte small-multiple. The mutable list and the immutable tuple side by side, captioned, between the same pair of cells."), + ("layout-banner-trio.html", "Layout · multiple banners across the walkthrough", + "The grammar at scale: a single-figure banner before the walkthrough, a pair-banner between two cells, a single-figure summary after the last cell. Multiple diagrams; cells never displaced."), +] + + +def build_index() -> None: + items = "".join( + f'
  • {html.escape(title)}

    {html.escape(desc)}

  • ' + for (slug, title, desc) in PROTOTYPES + ) + body = f""" +
    +
    +

    Prototypes · cache: no-cache, must-revalidate

    +

    Visual explainer prototypes

    +

    Real example pages with their attached figures, plus the design-review pages. The example pages all use the production layout: a cell with an attached figure stacks prose, figure, and code vertically; cells without figures keep today's prose|code grid.

    +
    +
      {items}
    +
    +""" + style = """ + .prototype-list { list-style: none; padding: 0; margin: var(--space-4) 0 0; } + .prototype-list li { padding: var(--space-3) 0; border-bottom: 1px dashed var(--hairline-soft); } + .prototype-list li:first-child { border-top: 1px dashed var(--hairline-soft); } + .prototype-list strong { font-weight: 600; font-size: 1.05rem; } + .prototype-list .meta { margin: .25rem 0 0; max-width: 60ch; } +""" + (OUT_DIR / "index.html").write_text( + page("Visual explainer prototypes", "All prototypes", style, body) + ) + + +# ─── Banner CSS (lives between cells, never inside) ─────────────────── + + +BANNER_CSS = """ + /* Banner rows live BETWEEN cells, never inside them. The cell keeps + its prose|code 2-column grid intact; the banner spans the full + content width and holds 1+ figures via an auto-fit grid. Generous + vertical rhythm marks each banner as a teaching pause between + cells (Tufte's small-multiples, Knuth's interleaved literate + prose, algebrica's quiet figure+caption pairing). */ + .cell-banner { + margin: var(--space-5) 0; + padding: var(--space-4) 0; + border-block: 1px dashed var(--hairline-soft); + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: var(--space-4); + justify-items: center; + } + .cell-banner figure { margin: 0; padding: 0; max-width: 360px; } + .cell-banner svg { max-width: 100%; height: auto; display: block; } + .cell-banner figcaption { + margin-top: var(--space-2); + color: var(--muted); + font-size: .92rem; + font-style: italic; + max-width: 44ch; + } + /* Single figure: centered, generous breathing room. */ + .cell-banner--1 figure { max-width: 440px; } +""" + + + +# ─── Journey-figures gestalt (all 18 section figures on one page) ────── + + +JOURNEY_FIGURES_GESTALT_STYLE = """ + .journey-block { margin-block: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--hairline); } + .journey-block:first-of-type { border-top: 0; padding-top: 0; } + .journey-block h2 { margin: 0 0 var(--space-2); } + .journey-block .meta { max-width: 64ch; color: var(--muted); margin: 0 0 var(--space-4); } + .section-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: var(--space-4) var(--space-4); + } + .section-grid figure { margin: 0; padding: 0; } + .section-grid h3 { + font-size: 1rem; font-weight: 600; letter-spacing: -0.005em; + margin: 0 0 var(--space-2); color: var(--text); + } + .section-grid svg { max-width: min(100%, 320px); height: auto; display: block; } + .section-grid figcaption { + margin-top: var(--space-2); color: var(--muted); + font-size: .9rem; font-style: italic; max-width: 44ch; + } + .section-grid figure .score-line { + margin: var(--space-1) 0 0; color: var(--muted); + font-size: .82rem; font-family: -apple-system, 'Source Sans Pro', sans-serif; + } +""" + + +def build_journey_figures_gestalt() -> None: + """One page showing every journey section's figure, grouped by journey. + + Reviewers can see all 18 section figures at once to spot drift and + apply the rubric uniformly (see docs/journey-visualisation-rubric.md). + """ + blocks: list[str] = [] + for slug in ( + "runtime", + "control-flow", + "iteration", + "shapes", + "interfaces", + "types", + "reliability", + "workers", + ): + journey = JOURNEYS_BY_SLUG[slug] + cards: list[str] = [] + for section in journey["sections"]: + entry = JOURNEY_SECTION_FIGURES.get(section["title"]) + if entry is None: + continue + fig_name, caption = entry + cards.append( + f"
    " + f'

    {html.escape(section["title"])}

    ' + f"{_render_svg(fig_name)}" + f"
    {html.escape(caption)}
    " + f"
    " + ) + blocks.append( + f'
    ' + f'

    {html.escape(journey["title"])}

    ' + f'

    {html.escape(journey["summary"])}

    ' + f'
    {"".join(cards)}
    ' + f"
    " + ) + body = f""" +
    +
    +

    Journeys · 18 section figures

    +

    Journey-figures gestalt

    +

    Every journey section's figure on one page so the set can be reasoned about as a whole. Score against the rubric; redesign anything below the 8.5 gate before shipping to /journeys/<slug>.

    +
    + {''.join(blocks)} +
    +""" + (OUT_DIR / "journey-figures-gestalt.html").write_text( + page( + "Journey-figures gestalt", + "All 18 journey section figures grouped by journey, for uniform rubric review.", + JOURNEY_FIGURES_GESTALT_STYLE, + body, + ) + ) + + +def build_production_figures_gestalt() -> None: + """One page showing exactly what is registered in src/marginalia.py FIGURES. + + Distinct from marginalia-gestalt (which renders the design-only catalogue + in scripts/build_marginalia.py) and journey-figures-gestalt (which only + renders figures attached to journey sections). This page makes the + ship-vs-design gap visible: any figure shown here is wired through to + production attachments OR available for attachment. + """ + from marginalia import ATTACHMENTS, FIGURES, SCORES # noqa: PLC0415 + + # Build a slug→figure_names index of attached figures so we can mark + # figures that already render somewhere on a real page. + attached_to_slug: dict[str, list[str]] = {} + for slug, attachments in ATTACHMENTS.items(): + for _, fig_name, _ in attachments: + attached_to_slug.setdefault(fig_name, []).append(slug) + journey_section_figs = {n for n, _ in JOURNEY_SECTION_FIGURES.values()} + + def score_summary(slugs: list[str]) -> str: + scores = [SCORES.get(s) for s in slugs] + present = [(s, sc) for s, sc in zip(slugs, scores) if sc is not None] + if not present: + return "" + pieces = [f"{s} {score:.1f}" for s, (score, _note) in present] + return " · ".join(pieces) + + cards: list[str] = [] + for name, (_, w, h) in FIGURES.items(): + kind: list[str] = [] + if name in attached_to_slug: + slugs = ", ".join(attached_to_slug[name]) + kind.append(f"attached to /examples/{slugs}") + if name in journey_section_figs: + kind.append("attached to a journey section") + if not kind: + kind.append("registered, not yet attached") + kind_html = " · ".join(html.escape(k) for k in kind) + score_html = "" + if name in attached_to_slug: + summary = score_summary(attached_to_slug[name]) + if summary: + score_html = f'

    v2 scores: {html.escape(summary)}

    ' + cards.append( + f"
    " + f'

    {html.escape(name)}

    ' + f"{_render_svg(name)}" + f'
    {kind_html} · viewBox {w}×{h}
    ' + f"{score_html}" + f"
    " + ) + body = f""" +
    +
    +

    Production figure registry · {len(FIGURES)} figures

    +

    Production figures gestalt

    +

    Every figure currently registered in src/marginalia.py FIGURES. Each card names the figure, where it renders today (an example attachment, a journey section, or "not yet attached"), and the intrinsic viewBox dimensions. Use this page beside the example-figure rubric to triage which figures are shipping, which are journey-only, and which are sitting in the registry waiting for an example attachment.

    +
    +
    {"".join(cards)}
    +
    +""" + (OUT_DIR / "production-figures-gestalt.html").write_text( + page( + "Production figures gestalt", + f"All {len(FIGURES)} figures currently registered in src/marginalia.py FIGURES; each card names where it renders.", + JOURNEY_FIGURES_GESTALT_STYLE, + body, + ) + ) + + +def main() -> None: + OUT_DIR.mkdir(parents=True, exist_ok=True) + build_index() + # ─── Banner-between grammar ───────────────────────────────────── + # Cells stay 2-column; banners between cells hold 1+ figures. + aliasing_caption = ( + "Two names share one mutable list — appending through one name " + "changes the object visible through both." + ) + tuple_caption = ( + "By contrast, a tuple is frozen — its contents cannot change in " + "place, so aliasing carries no mutation hazard." + ) + ex_mut = get_example("mutability") + + # 1) one banner, one figure between cells 0 and 1 + body = render_article( + ex_mut, + banners={"after-cell-0": banner(("aliasing-mutation", aliasing_caption))}, + ) + (OUT_DIR / "layout-banner-single.html").write_text( + page( + "Mutability · banner between cells (single figure)", + "Cells keep their prose|code grid; one figure sits in a banner row between cell 0 and cell 1.", + BANNER_CSS, + body, + ) + ) + + # 2) one banner, two figures (small multiples) between cells 0 and 1 + body = render_article( + ex_mut, + banners={ + "after-cell-0": banner( + ("aliasing-mutation", aliasing_caption), + ("tuple-no-mutation", tuple_caption), + ) + }, + ) + (OUT_DIR / "layout-banner-pair.html").write_text( + page( + "Mutability · banner with paired small-multiples", + "One banner with two figures — list mutates, tuple does not. Same grammar, just more figures in the slot.", + BANNER_CSS, + body, + ) + ) + + # 3) multiple banners across the walkthrough + body = render_article( + ex_mut, + banners={ + "before": banner( + ("aliasing-mutation", aliasing_caption), + ), + "after-cell-1": banner( + ("aliasing-mutation", "Mutable: change visible through any alias."), + ("tuple-no-mutation", "Immutable: aliases share a frozen value."), + ), + "after-walkthrough": banner( + ("tuple-no-mutation", "When in doubt, reach for the immutable shape."), + ), + }, + ) + (OUT_DIR / "layout-banner-trio.html").write_text( + page( + "Mutability · banners across the whole walkthrough", + "Demonstrates that the grammar accepts multiple banners at any position: lead-in figure, mid-walkthrough small-multiple pair, summary figure. Cells never reflow.", + BANNER_CSS, + body, + ) + ) + + build_journey_figures_gestalt() + build_production_figures_gestalt() + written = sorted(p.name for p in OUT_DIR.iterdir() if p.is_file() and p.suffix == ".html") + print(f"wrote {len(written)} files to {OUT_DIR.relative_to(ROOT)}/:") + for name in written: + print(f" {name}") + + +if __name__ == "__main__": + main() diff --git a/scripts/fingerprint_assets.py b/scripts/fingerprint_assets.py index b5d1099..6beb53e 100755 --- a/scripts/fingerprint_assets.py +++ b/scripts/fingerprint_assets.py @@ -41,6 +41,8 @@ def html_version(paths: dict[str, str]) -> str: ROOT / "src" / "examples.py", ROOT / "src" / "example_loader.py", ROOT / "src" / "example_sources_data.py", + ROOT / "src" / "marginalia.py", + ROOT / "src" / "marginalia_grammar.py", ROOT / "src" / "example_sources" / "manifest.toml", *sorted((ROOT / "src" / "example_sources").glob("*.md")), *sorted((ROOT / "src" / "templates").glob("*.html")), @@ -67,7 +69,9 @@ def main() -> None: "/editor.*.js\n" " Cache-Control: public, max-age=31536000, immutable\n\n" "/favicon.svg\n" - " Cache-Control: public, max-age=31536000, immutable\n" + " Cache-Control: public, max-age=31536000, immutable\n\n" + "/prototyping/*\n" + " Cache-Control: no-cache, must-revalidate\n" ) for name, path in paths.items(): print(f"{name}={path}") diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh new file mode 100755 index 0000000..43bc33c --- /dev/null +++ b/scripts/install-git-hooks.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# One-time local git config so merges and rebases regenerate the asset +# manifest automatically instead of producing conflict markers. +# +# * `merge.ours.driver = true` lets `.gitattributes`' `merge=ours` +# resolve `src/asset_manifest.py` conflicts by keeping our side. +# * `core.hooksPath = .githooks` activates the post-merge and +# post-rewrite hooks that re-run `scripts/fingerprint_assets.py` +# after the merge or rebase finishes. +# +# Both settings are local-only; nothing in this script touches the +# remote or shared config. +set -e +cd "$(git rev-parse --show-toplevel)" +git config merge.ours.driver true +git config core.hooksPath .githooks +chmod +x .githooks/post-merge .githooks/post-rewrite +echo "git hooks installed: merge.ours.driver=true, core.hooksPath=.githooks" diff --git a/scripts/score_examples.py b/scripts/score_examples.py deleted file mode 100755 index 1bb5892..0000000 --- a/scripts/score_examples.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env python3 -"""Score Python By Example and sampled Go/Rust By Example pages against the rubric.""" -from __future__ import annotations - -import ast -import contextlib -import html -import io -import re -import sys -import urllib.request -from dataclasses import dataclass -from pathlib import Path -from statistics import mean - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) -from src.examples import EXAMPLES # noqa: E402 - -APP_SOURCE = (ROOT / "src" / "app.py").read_text() -STYLE_SOURCE = (ROOT / "public" / "site.css").read_text() -PROJECT_SURFACE = APP_SOURCE + "\n" + STYLE_SOURCE - -GOBYEXAMPLE_SAMPLE = ["hello-world", "values", "variables", "for", "if-else", "slices", "maps", "functions", "methods", "interfaces", "regular-expressions"] -RUST_BY_EXAMPLE_SAMPLE = ["hello.html", "primitives.html", "variable_bindings.html", "flow_control.html", "fn.html", "mod.html"] - - -@dataclass -class Score: - name: str - total: float - payoff: float - deterministic: float - idiom: float - literate: float - output: float - navigation: float - graph: float - layout: float - - -def _runs(code: str) -> bool: - try: - ast.parse(code) - with contextlib.redirect_stdout(io.StringIO()): - exec(compile(code, "", "exec"), {"__name__": "__main__"}) - return True - except Exception: - return False - - -def _fetch_text(url: str) -> str: - try: - req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) - raw = urllib.request.urlopen(req, timeout=20).read().decode("utf-8", "ignore") - except Exception: - return "" - text = re.sub(r"", " ", raw, flags=re.S) - text = re.sub(r"", " ", text, flags=re.S) - text = re.sub(r"<[^>]+>", " ", text) - return html.unescape(re.sub(r"\s+", " ", text)) - - -def score_python_example(example: dict) -> Score: - code = example["code"] - explanation = "\n".join(example.get("explanation", [])) - notes = "\n".join(example.get("notes", [])) - walkthrough = example.get("walkthrough", []) - words = re.findall(r"[a-z]+", (example["summary"] + " " + explanation + " " + notes).lower()) - concept_words = {"why", "when", "use", "because", "idiomatic", "mutable", "iterable", "protocol", "explicit", "lazy", "mapping", "cleanup", "recover"} - executable_lines = [line for line in code.splitlines() if line.strip() and not line.strip().startswith("#")] - - payoff = 1.2 + min(0.8, 0.12 * len(set(words) & concept_words) + 0.1 * min(4, len(walkthrough))) - deterministic = 1.25 if _runs(code) and not re.search(r"random|datetime\.now|time\.time|requests|urllib", code) else 0.7 - idiom = 1.75 - if re.search(r"range\(len\(|except Exception|from .* import \*", code): - idiom -= 0.4 - if len(executable_lines) > 35: - idiom -= 0.2 - - matched_steps = sum(1 for step in walkthrough if step.get("prose") and step.get("code") and step["code"].strip() in code) - literate = 0.8 + min(0.9, 0.9 * matched_steps / max(1, len(walkthrough))) - if any(phrase in explanation.lower() for phrase in ["use the linked python", "full language rules", "sample code is deliberately"]): - literate -= 0.5 - if re.search(rf"^# {re.escape(example['title'])}\s*$", code, re.M): - literate -= 0.3 - literate = max(0, min(2, literate)) - - output = 1.0 if example.get("expected_output") is not None and "white-space: pre-wrap" in PROJECT_SURFACE and "overflow-wrap: anywhere" in PROJECT_SURFACE else 0.4 - navigation = 1.0 if "rel=\"prev\"" in PROJECT_SURFACE and "rel=\"next\"" in PROJECT_SURFACE and "docs.python.org/3.13/" in example.get("doc_url", "") else 0.5 - see_also = example.get("see_also", []) - graph = 0.3 if see_also and "see-also-label" in PROJECT_SURFACE else 0.0 - layout = 1.0 - if "class=\"pill\"" in PROJECT_SURFACE or "corner" in PROJECT_SURFACE or "border-radius: 999px; color: inherit" in PROJECT_SURFACE: - layout -= 0.4 - if "nav a { color: inherit; text-decoration: underline" not in PROJECT_SURFACE: - layout -= 0.2 - total = round(max(0, payoff + deterministic + idiom + literate + output + navigation + graph + layout), 2) - return Score(example["slug"], total, payoff, deterministic, idiom, literate, output, navigation, graph, max(0, layout)) - - -def score_external_literate_page(name: str, url: str, language_markers: tuple[str, ...], reference_label: str) -> Score: - text = _fetch_text(url) - if not text: - return Score(name, 0, 0, 0, 0, 0, 0, 0, 0, 0) - words = text.split() - payoff = 1.8 if len(words) > 220 else 1.5 - deterministic = 1.1 if any(marker in text for marker in ["Run", "Playground", "$ go run", "go run"]) else 0.8 - idiom = 1.75 if any(marker in text for marker in language_markers) else 1.3 - cues = len(re.findall(r"\bHere('|’)s|\bThis |\bNow |\bFor |\bNext |\bTo |\bLet's|\bThe ", text)) - literate = 2.0 if cues >= 6 else 1.6 if cues >= 3 else 1.2 - output = 0.8 if any(marker in text for marker in ["Output", "$ ", "Run"]) else 0.4 - navigation = 0.8 if any(marker in text for marker in ["Next", "Previous", "Rust By Example", "Go by Example"]) else 0.4 - layout = 1.0 - total = round(payoff + deterministic + idiom + literate + output + navigation + layout, 2) - return Score(name, total, payoff, deterministic, idiom, literate, output, navigation, 0.0, layout) - - -def score_gobyexample_page(slug: str) -> Score: - return score_external_literate_page(slug, f"https://gobyexample.com/{slug}", ("package main", "func", "fmt"), "Go by Example") - - -def score_rust_by_example_page(slug: str) -> Score: - return score_external_literate_page(slug.removesuffix(".html"), f"https://doc.rust-lang.org/rust-by-example/{slug}", ("fn main", "let ", "println!", "use "), "Rust By Example") - - -def print_table(title: str, scores: list[Score]) -> None: - print(f"\n{title}") - print("name,total,payoff,deterministic,idiom,literate,output,navigation,graph,layout") - for s in scores: - print(f"{s.name},{s.total:.2f},{s.payoff:.1f},{s.deterministic:.1f},{s.idiom:.1f},{s.literate:.1f},{s.output:.1f},{s.navigation:.1f},{s.graph:.1f},{s.layout:.1f}") - print(f"average,{mean(s.total for s in scores):.2f}") - - -def main() -> int: - py_scores = [score_python_example(example) for example in EXAMPLES] - go_scores = [score_gobyexample_page(slug) for slug in GOBYEXAMPLE_SAMPLE] - rust_scores = [score_rust_by_example_page(slug) for slug in RUST_BY_EXAMPLE_SAMPLE] - print_table("Python By Example", py_scores) - print_table("Go By Example sample", go_scores) - print_table("Rust By Example sample", rust_scores) - failing = [s for s in py_scores if s.total < 8.5] - if failing: - print("\nBelow gate:", ", ".join(f"{s.name}={s.total:.2f}" for s in failing)) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/app.py b/src/app.py index e0c1d00..e11cc60 100644 --- a/src/app.py +++ b/src/app.py @@ -10,9 +10,11 @@ try: from .asset_manifest import ASSET_PATHS from .examples import EXAMPLES, EXAMPLES_BY_SLUG, PYTHON_VERSION, REFERENCE_URL + from .marginalia import render_for_anchor, render_for_section except ImportError: # Cloudflare Python Workers import sibling modules from main's directory. from asset_manifest import ASSET_PATHS from examples import EXAMPLES, EXAMPLES_BY_SLUG, PYTHON_VERSION, REFERENCE_URL + from marginalia import render_for_anchor, render_for_section class AppResponse: @@ -570,7 +572,8 @@ def render_journey_page(journey): else: sentence = f"This gap should {description}." rows.append(f'
  • Gap · {html.escape(value)}

    {html.escape(sentence)}

  • ') - sections.append(f'

    {html.escape(section["title"])}

    {html.escape(section["summary"])}

      {"".join(rows)}
    ') + figure_html = render_for_section(section["title"]) + sections.append(f'

    {html.escape(section["title"])}

    {html.escape(section["summary"])}

    {figure_html}
      {"".join(rows)}
    ') content = f'''
    @@ -747,7 +750,13 @@ def render_example_page(example, output=None, code=None, execution_time_ms=None) if next_example else "" ) - walkthrough_html = "".join(_render_cell(step) for step in walkthrough) + walkthrough_parts: list[str] = [] + for i, step in enumerate(walkthrough): + walkthrough_parts.append(_render_cell(step)) + banner_html = render_for_anchor(example["slug"], f"cell-{i}") + if banner_html: + walkthrough_parts.append(banner_html) + walkthrough_html = "".join(walkthrough_parts) notes_html = "".join(f"
  • {note}
  • " for note in notes) see_also_examples = [get_example(slug) for slug in example.get("see_also", [])] see_also_links = "".join( diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 7dae6fd..929c743 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.5f6f7da7c305.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = 'e02f5d13cadb' +ASSET_PATHS = {'SITE_CSS': '/site.1452cc5609f2.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} +HTML_CACHE_VERSION = '2ef350ca9050' diff --git a/src/example_sources/datetime.md b/src/example_sources/datetime.md index b5e1e1a..6c4441c 100644 --- a/src/example_sources/datetime.md +++ b/src/example_sources/datetime.md @@ -28,7 +28,8 @@ expires_at = created_at + timedelta(days=7, hours=2) print(expires_at.isoformat()) print(created_at.strftime("%Y-%m-%d %H:%M %Z")) -parsed = datetime.fromisoformat("2026-05-04T12:30:00+00:00") +iso_text = "2026-05-04T12:30:00+00:00" +parsed = datetime.fromisoformat(iso_text) print(parsed == created_at) ``` ::: @@ -77,7 +78,8 @@ Use `strftime()` for human-facing formatting and `fromisoformat()` when reading ```python print(created_at.strftime("%Y-%m-%d %H:%M %Z")) -parsed = datetime.fromisoformat("2026-05-04T12:30:00+00:00") +iso_text = "2026-05-04T12:30:00+00:00" +parsed = datetime.fromisoformat(iso_text) print(parsed == created_at) ``` diff --git a/src/example_sources/logging.md b/src/example_sources/logging.md index d2c55fe..39cecb4 100644 --- a/src/example_sources/logging.md +++ b/src/example_sources/logging.md @@ -20,7 +20,8 @@ import sys logger = logging.getLogger("example") logger.setLevel(logging.INFO) handler = logging.StreamHandler(sys.stdout) -handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) +formatter = logging.Formatter("%(levelname)s:%(message)s") +handler.setFormatter(formatter) logger.handlers[:] = [handler] logger.debug("hidden") @@ -39,7 +40,8 @@ import sys logger = logging.getLogger("example") logger.setLevel(logging.INFO) handler = logging.StreamHandler(sys.stdout) -handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) +formatter = logging.Formatter("%(levelname)s:%(message)s") +handler.setFormatter(formatter) logger.handlers[:] = [handler] logger.debug("hidden") diff --git a/src/example_sources/networking.md b/src/example_sources/networking.md index c72f9ff..4eb3def 100644 --- a/src/example_sources/networking.md +++ b/src/example_sources/networking.md @@ -31,7 +31,7 @@ finally: ::: :::unsupported -Dynamic Workers do not provide arbitrary low-level sockets, and this app disables Dynamic Worker outbound access. +`socketpair()` returns two connected endpoints. `sendall` writes encoded bytes into one end, and `recv` reads up to 16 bytes off the other. The byte boundary is the whole point: `"ping".encode("utf-8")` produces `b'ping'`, which is what the socket actually moves. (This fragment runs in standard Python only — Dynamic Workers don't expose arbitrary sockets and this app disables Worker outbound access.) ```python left, right = socket.socketpair() @@ -41,7 +41,7 @@ data = right.recv(16) ::: :::cell -Sockets exchange bytes. Encoding and decoding make the boundary between Python text and network data visible. +The complete version adds two things: a `try`/`finally` so both endpoints close even if `recv` or the surrounding work raises, and a second `print` that `decode`s the received bytes back into a Python `str` for display. The first `print` shows the raw bytes `b'ping'`; the second shows the decoded text `ping`. ```python import socket diff --git a/src/example_sources/strings.md b/src/example_sources/strings.md index 688b4d4..4b8142e 100644 --- a/src/example_sources/strings.md +++ b/src/example_sources/strings.md @@ -15,9 +15,10 @@ Use `str` when you mean text, and encode to `bytes` only at boundaries such as f :::program ```python english = "hello" +french = "café" thai = "สวัสดี" -for label, word in [("English", english), ("Thai", thai)]: +for label, word in [("English", english), ("French", french), ("Thai", thai)]: print(label, word, len(word), len(word.encode("utf-8"))) print(thai[0]) @@ -32,18 +33,20 @@ print(clean.encode("utf-8")) ::: :::cell -Compare an English greeting with a Thai greeting. Both are Python `str` values, but UTF-8 uses one byte for each ASCII code point and multiple bytes for many non-ASCII code points. +Compare three words by code-point count and UTF-8 byte count. ASCII characters take one byte each (`hello` → 5 bytes); the `é` in `café` is one code point but two UTF-8 bytes; each Thai character takes three. The `str` type abstracts over all three. ```python english = "hello" +french = "café" thai = "สวัสดี" -for label, word in [("English", english), ("Thai", thai)]: +for label, word in [("English", english), ("French", french), ("Thai", thai)]: print(label, word, len(word), len(word.encode("utf-8"))) ``` ```output English hello 5 5 +French café 4 5 Thai สวัสดี 6 18 ``` ::: diff --git a/src/example_sources/subprocesses.md b/src/example_sources/subprocesses.md index ed8d8df..c77375c 100644 --- a/src/example_sources/subprocesses.md +++ b/src/example_sources/subprocesses.md @@ -31,7 +31,7 @@ print(result.returncode) ::: :::unsupported -Dynamic Workers do not provide child processes. +`subprocess.run` spawns a child Python interpreter, captures its stdout and stderr (`capture_output=True`), decodes them as text (`text=True`), and raises `CalledProcessError` if the child exits non-zero (`check=True`). The returned `result` holds the captured streams and exit code as portable evidence the child ran. (This fragment runs in standard Python only — Dynamic Workers don't provide child processes.) ```python result = subprocess.run( diff --git a/src/example_sources/testing.md b/src/example_sources/testing.md index e1776ce..8f8c3f5 100644 --- a/src/example_sources/testing.md +++ b/src/example_sources/testing.md @@ -47,9 +47,11 @@ class AddTests(unittest.TestCase): with self.assertRaises(ZeroDivisionError): divide(1, 0) -suite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests) +loader = unittest.defaultTestLoader +suite = loader.loadTestsFromTestCase(AddTests) stream = io.StringIO() -result = unittest.TextTestRunner(stream=stream, verbosity=0).run(suite) +runner = unittest.TextTestRunner(stream=stream, verbosity=0) +result = runner.run(suite) print(result.testsRun) print(result.wasSuccessful()) ``` @@ -111,9 +113,11 @@ A runner executes the suite and records whether every assertion passed. Capturin ```python import io -suite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests) +loader = unittest.defaultTestLoader +suite = loader.loadTestsFromTestCase(AddTests) stream = io.StringIO() -result = unittest.TextTestRunner(stream=stream, verbosity=0).run(suite) +runner = unittest.TextTestRunner(stream=stream, verbosity=0) +result = runner.run(suite) print(result.testsRun) print(result.wasSuccessful()) ``` diff --git a/src/example_sources/threads-and-processes.md b/src/example_sources/threads-and-processes.md index 65158ab..c3fbaf6 100644 --- a/src/example_sources/threads-and-processes.md +++ b/src/example_sources/threads-and-processes.md @@ -29,7 +29,7 @@ print(ProcessPoolExecutor.__name__) ::: :::unsupported -Dynamic Workers do not provide native threads or child processes. +`ThreadPoolExecutor` runs `square` across two worker threads sharing the same interpreter (and the GIL); `ProcessPoolExecutor` runs `pow` across two child processes with isolated memory. Each `pool.map` returns an iterator over results in input order, and the surrounding `with` block joins the workers when the body exits. (This fragment runs in standard Python only — Dynamic Workers don't provide native threads or child processes.) ```python with ThreadPoolExecutor(max_workers=2) as pool: diff --git a/src/example_sources/virtual-environments.md b/src/example_sources/virtual-environments.md index 3a3fa3d..2edd550 100644 --- a/src/example_sources/virtual-environments.md +++ b/src/example_sources/virtual-environments.md @@ -30,7 +30,7 @@ with tempfile.TemporaryDirectory() as directory: ::: :::unsupported -Dynamic Workers do not provide the `venv` module or a project environment workflow. +`venv.EnvBuilder` configures the description of a new environment, then `create(".venv")` materialises it on disk as a directory containing its own interpreter and `site-packages`. `with_pip=False` skips bootstrapping pip — useful when the venv is for an isolated tool that doesn't need to install third-party packages. (This fragment runs in standard Python only — Dynamic Workers don't provide the `venv` module or a project environment workflow.) ```python builder = venv.EnvBuilder(with_pip=False) diff --git a/src/example_sources_data.py b/src/example_sources_data.py index 58dbf0f..504a064 100644 --- a/src/example_sources_data.py +++ b/src/example_sources_data.py @@ -1,2 +1,2 @@ # Generated by scripts/embed_example_sources.py. Do not edit by hand. -EXAMPLE_SOURCE_FILES = {'manifest.toml': 'python_version = "3.13"\ndocs_base_url = "https://docs.python.org/3.13"\n\norder = [\n "hello-world",\n "values",\n "literals",\n "numbers",\n "booleans",\n "operators",\n "none",\n "variables",\n "constants",\n "truthiness",\n "equality-and-identity",\n "mutability",\n "object-lifecycle",\n "strings",\n "bytes-and-bytearray",\n "string-formatting",\n "conditionals",\n "guard-clauses",\n "assignment-expressions",\n "for-loops",\n "break-and-continue",\n "loop-else",\n "iterating-over-iterables",\n "iterators",\n "iterator-vs-iterable",\n "sentinel-iteration",\n "match-statements",\n "advanced-match-patterns",\n "while-loops",\n "lists",\n "tuples",\n "unpacking",\n "dicts",\n "sets",\n "slices",\n "comprehensions",\n "comprehension-patterns",\n "sorting",\n "collections-module",\n "copying-collections",\n "functions",\n "keyword-only-arguments",\n "positional-only-parameters",\n "args-and-kwargs",\n "multiple-return-values",\n "closures",\n "partial-functions",\n "scope-global-nonlocal",\n "recursion",\n "lambdas",\n "generators",\n "yield-from",\n "generator-expressions",\n "itertools",\n "decorators",\n "classes",\n "inheritance-and-super",\n "classmethods-and-staticmethods",\n "dataclasses",\n "properties",\n "special-methods",\n "truth-and-size",\n "container-protocols",\n "callable-objects",\n "operator-overloading",\n "attribute-access",\n "bound-and-unbound-methods",\n "descriptors",\n "metaclasses",\n "context-managers",\n "delete-statements",\n "exceptions",\n "assertions",\n "exception-chaining",\n "exception-groups",\n "warnings",\n "modules",\n "import-aliases",\n "packages",\n "virtual-environments",\n "type-hints",\n "runtime-type-checks",\n "union-and-optional-types",\n "type-aliases",\n "typed-dicts",\n "structured-data-shapes",\n "literal-and-final",\n "callable-types",\n "generics-and-typevar",\n "paramspec",\n "overloads",\n "casts-and-any",\n "newtype",\n "protocols",\n "abstract-base-classes",\n "enums",\n "regular-expressions",\n "number-parsing",\n "custom-exceptions",\n "json",\n "logging",\n "testing",\n "subprocesses",\n "threads-and-processes",\n "networking",\n "datetime",\n "csv-data",\n "async-await",\n "async-iteration-and-context",\n]\n', 'abstract-base-classes.md': '+++\nslug = "abstract-base-classes"\ntitle = "Abstract Base Classes"\nsection = "Classes"\nsummary = "ABC and abstractmethod enforce that subclasses implement required methods."\ndoc_path = "/library/abc.html"\nsee_also = [\n "protocols",\n "inheritance-and-super",\n "classes",\n]\n+++\n\n`ABC` and `@abstractmethod` describe an interface that subclasses must implement. The base class refuses to instantiate until a concrete subclass provides every abstract method, which catches "I forgot to implement this" at construction time rather than at the first method call.\n\nABCs are different from `Protocol`. An ABC is nominal: a class participates in the contract by inheriting from it. A `Protocol` is structural: any class with the right methods qualifies, no inheritance required. Reach for an ABC when you want shared implementation in the base class or you want `isinstance()` to mean "explicitly opted in"; reach for a `Protocol` when you only care about behavior at the API boundary.\n\nThe cost is a small amount of ceremony at the type level. The benefit is that a half-implemented subclass cannot be created by accident.\n\n:::program\n```python\nfrom abc import ABC, abstractmethod\nfrom typing import Protocol\n\nclass Shape(ABC):\n @abstractmethod\n def area(self) -> float:\n ...\n\n def describe(self) -> str:\n return f"shape with area {self.area()}"\n\ntry:\n Shape()\nexcept TypeError as error:\n print(error)\n\nclass Square(Shape):\n def __init__(self, side):\n self.side = side\n\n def area(self):\n return self.side ** 2\n\nprint(Square(3).area())\nprint(Square(3).describe())\n\nclass Incomplete(Shape):\n pass\n\ntry:\n Incomplete()\nexcept TypeError as error:\n print(error)\n\nclass HasArea(Protocol):\n def area(self) -> float:\n ...\n\nclass Triangle:\n def __init__(self, base, height):\n self.base = base\n self.height = height\n\n def area(self):\n return 0.5 * self.base * self.height\n\ndef total_area(shapes: list[HasArea]) -> float:\n return sum(shape.area() for shape in shapes)\n\nprint(total_area([Square(3), Triangle(4, 3)]))\nprint(isinstance(Triangle(4, 3), Shape))\nprint(isinstance(Square(3), Shape))\n```\n:::\n\n:::cell\n`ABC` plus `@abstractmethod` declares the contract. Trying to construct the base class itself fails because at least one method has no implementation. A concrete `describe()` lives alongside the abstract `area()` so subclasses inherit shared behavior for free.\n\n```python\nfrom abc import ABC, abstractmethod\n\nclass Shape(ABC):\n @abstractmethod\n def area(self) -> float:\n ...\n\n def describe(self) -> str:\n return f"shape with area {self.area()}"\n\ntry:\n Shape()\nexcept TypeError as error:\n print(error)\n```\n\n```output\nCan\'t instantiate abstract class Shape without an implementation for abstract method \'area\'\n```\n:::\n\n:::cell\nA subclass that implements every abstract method is concrete and can be instantiated. It also inherits the non-abstract methods from the base class.\n\n```python\nclass Square(Shape):\n def __init__(self, side):\n self.side = side\n\n def area(self):\n return self.side ** 2\n\nprint(Square(3).area())\nprint(Square(3).describe())\n```\n\n```output\n9\nshape with area 9\n```\n:::\n\n:::cell\nA subclass that forgets to implement an abstract method also cannot be instantiated — that is the value the ABC adds. The error fires at construction, not when something later tries to call the missing method.\n\n```python\nclass Incomplete(Shape):\n pass\n\ntry:\n Incomplete()\nexcept TypeError as error:\n print(error)\n```\n\n```output\nCan\'t instantiate abstract class Incomplete without an implementation for abstract method \'area\'\n```\n:::\n\n:::cell\nContrast with `Protocol`. A `HasArea` protocol accepts any class with an `area()` method, no inheritance required. `Triangle` does not inherit from `Shape`, so it satisfies the protocol but fails `isinstance(_, Shape)`. `Square` satisfies both because it explicitly inherited from the ABC.\n\n```python\nfrom typing import Protocol\n\nclass HasArea(Protocol):\n def area(self) -> float:\n ...\n\nclass Triangle:\n def __init__(self, base, height):\n self.base = base\n self.height = height\n\n def area(self):\n return 0.5 * self.base * self.height\n\ndef total_area(shapes: list[HasArea]) -> float:\n return sum(shape.area() for shape in shapes)\n\nprint(total_area([Square(3), Triangle(4, 3)]))\nprint(isinstance(Triangle(4, 3), Shape))\nprint(isinstance(Square(3), Shape))\n```\n\n```output\n15.0\nFalse\nTrue\n```\n:::\n\n:::note\n- `ABC` plus `@abstractmethod` blocks instantiation until every abstract method has an implementation.\n- ABCs are nominal — subclasses opt in by inheriting; `isinstance()` reflects that opt-in.\n- Protocols are structural — any class with the right shape qualifies, regardless of inheritance.\n- Prefer an ABC when shared implementation or explicit opt-in matters; prefer a Protocol when only behavior at the API boundary matters.\n:::\n', 'advanced-match-patterns.md': '+++\nslug = "advanced-match-patterns"\ntitle = "Advanced Match Patterns"\nsection = "Control Flow"\nsummary = "match patterns can destructure sequences, combine alternatives, and add guards."\ndoc_path = "/tutorial/controlflow.html#match-statements"\nsee_also = [\n "match-statements",\n "tuples",\n "classes",\n]\n+++\n\nStructural pattern matching is more than equality checks. Patterns can destructure sequences, match several alternatives, capture the rest of a sequence, and use guards.\n\nUse these forms when the shape of data is the decision. If the decision is only a single boolean condition, ordinary `if` statements are usually clearer.\n\nThe wildcard `_` catches everything not matched earlier.\n\n:::program\n```python\ndef describe(command):\n match command:\n case ["move", x, y] if x >= 0 and y >= 0:\n return f"move to {x},{y}"\n case ["quit" | "exit"]:\n return "stop"\n case ["echo", *words]:\n return " ".join(words)\n case _:\n return "unknown"\n\nprint(describe(["move", 2, 3]))\nprint(describe(["exit"]))\nprint(describe(["echo", "hello", "python"]))\nprint(describe(["move", -1, 3]))\n```\n:::\n\n:::cell\nSequence patterns match by position. A guard after `if` adds a condition that must also be true.\n\n```python\ndef describe(command):\n match command:\n case ["move", x, y] if x >= 0 and y >= 0:\n return f"move to {x},{y}"\n case ["quit" | "exit"]:\n return "stop"\n case ["echo", *words]:\n return " ".join(words)\n case _:\n return "unknown"\n\nprint(describe(["move", 2, 3]))\n```\n\n```output\nmove to 2,3\n```\n:::\n\n:::cell\nAn OR pattern accepts several alternatives in one case. A star pattern captures the rest of a sequence.\n\n```python\nprint(describe(["exit"]))\nprint(describe(["echo", "hello", "python"]))\n```\n\n```output\nstop\nhello python\n```\n:::\n\n:::cell\nThe wildcard `_` catches values that did not match earlier cases. Here the guard rejects the negative coordinate.\n\n```python\nprint(describe(["move", -1, 3]))\n```\n\n```output\nunknown\n```\n:::\n\n:::note\n- Use `case _` as a wildcard fallback.\n- Guards refine a pattern after the structure matches.\n- OR patterns and star patterns keep shape-based branches compact.\n:::\n', 'args-and-kwargs.md': '+++\nslug = "args-and-kwargs"\ntitle = "Args and Kwargs"\nsection = "Functions"\nsummary = "*args collects extra positional arguments and **kwargs collects named ones."\ndoc_path = "/tutorial/controlflow.html#arbitrary-argument-lists"\n+++\n\n`*args` and `**kwargs` let a function accept flexible positional and keyword arguments. They are the function-definition counterpart to unpacking at a call site.\n\nThese parameters are useful for wrappers, decorators, logging helpers, and APIs that forward arguments to another function.\n\nThey should not replace clear signatures. If a function has a stable interface, explicit parameters document expectations better than a bag of arguments.\n\n:::program\n```python\ndef total(*numbers):\n return sum(numbers)\n\nprint(total(2, 3, 5))\n\n\ndef describe(**metadata):\n print(metadata)\n\ndescribe(owner="Ada", public=True)\n\n\ndef report(title, *items, **metadata):\n print(title)\n print(items)\n print(metadata)\n\nreport("scores", 10, 9, owner="Ada")\n```\n:::\n\n:::cell\n`*args` collects extra positional arguments into a tuple. This fits functions that naturally accept any number of similar values.\n\n```python\ndef total(*numbers):\n return sum(numbers)\n\nprint(total(2, 3, 5))\n```\n\n```output\n10\n```\n:::\n\n:::cell\n`**kwargs` collects named arguments into a dictionary. The names become string keys.\n\n```python\ndef describe(**metadata):\n print(metadata)\n\ndescribe(owner="Ada", public=True)\n```\n\n```output\n{\'owner\': \'Ada\', \'public\': True}\n```\n:::\n\n:::cell\nA function can combine explicit parameters, `*args`, and `**kwargs`. Put the flexible parts last so the fixed shape remains visible.\n\n```python\ndef report(title, *items, **metadata):\n print(title)\n print(items)\n print(metadata)\n\nreport("scores", 10, 9, owner="Ada")\n```\n\n```output\nscores\n(10, 9)\n{\'owner\': \'Ada\'}\n```\n:::\n\n:::note\n- Use these tools when a function naturally accepts a flexible shape.\n- Prefer explicit parameters when the accepted arguments are known and fixed.\n- `*args` is a tuple; `**kwargs` is a dictionary.\n:::\n', 'assertions.md': '+++\nslug = "assertions"\ntitle = "Assertions"\nsection = "Errors"\nsummary = "assert documents internal assumptions and fails loudly when they are false."\ndoc_path = "/reference/simple_stmts.html#the-assert-statement"\nsee_also = [\n "exceptions",\n "custom-exceptions",\n "type-hints",\n]\n+++\n\n`assert` checks an internal assumption. If the condition is false, Python raises `AssertionError` with an optional message.\n\nUse assertions for programmer assumptions, not for validating user input or external data. Input validation should raise ordinary exceptions that production code expects to handle.\n\nAssertions make invariants executable while keeping the successful path compact.\n\n:::program\n```python\ndef average(scores):\n assert scores, "scores must not be empty"\n return sum(scores) / len(scores)\n\nprint(average([8, 10]))\n\ntry:\n average([])\nexcept AssertionError as error:\n print(error)\n```\n:::\n\n:::cell\nWhen the assertion is true, execution continues normally. The assertion documents the function\'s internal expectation.\n\n```python\ndef average(scores):\n assert scores, "scores must not be empty"\n return sum(scores) / len(scores)\n\nprint(average([8, 10]))\n```\n\n```output\n9.0\n```\n:::\n\n:::cell\nWhen the assertion is false, Python raises `AssertionError`. This signals a broken assumption, not a normal recovery path.\n\n```python\ntry:\n average([])\nexcept AssertionError as error:\n print(error)\n```\n\n```output\nscores must not be empty\n```\n:::\n\n:::note\n- Use `assert` for internal invariants and debugging assumptions.\n- Use explicit exceptions for user input, files, network responses, and other expected failures.\n- Assertions can be disabled with Python optimization flags, so do not rely on them for security checks.\n:::\n', 'assignment-expressions.md': '+++\nslug = "assignment-expressions"\ntitle = "Assignment Expressions"\nsection = "Control Flow"\nsummary = "The walrus operator assigns a value inside an expression."\ndoc_path = "/reference/expressions.html#assignment-expressions"\nsee_also = [\n "conditionals",\n "while-loops",\n "variables",\n]\n+++\n\nThe assignment expression operator `:=` assigns a name while evaluating an expression. It is often called the walrus operator.\n\nUse it when computing a value and testing it are naturally one step. Avoid it when a separate assignment would make the code easier to read.\n\nThe boundary is readability: the walrus operator can remove duplication, but it should not hide important state changes.\n\n:::program\n```python\nmessages = ["hello", "", "python"]\n\nfor message in messages:\n if length := len(message):\n print(message, length)\n\nqueue = ["retry", "ok"]\nwhile (status := queue.pop(0)) != "ok":\n print(status)\nprint(status)\n```\n:::\n\n:::cell\nAn assignment expression can name a computed value while a condition tests it. Here empty strings are skipped because their length is zero.\n\n```python\nmessages = ["hello", "", "python"]\n\nfor message in messages:\n if length := len(message):\n print(message, length)\n```\n\n```output\nhello 5\npython 6\n```\n:::\n\n:::cell\nThe same idea works in loops that read state until a sentinel appears. The assignment and comparison stay together.\n\n```python\nqueue = ["retry", "ok"]\nwhile (status := queue.pop(0)) != "ok":\n print(status)\nprint(status)\n```\n\n```output\nretry\nok\n```\n:::\n\n:::note\n- `name := expression` assigns and evaluates to the assigned value.\n- Use it to avoid computing the same value twice.\n- Prefer a normal assignment when the expression becomes hard to scan.\n:::\n', 'async-await.md': '+++\nslug = "async-await"\ntitle = "Async Await"\nsection = "Async"\nsummary = "async def creates coroutines, and await pauses until awaitable work completes."\ndoc_path = "/library/asyncio-task.html"\nsee_also = [\n "async-iteration-and-context",\n "functions",\n "context-managers",\n]\n+++\n\n`async def` creates a coroutine function. Calling it creates a coroutine object; the body runs when an event loop awaits or schedules it.\n\n`await` pauses the current coroutine until another awaitable completes. This lets one event loop make progress on other work while a task waits for I/O.\n\nCloudflare Workers handlers are asynchronous, so understanding `await` is practical for fetch calls, bindings, and service interactions even when a small example uses `asyncio.sleep(0)` as a stand-in.\n\nThe alternative is ordinary `def` for work that completes immediately. Use async code for I/O-shaped waiting, not as a faster replacement for CPU-bound Python.\n\n:::program\n```python\nimport asyncio\n\nasync def fetch_title(slug):\n await asyncio.sleep(0)\n return slug.replace("-", " ").title()\n\nasync def main():\n title = await fetch_title("async-await")\n print(title)\n titles = await asyncio.gather(fetch_title("json"), fetch_title("datetime"))\n print(titles)\n\nasyncio.run(main())\n\n\nclass Session:\n async def __aenter__(self):\n print("open")\n return self\n\n async def __aexit__(self, *_):\n print("close")\n return False\n\n\nasync def stream():\n for slug in ["json", "datetime"]:\n await asyncio.sleep(0)\n yield slug\n\n\nasync def driver():\n async with Session():\n async for slug in stream():\n print(slug)\n\nasyncio.run(driver())\n```\n:::\n\n:::cell\nAn `async def` function returns a coroutine object when called. The function body has not produced its final result yet.\n\n```python\nimport asyncio\n\nasync def fetch_title(slug):\n await asyncio.sleep(0)\n return slug.replace("-", " ").title()\n\ncoroutine = fetch_title("async-await")\nprint(coroutine.__class__.__name__)\ncoroutine.close()\n```\n\n```output\ncoroutine\n```\n:::\n\n:::cell\nUse `await` inside another coroutine to get the eventual result. `asyncio.run()` starts an event loop for the top-level coroutine.\n\n```python\nasync def main():\n title = await fetch_title("async-await")\n print(title)\n\nasyncio.run(main())\n```\n\n```output\nAsync Await\n```\n:::\n\n:::cell\n`asyncio.gather()` awaits several awaitables and returns their results in order. This is the shape used when independent I/O operations can progress together.\n\n```python\nasync def main():\n titles = await asyncio.gather(fetch_title("json"), fetch_title("datetime"))\n print(titles)\n\nasyncio.run(main())\n```\n\n```output\n[\'Json\', \'Datetime\']\n```\n:::\n\n:::cell\n`async with` and `async for` are the asynchronous forms of context managers and iteration. A class implements `__aenter__`/`__aexit__` to act as an async context manager; an `async def` function with `yield` becomes an async generator. The dedicated [async iteration and context](/iteration/async-iteration-and-context) page explains the protocols in depth.\n\n```python\nclass Session:\n async def __aenter__(self):\n print("open")\n return self\n\n async def __aexit__(self, *_):\n print("close")\n return False\n\n\nasync def stream():\n for slug in ["json", "datetime"]:\n await asyncio.sleep(0)\n yield slug\n\n\nasync def driver():\n async with Session():\n async for slug in stream():\n print(slug)\n\nasyncio.run(driver())\n```\n\n```output\nopen\njson\ndatetime\nclose\n```\n:::\n\n:::note\n- Calling an async function creates a coroutine object.\n- `await` yields control until an awaitable completes.\n- Workers request handlers are async, so this pattern appears around fetches and bindings.\n- Prefer ordinary functions when there is no awaitable work to coordinate.\n:::\n', 'async-iteration-and-context.md': '+++\nslug = "async-iteration-and-context"\ntitle = "Async Iteration and Context"\nsection = "Async"\nsummary = "async for and async with consume asynchronous streams and cleanup protocols."\ndoc_path = "/reference/compound_stmts.html#async-for"\nsee_also = [\n "async-await",\n "iterators",\n "context-managers",\n]\n+++\n\n`async for` consumes an asynchronous iterator: a stream whose next value may require `await`. `async with` surrounds a block with asynchronous setup and cleanup.\n\nThese forms appear around network streams, database cursors, locks, and service clients where both iteration and cleanup may wait on I/O.\n\nUse ordinary `for` and `with` when producing the next value or cleaning up does not need to await anything.\n\nThe syntax mirrors `for` and `with`, but the protocol methods are asynchronous.\n\n:::program\n```python\nimport asyncio\n\nasync def titles():\n for slug in ["values", "async-await"]:\n await asyncio.sleep(0)\n yield slug.replace("-", " ").title()\n\nclass Session:\n async def __aenter__(self):\n print("open")\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n print("close")\n\nasync def main():\n async with Session():\n async for title in titles():\n print(title)\n\nasyncio.run(main())\n```\n:::\n\n:::cell\nAn async generator can `await` before yielding each value. `async for` consumes those values with the asynchronous iteration protocol.\n\n```python\nimport asyncio\n\nasync def titles():\n for slug in ["values", "async-await"]:\n await asyncio.sleep(0)\n yield slug.replace("-", " ").title()\n\nprint(titles.__name__)\n```\n\n```output\ntitles\n```\n:::\n\n:::cell\nAn async context manager defines `__aenter__` and `__aexit__`. `async with` awaits setup and cleanup around the block.\n\n```python\nclass Session:\n async def __aenter__(self):\n print("open")\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n print("close")\n\nprint(Session.__name__)\n```\n\n```output\nSession\n```\n:::\n\n:::cell\nThe top-level coroutine combines both protocols: open the async resource, then consume the async stream inside it.\n\n```python\nasync def main():\n async with Session():\n async for title in titles():\n print(title)\n\nasyncio.run(main())\n```\n\n```output\nopen\nValues\nAsync Await\nclose\n```\n:::\n\n:::note\n- `async for` consumes asynchronous iterators.\n- `async with` awaits asynchronous setup and cleanup.\n- These forms are common around I/O-shaped resources.\n:::\n', 'attribute-access.md': '+++\nslug = "attribute-access"\ntitle = "Attribute Access"\nsection = "Data Model"\nsummary = "Attribute hooks customize lookup, missing attributes, and assignment."\ndoc_path = "/reference/datamodel.html#customizing-attribute-access"\nsee_also = [\n "properties",\n "descriptors",\n "special-methods",\n "bound-and-unbound-methods",\n]\n+++\n\nAttribute access is usually simple: `obj.name` looks up an attribute. Python exposes hooks for the uncommon cases where lookup or assignment needs to be customized.\n\n`__getattr__` runs only when normal lookup fails, which makes it a safer hook for computed fallback attributes. `__setattr__` runs for every assignment, so it should be used sparingly and carefully.\n\nPrefer ordinary attributes and `@property` first. Reach for these hooks when an object is intentionally adapting another interface, validating all assignments, or exposing dynamic fields.\n\n:::program\n```python\nclass Settings:\n def __init__(self, values):\n self._values = dict(values)\n\n def __getattr__(self, name):\n try:\n return self._values[name]\n except KeyError as error:\n raise AttributeError(name) from error\n\n def __setattr__(self, name, value):\n if name.startswith("_"):\n object.__setattr__(self, name, value)\n else:\n self._values[name] = value\n\nsettings = Settings({"theme": "dark"})\nprint(settings.theme)\nsettings.volume = 7\nprint(settings._values["volume"])\n```\n:::\n\n:::cell\nNormal initialization still needs to set real attributes. Calling `object.__setattr__` avoids recursing through your own hook.\n\n```python\nclass Settings:\n def __init__(self, values):\n self._values = dict(values)\n\nsettings = Settings({"theme": "dark"})\nprint(settings._values)\n```\n\n```output\n{\'theme\': \'dark\'}\n```\n:::\n\n:::cell\n`__getattr__` runs only for missing attributes, so it can provide fallback lookup.\n\n```python\nclass Settings:\n def __init__(self, values):\n self._values = dict(values)\n\n def __getattr__(self, name):\n try:\n return self._values[name]\n except KeyError as error:\n raise AttributeError(name) from error\n\nsettings = Settings({"theme": "dark"})\nprint(settings.theme)\n```\n\n```output\ndark\n```\n:::\n\n:::cell\n`__setattr__` intercepts assignment. This example stores public names in the backing dictionary.\n\n```python\nclass Settings:\n def __init__(self, values):\n self._values = dict(values)\n\n def __setattr__(self, name, value):\n if name.startswith("_"):\n object.__setattr__(self, name, value)\n else:\n self._values[name] = value\n\nsettings = Settings({"theme": "dark"})\nsettings.volume = 7\nprint(settings._values["volume"])\n```\n\n```output\n7\n```\n:::\n\n:::note\n- `__getattr__` is narrower than `__getattribute__` because it handles only missing attributes.\n- `__setattr__` affects every assignment on the instance.\n- Use `property` or descriptors when the behavior is attached to a known attribute name.\n:::\n', 'booleans.md': '+++\nslug = "booleans"\ntitle = "Booleans"\nsection = "Basics"\nsummary = "Booleans represent truth values and combine with logical operators."\ndoc_path = "/library/stdtypes.html#boolean-type-bool"\n+++\n\nBooleans are the values `True` and `False`. They are produced by comparisons and combined with `and`, `or`, and `not`.\n\nPython\'s logical operators short-circuit. That means the right side is evaluated only when needed, which keeps guard checks efficient and safe.\n\nBooleans are also connected to truthiness: many objects can be tested in conditions even when they are not literally `True` or `False`.\n\n:::program\n```python\nlogged_in = True\nhas_permission = False\n\nprint(logged_in and has_permission)\nprint(logged_in or has_permission)\nprint(not has_permission)\n\nname = "Ada"\nprint(name == "Ada" and len(name) > 0)\n\nprint(isinstance(True, int))\nprint(True + True)\nprint(sum([True, True, False, True]))\n\ndef is_strict_int(value):\n return isinstance(value, int) and not isinstance(value, bool)\n\nprint(is_strict_int(True))\nprint(is_strict_int(1))\n```\n:::\n\n:::cell\nUse booleans for facts that are either true or false. Python spells the constants `True` and `False`.\n\nUse `and`, `or`, and `not` to combine truth values. These operators read like English and short-circuit when possible.\n\n```python\nlogged_in = True\nhas_permission = False\n\nprint(logged_in and has_permission)\nprint(logged_in or has_permission)\nprint(not has_permission)\n```\n\n```output\nFalse\nTrue\nTrue\n```\n:::\n\n:::cell\nComparisons produce booleans too, so they compose naturally with logical operators in conditions and validation checks.\n\n```python\nname = "Ada"\nprint(name == "Ada" and len(name) > 0)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\n`bool` is a subclass of `int`, which is occasionally a footgun. `True` behaves as `1` and `False` as `0` in arithmetic, and `isinstance(True, int)` is `True`. When a function must reject booleans, exclude them explicitly with `isinstance(value, int) and not isinstance(value, bool)`.\n\n```python\nprint(isinstance(True, int))\nprint(True + True)\nprint(sum([True, True, False, True]))\n\ndef is_strict_int(value):\n return isinstance(value, int) and not isinstance(value, bool)\n\nprint(is_strict_int(True))\nprint(is_strict_int(1))\n```\n\n```output\nTrue\n2\n3\nFalse\nTrue\n```\n:::\n\n:::note\n- Boolean constants are `True` and `False`, with capital letters.\n- `and` and `or` short-circuit: Python does not evaluate the right side if the left side already determines the result.\n- Prefer truthiness for containers and explicit comparisons when the exact boolean condition matters.\n- `bool` subclasses `int`; `isinstance(True, int)` is `True`. Exclude booleans explicitly when only "real" integers should pass.\n:::\n', 'bound-and-unbound-methods.md': '+++\nslug = "bound-and-unbound-methods"\ntitle = "Bound and Unbound Methods"\nsection = "Data Model"\nsummary = "instance.method binds self automatically; Class.method is a plain function."\ndoc_path = "/reference/datamodel.html#instance-methods"\nsee_also = [\n "classes",\n "attribute-access",\n "descriptors",\n "callable-objects",\n]\n+++\n\nWhen you write `instance.method`, Python returns a bound method — a callable that already remembers which instance to pass as `self`. When you write `Class.method`, you get the underlying function back, and calling it requires passing an instance yourself.\n\nThat distinction is why methods can be stored in collections, passed as callbacks, and called later without losing track of the object they belong to. Each bound method carries its own `__self__`, so two callables produced from two different instances stay independent even when their underlying function is the same.\n\nThe mechanism is the descriptor protocol: a function attached to a class implements `__get__`, and that hook turns attribute access on an instance into a bound method. The page does not need that detail to use methods, but it explains what is happening underneath.\n\n:::program\n```python\nclass Counter:\n def __init__(self, start=0):\n self.value = start\n\n def increment(self):\n self.value += 1\n return self.value\n\nbound_counter = Counter(10)\nm = bound_counter.increment\nprint(m.__self__ is bound_counter)\nprint(m())\nprint(m())\n\nunbound_counter = Counter(0)\nunbound = Counter.increment\nprint(type(unbound).__name__)\nprint(unbound(unbound_counter))\nprint(unbound(unbound_counter))\n\nhandlers = []\nfor _ in range(2):\n handlers.append(Counter().increment)\n\nprint(handlers[0]())\nprint(handlers[0]())\nprint(handlers[1]())\n\ndescriptor_counter = Counter(0)\nfunc = Counter.__dict__["increment"]\nprint(type(func).__name__)\nrebound = func.__get__(descriptor_counter, Counter)\nprint(type(rebound).__name__)\nprint(rebound.__self__ is descriptor_counter)\n```\n:::\n\n:::cell\n`instance.method` returns a bound method. The method already remembers the instance through `__self__`, so calling it does not require passing `self` again.\n\n```python\nclass Counter:\n def __init__(self, start=0):\n self.value = start\n\n def increment(self):\n self.value += 1\n return self.value\n\nbound_counter = Counter(10)\nm = bound_counter.increment\nprint(m.__self__ is bound_counter)\nprint(m())\nprint(m())\n```\n\n```output\nTrue\n11\n12\n```\n:::\n\n:::cell\n`Class.method` returns the underlying function — there is no `self` attached. Calling it requires passing the instance as the first argument explicitly. Using a fresh counter here makes the output independent of the previous cell.\n\n```python\nunbound_counter = Counter(0)\nunbound = Counter.increment\nprint(type(unbound).__name__)\nprint(unbound(unbound_counter))\nprint(unbound(unbound_counter))\n```\n\n```output\nfunction\n1\n2\n```\n:::\n\n:::cell\nBound methods are first-class values. They can be stored in lists, passed to other functions, and called later. Each bound method carries its own `__self__`, so two methods produced from two different instances stay independent.\n\n```python\nhandlers = []\nfor _ in range(2):\n handlers.append(Counter().increment)\n\nprint(handlers[0]())\nprint(handlers[0]())\nprint(handlers[1]())\n```\n\n```output\n1\n2\n1\n```\n:::\n\n:::cell\nThe binding is the descriptor protocol at work. The function lives on the class as a plain function; instance attribute access invokes `__get__`, which returns a bound method that knows the instance.\n\n```python\ndescriptor_counter = Counter(0)\nfunc = Counter.__dict__["increment"]\nprint(type(func).__name__)\nrebound = func.__get__(descriptor_counter, Counter)\nprint(type(rebound).__name__)\nprint(rebound.__self__ is descriptor_counter)\n```\n\n```output\nfunction\nmethod\nTrue\n```\n:::\n\n:::note\n- `instance.method` produces a bound method whose `__self__` is the instance.\n- `Class.method` produces the plain function and requires you to pass the instance.\n- Each bound method is its own object; storing one captures its instance.\n- The binding is implemented by the descriptor protocol on the function object.\n:::\n', 'break-and-continue.md': '+++\nslug = "break-and-continue"\ntitle = "Break and Continue"\nsection = "Control Flow"\nsummary = "break exits a loop early, while continue skips to the next iteration."\ndoc_path = "/tutorial/controlflow.html#break-and-continue-statements"\nsee_also = [\n "for-loops",\n "while-loops",\n "loop-else",\n]\n+++\n\n`break` and `continue` control the nearest enclosing loop. They exist for loops whose body discovers an early stop rule or an item-level skip rule.\n\nUse `continue` when the current item should not run the rest of the body. Use `break` when no later item should be processed.\n\nThe alternative is ordinary `if`/`else` nesting. Prefer `break` and `continue` when they keep the normal path flatter and easier to read.\n\n:::program\n```python\nnames = ["Ada", "", "Grace"]\nfor name in names:\n if not name:\n continue\n print(name)\n\ncommands = ["load", "save", "stop", "delete"]\nfor command in commands:\n if command == "stop":\n break\n print(command)\n```\n:::\n\n:::cell\n`continue` skips the rest of the current iteration. The empty name is ignored, and the loop moves on to the next value.\n\n```python\nnames = ["Ada", "", "Grace"]\nfor name in names:\n if not name:\n continue\n print(name)\n```\n\n```output\nAda\nGrace\n```\n:::\n\n:::cell\n`break` exits the loop immediately. The value after `stop` is never processed because the loop has already ended.\n\n```python\ncommands = ["load", "save", "stop", "delete"]\nfor command in commands:\n if command == "stop":\n break\n print(command)\n```\n\n```output\nload\nsave\n```\n:::\n\n:::note\n- `continue` skips to the next loop iteration.\n- `break` exits the nearest enclosing loop immediately.\n- Prefer plain `if`/`else` when the loop does not need early skip or early stop behavior.\n:::\n', 'bytes-and-bytearray.md': '+++\nslug = "bytes-and-bytearray"\ntitle = "Bytes and Bytearray"\nsection = "Basics"\nsummary = "bytes and bytearray store binary data, not Unicode text."\ndoc_path = "/library/stdtypes.html#binary-sequence-types-bytes-bytearray-memoryview"\nsee_also = [\n "strings",\n "literals",\n "networking",\n]\n+++\n\n`str` stores Unicode text. `bytes` stores raw byte values. The boundary matters whenever text leaves Python for a file, network protocol, subprocess, or binary format.\n\nEncoding turns text into bytes with a named encoding such as UTF-8. Decoding turns bytes back into text. The lengths can differ because one Unicode character may require several bytes.\n\nUse immutable `bytes` for stable binary data and `bytearray` when the bytes must be changed in place.\n\n:::program\n```python\ntext = "café"\ndata = text.encode("utf-8")\n\nprint(data)\nprint(len(text), len(data))\nprint(data.decode("utf-8"))\nprint(data[0])\n\npacket = bytearray(b"py")\npacket[0] = ord("P")\nprint(packet)\n```\n:::\n\n:::cell\nEncode text when an external boundary needs bytes. UTF-8 uses one byte for ASCII characters and more than one byte for many other characters.\n\n```python\ntext = "café"\ndata = text.encode("utf-8")\nprint(data)\nprint(len(text), len(data))\n```\n\n```output\nb\'caf\\xc3\\xa9\'\n4 5\n```\n:::\n\n:::cell\nDecode bytes when the program needs text again. The decoder must match the encoding used at the boundary.\n\n```python\nprint(data.decode("utf-8"))\n```\n\n```output\ncafé\n```\n:::\n\n:::cell\nIndexing a `bytes` object returns an integer byte value, not a one-character `bytes` object.\n\n```python\nprint(data[0])\n```\n\n```output\n99\n```\n:::\n\n:::cell\n`bytes` is immutable. Use `bytearray` when binary data must be changed in place.\n\n```python\npacket = bytearray(b"py")\npacket[0] = ord("P")\nprint(packet)\n```\n\n```output\nbytearray(b\'Py\')\n```\n:::\n\n:::note\n- Encode text when an external boundary needs bytes.\n- Decode bytes when you want text again.\n- Indexing `bytes` returns integers from 0 to 255.\n- Use `bytearray` when binary data must be changed in place.\n:::\n', 'callable-objects.md': '+++\nslug = "callable-objects"\ntitle = "Callable Objects"\nsection = "Data Model"\nsummary = "__call__ lets an instance behave like a function while keeping state."\ndoc_path = "/reference/datamodel.html#object.__call__"\nsee_also = [\n "functions",\n "closures",\n "callable-types",\n "bound-and-unbound-methods",\n]\n+++\n\nFunctions are not the only callable objects in Python. Any instance can be called with parentheses when its class defines `__call__`.\n\nCallable objects are useful when behavior needs remembered configuration or evolving state. A closure can do this too; a class is often clearer when the state has multiple fields or needs named methods.\n\nThe tradeoff is ceremony. Use a function for simple behavior, a closure for small captured state, and a callable object when naming the state improves the interface.\n\n:::program\n```python\nclass Multiplier:\n def __init__(self, factor):\n self.factor = factor\n self.calls = 0\n\n def __call__(self, value):\n self.calls += 1\n return value * self.factor\n\ndouble = Multiplier(2)\nprint(double(5))\nprint(double(7))\nprint(double.calls)\n```\n:::\n\n:::cell\nA callable object starts as ordinary state stored on an instance.\n\n```python\nclass Multiplier:\n def __init__(self, factor):\n self.factor = factor\n self.calls = 0\n\ndouble = Multiplier(2)\nprint(double.factor)\n```\n\n```output\n2\n```\n:::\n\n:::cell\n`__call__` makes the instance usable with function-call syntax.\n\n```python\nclass Multiplier:\n def __init__(self, factor):\n self.factor = factor\n self.calls = 0\n\n def __call__(self, value):\n self.calls += 1\n return value * self.factor\n\ndouble = Multiplier(2)\nprint(double(5))\nprint(double(7))\n```\n\n```output\n10\n14\n```\n:::\n\n:::cell\nBecause the callable is still an object, it can remember state across calls.\n\n```python\nprint(double.calls)\n```\n\n```output\n2\n```\n:::\n\n:::note\n- `callable(obj)` checks whether an object can be called.\n- Callable objects are good for named, stateful behavior.\n- Prefer plain functions when no instance state is needed.\n:::\n', 'callable-types.md': '+++\nslug = "callable-types"\ntitle = "Callable Types"\nsection = "Types"\nsummary = "Callable annotations describe functions passed as values."\ndoc_path = "/library/typing.html#annotating-callable-objects"\nsee_also = [\n "functions",\n "callable-objects",\n "protocols",\n]\n+++\n\nCallable annotations describe values that can be called like functions. They are useful when a function accepts a callback, strategy, predicate, or transformation.\n\n`Callable[[int], int]` says how the callback will be called: one integer argument, integer result. The annotation helps tools and readers, while runtime still only needs an object that is actually callable.\n\nUse `Callable` for simple call shapes. Use a protocol when the callback needs named attributes, overloaded signatures, or a more descriptive interface.\n\n:::program\n```python\nfrom collections.abc import Callable\n\n\ndef apply_twice(value: int, func: Callable[[int], int]) -> int:\n return func(func(value))\n\n\ndef add_one(number: int) -> int:\n return number + 1\n\nclass Doubler:\n def __call__(self, number: int) -> int:\n return number * 2\n\nprint(apply_twice(3, add_one))\nprint(apply_twice(3, Doubler()))\nprint(callable(add_one), callable(Doubler()))\n```\n:::\n\n:::cell\nUse `Callable[[Arg], Return]` for function-shaped values. The callback is passed in and called by the receiving function.\n\n```python\nfrom collections.abc import Callable\n\n\ndef apply_twice(value: int, func: Callable[[int], int]) -> int:\n return func(func(value))\n\n\ndef add_one(number: int) -> int:\n return number + 1\n\nprint(apply_twice(3, add_one))\n```\n\n```output\n5\n```\n:::\n\n:::cell\nCallable annotations are structural: an object with `__call__` can also satisfy the shape.\n\n```python\nclass Doubler:\n def __call__(self, number: int) -> int:\n return number * 2\n\nprint(apply_twice(3, Doubler()))\n```\n\n```output\n12\n```\n:::\n\n:::cell\nRuntime callability is a separate question from static annotation. `callable()` checks whether Python can call the object.\n\n```python\nprint(callable(add_one), callable(Doubler()))\n```\n\n```output\nTrue True\n```\n:::\n\n:::note\n- Use `Callable[[Arg], Return]` for simple function-shaped values.\n- The annotation documents how the callback will be called.\n- For complex call signatures, protocols can be clearer.\n:::\n', 'casts-and-any.md': '+++\nslug = "casts-and-any"\ntitle = "Casts and Any"\nsection = "Types"\nsummary = "Any and cast are escape hatches for places static analysis cannot prove."\ndoc_path = "/library/typing.html#typing.cast"\nsee_also = [\n "type-hints",\n "runtime-type-checks",\n "typed-dicts",\n]\n+++\n\n`Any` and `cast()` are escape hatches. They are useful at messy boundaries where a type checker cannot prove what a value is, but they also remove protection when overused.\n\n`Any` tells static tools to stop checking most operations on a value. `cast(T, value)` tells the type checker to treat a value as `T`, but it returns the same runtime object unchanged.\n\nPrefer narrowing with runtime checks when possible. Use `cast()` when another invariant already proves the type and the checker cannot see that proof.\n\n:::program\n```python\nfrom typing import Any, cast\n\nraw: Any = {"score": "98"}\nscore_text = cast(dict[str, str], raw)["score"]\nscore = int(score_text)\n\nprint(score + 2)\nprint(cast(list[int], raw) is raw)\nprint(type(raw).__name__)\n```\n:::\n\n:::cell\n`Any` disables most static checking for a value. The runtime object is still whatever value was actually assigned.\n\n```python\nfrom typing import Any, cast\n\nraw: Any = {"score": "98"}\nscore_text = cast(dict[str, str], raw)["score"]\nscore = int(score_text)\nprint(score + 2)\n```\n\n```output\n100\n```\n:::\n\n:::cell\n`cast()` does not convert or validate the value. It returns the same object at runtime.\n\n```python\nprint(cast(list[int], raw) is raw)\nprint(type(raw).__name__)\n```\n\n```output\nTrue\ndict\n```\n:::\n\n:::cell\nA real runtime check narrows by inspecting the value. This is safer when the input is untrusted.\n\n```python\nvalue: object = {"score": "98"}\nif isinstance(value, dict):\n print(value["score"])\n```\n\n```output\n98\n```\n:::\n\n:::note\n- `Any` disables most static checking for a value.\n- `cast()` tells the type checker to trust you without changing the runtime object.\n- Prefer narrowing with checks when possible.\n:::\n', 'classes.md': '+++\nslug = "classes"\ntitle = "Classes"\nsection = "Classes"\nsummary = "Classes bundle data and behavior into new object types."\ndoc_path = "/tutorial/classes.html"\nsee_also = [\n "inheritance-and-super",\n "classmethods-and-staticmethods",\n "bound-and-unbound-methods",\n "dataclasses",\n]\n+++\n\nClasses define new object types by bundling data with behavior. They are useful when several values and operations belong together and should travel as one object.\n\nThe alternative is often a dictionary plus separate functions. That is fine for loose data, but a class gives the data a stable API and keeps behavior next to the state it changes.\n\n`__init__` initializes each instance, and methods receive the instance as `self`. Separate instances keep separate state because each object has its own attributes.\n\n:::program\n```python\nclass Counter:\n step = 1\n\n def __init__(self, start=0):\n self.value = start\n\n def increment(self, amount=1):\n self.value += amount\n return self.value\n\nfirst = Counter()\nsecond = Counter(10)\n\nprint(first.value)\nprint(second.value)\nprint(first.increment())\nprint(second.increment(5))\nprint(first.step)\nCounter.step = 5\nprint(second.step)\n\nclass Cart:\n items = []\n\n def add(self, item):\n self.items.append(item)\n\nshared_a = Cart()\nshared_b = Cart()\nshared_a.add("apple")\nprint(shared_b.items)\n\nclass FixedCart:\n def __init__(self):\n self.items = []\n\n def add(self, item):\n self.items.append(item)\n\nown_a = FixedCart()\nown_b = FixedCart()\nown_a.add("apple")\nprint(own_b.items)\n```\n:::\n\n:::cell\nDefine a class when data and behavior should travel together. The initializer gives each object its starting state.\n\n```python\nclass Counter:\n def __init__(self, start=0):\n self.value = start\n\nfirst = Counter()\nsecond = Counter(10)\nprint(first.value)\nprint(second.value)\n```\n\n```output\n0\n10\n```\n:::\n\n:::cell\nMethods are functions attached to the class. `self` is the particular object receiving the method call, so separate instances keep separate state.\n\n```python\nclass Counter:\n def __init__(self, start=0):\n self.value = start\n\n def increment(self, amount=1):\n self.value += amount\n return self.value\n\nfirst = Counter()\nsecond = Counter(10)\nprint(first.increment())\nprint(second.increment(5))\n```\n\n```output\n1\n15\n```\n:::\n\n:::cell\nA name defined directly on the class body is a class attribute, shared by every instance. Reading falls back to the class when the instance has no attribute of that name; assigning to the class itself changes the value for every instance at once.\n\n```python\nclass Counter:\n step = 1\n\n def __init__(self, start=0):\n self.value = start\n\nfirst = Counter()\nsecond = Counter()\nprint(first.step)\nCounter.step = 5\nprint(second.step)\n```\n\n```output\n1\n5\n```\n:::\n\n:::cell\nA mutable class attribute is shared mutable state — the classic footgun. Define per-instance containers in `__init__` so each object owns its own copy.\n\n```python\nclass Cart:\n items = []\n\n def add(self, item):\n self.items.append(item)\n\nshared_a = Cart()\nshared_b = Cart()\nshared_a.add("apple")\nprint(shared_b.items)\n\nclass FixedCart:\n def __init__(self):\n self.items = []\n\n def add(self, item):\n self.items.append(item)\n\nown_a = FixedCart()\nown_b = FixedCart()\nown_a.add("apple")\nprint(own_b.items)\n```\n\n```output\n[\'apple\']\n[]\n```\n:::\n\n:::note\n- `self` is the instance the method is operating on.\n- `__init__` initializes each new object.\n- Class attributes are shared across instances; instance attributes belong to one object.\n- Put mutable defaults in `__init__`, not on the class body.\n- Use classes when behavior belongs with state; use dictionaries for looser structured data.\n:::\n', 'classmethods-and-staticmethods.md': '+++\nslug = "classmethods-and-staticmethods"\ntitle = "Classmethods and Staticmethods"\nsection = "Classes"\nsummary = "Three method shapes: instance, class, and static — each receives a different first argument."\ndoc_path = "/library/functions.html#classmethod"\nsee_also = [\n "classes",\n "decorators",\n "inheritance-and-super",\n]\n+++\n\nA regular method receives the instance as `self`. `@classmethod` makes a method receive the class as `cls` instead, which is the standard shape for alternate constructors. `@staticmethod` removes the implicit first argument entirely, leaving a plain function attached to the class for namespacing.\n\nThe pressure that justifies the decorators is name organization. `Date.from_string("2026-05-09")` reads better than a free-floating `parse_date` function, and `Date.is_leap_year(2024)` keeps the helper next to the class it belongs to even when the helper does not need any class state.\n\nPick instance methods when the work depends on instance state, classmethods when an alternate constructor or class-level operation is the right shape, and staticmethods when the function only happens to live near a class.\n\n:::program\n```python\nclass Date:\n def __init__(self, year, month, day):\n self.year = year\n self.month = month\n self.day = day\n\n def display(self):\n return f"{self.year}-{self.month:02d}-{self.day:02d}"\n\n @classmethod\n def from_string(cls, text):\n year, month, day = (int(part) for part in text.split("-"))\n return cls(year, month, day)\n\n @staticmethod\n def is_leap_year(year):\n return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)\n\ntoday = Date(2026, 5, 9)\nprint(today.display())\n\nlater = Date.from_string("2026-12-31")\nprint(later.display())\n\nprint(Date.is_leap_year(2024))\nprint(Date.is_leap_year(2025))\n\nclass Demo:\n def instance_method(self):\n return type(self).__name__\n\n @classmethod\n def class_method(cls):\n return cls.__name__\n\n @staticmethod\n def static_method():\n return "no receiver"\n\nprint(Demo().instance_method())\nprint(Demo.class_method())\nprint(Demo.static_method())\n```\n:::\n\n:::cell\nAn instance method receives the instance as `self` and reads its state. This is the default and the right shape when the work depends on a particular object\'s data.\n\n```python\nclass Date:\n def __init__(self, year, month, day):\n self.year = year\n self.month = month\n self.day = day\n\n def display(self):\n return f"{self.year}-{self.month:02d}-{self.day:02d}"\n\ntoday = Date(2026, 5, 9)\nprint(today.display())\n```\n\n```output\n2026-05-09\n```\n:::\n\n:::cell\n`@classmethod` makes the method receive the class itself as `cls`. The canonical use is an alternate constructor that parses some other input format and calls `cls(...)`. Because `cls` is the actual class, subclasses calling the same method get an instance of their own type.\n\n```python\nclass Date:\n def __init__(self, year, month, day):\n self.year = year\n self.month = month\n self.day = day\n\n @classmethod\n def from_string(cls, text):\n year, month, day = (int(part) for part in text.split("-"))\n return cls(year, month, day)\n\nlater = Date.from_string("2026-12-31")\nprint(later.year, later.month, later.day)\n```\n\n```output\n2026 12 31\n```\n:::\n\n:::cell\n`@staticmethod` strips the implicit first argument. The function lives on the class for namespacing — like `Date.is_leap_year(2024)` — but does not touch any instance or class state.\n\n```python\nclass Date:\n @staticmethod\n def is_leap_year(year):\n return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)\n\nprint(Date.is_leap_year(2024))\nprint(Date.is_leap_year(2025))\n```\n\n```output\nTrue\nFalse\n```\n:::\n\n:::cell\nSide by side: instance methods receive the instance, classmethods receive the class, staticmethods receive nothing. Classmethods and staticmethods can be called on either the class or an instance.\n\n```python\nclass Demo:\n def instance_method(self):\n return type(self).__name__\n\n @classmethod\n def class_method(cls):\n return cls.__name__\n\n @staticmethod\n def static_method():\n return "no receiver"\n\nprint(Demo().instance_method())\nprint(Demo.class_method())\nprint(Demo.static_method())\n```\n\n```output\nDemo\nDemo\nno receiver\n```\n:::\n\n:::note\n- Instance methods need an instance; classmethods and staticmethods can be called on the class.\n- Use `@classmethod` for alternate constructors and class-level operations that respect subclassing.\n- Use `@staticmethod` only when a function is truly independent of instance and class state but still belongs in the class\'s namespace.\n- A free function is often the right answer when neither decorator applies.\n:::\n', 'closures.md': '+++\nslug = "closures"\ntitle = "Closures"\nsection = "Functions"\nsummary = "Inner functions can remember values from an enclosing scope."\ndoc_path = "/reference/executionmodel.html#binding-of-names"\n+++\n\nA closure is a function that remembers names from the scope where it was created. This lets you configure behavior once and call it later.\n\nEach call to the outer function creates a separate remembered environment. That is why `double` and `triple` can share the same code but keep different factors.\n\nClosures are a foundation for decorators, callbacks, and small function factories.\n\n:::program\n```python\ndef make_multiplier(factor):\n def multiply(value):\n return value * factor\n return multiply\n\ndouble = make_multiplier(2)\nprint(double(5))\n\ntriple = make_multiplier(3)\nprint(triple(5))\n\nlate = []\nfor i in range(3):\n late.append(lambda: i)\nprint([f() for f in late])\n\nbound = []\nfor i in range(3):\n bound.append(lambda i=i: i)\nprint([f() for f in bound])\n```\n:::\n\n:::cell\nDefine a function inside another function when the inner behavior needs to remember setup from the outer call. The returned function keeps access to `factor`.\n\n```python\ndef make_multiplier(factor):\n def multiply(value):\n return value * factor\n return multiply\n\ndouble = make_multiplier(2)\nprint(double(5))\n```\n\n```output\n10\n```\n:::\n\n:::cell\nCalling the outer function again creates a separate closure. `triple` uses the same inner code, but remembers a different `factor`.\n\n```python\ntriple = make_multiplier(3)\nprint(triple(5))\n```\n\n```output\n15\n```\n:::\n\n:::cell\nClosures bind names, not values. Lambdas defined in a loop all reference the same loop variable, so calling them later sees its final value. Capture the value at definition time by binding it as a default argument — `lambda i=i: i` — so each closure remembers its own `i`.\n\n```python\nlate = []\nfor i in range(3):\n late.append(lambda: i)\nprint([f() for f in late])\n\nbound = []\nfor i in range(3):\n bound.append(lambda i=i: i)\nprint([f() for f in bound])\n```\n\n```output\n[2, 2, 2]\n[0, 1, 2]\n```\n:::\n\n:::note\n- A closure keeps access to names from the scope where the inner function was created.\n- Each call to the outer function can create a separate remembered environment.\n- Closures are useful for callbacks, small factories, and decorators.\n- Closures bind names, not values; capture loop variables with `lambda x=x: ...` to freeze them at definition time.\n:::\n', 'collections-module.md': '+++\nslug = "collections-module"\ntitle = "Collections Module"\nsection = "Collections"\nsummary = "collections provides specialized containers for common data shapes."\ndoc_path = "/library/collections.html"\n+++\n\ncollections provides specialized containers for common data shapes. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nfrom collections import Counter, defaultdict, deque, namedtuple\n\ncounts = Counter("banana")\nprint(counts.most_common(2))\n\ngroups = defaultdict(list)\nfor name, team in [("Ada", "red"), ("Grace", "blue"), ("Lin", "red")]:\n groups[team].append(name)\nprint(dict(groups))\n\nqueue = deque(["first"])\nqueue.append("second")\nprint(queue.popleft())\n\nPoint = namedtuple("Point", "x y")\nprint(Point(2, 3).x)\n```\n:::\n\n:::cell\nUse `Counter` when counting is the data shape.\n\n```python\nfrom collections import Counter, defaultdict, deque, namedtuple\n\ncounts = Counter("banana")\nprint(counts.most_common(2))\n\ngroups = defaultdict(list)\nfor name, team in [("Ada", "red"), ("Grace", "blue"), ("Lin", "red")]:\n groups[team].append(name)\nprint(dict(groups))\n\nqueue = deque(["first"])\nqueue.append("second")\nprint(queue.popleft())\n\nPoint = namedtuple("Point", "x y")\nprint(Point(2, 3).x)\n```\n\n```output\n[(\'a\', 3), (\'n\', 2)]\n{\'red\': [\'Ada\', \'Lin\'], \'blue\': [\'Grace\']}\nfirst\n2\n```\n:::\n\n:::note\n- Use `Counter` when counting is the data shape.\n- Use `defaultdict` when grouping values by key.\n- Use `deque` for efficient queue operations and `namedtuple` for lightweight named records.\n:::\n', 'comprehension-patterns.md': '+++\nslug = "comprehension-patterns"\ntitle = "Comprehension Patterns"\nsection = "Collections"\nsummary = "Comprehensions can use multiple for clauses and filters when the shape stays clear."\ndoc_path = "/tutorial/datastructures.html#list-comprehensions"\nsee_also = [\n "comprehensions",\n "generator-expressions",\n "for-loops",\n]\n+++\n\nComprehensions can contain more than one `for` clause and more than one `if` filter. The clauses are read in the same order as nested loops.\n\nUse these forms only while the shape remains easy to scan. If a comprehension starts needing several names, comments, or branches, an explicit loop is usually better.\n\nNested comprehensions build concrete collections immediately, just like simpler list, dict, and set comprehensions.\n\n:::program\n```python\ncolors = ["red", "blue"]\nsizes = ["S", "M"]\nvariants = [(color, size) for color in colors for size in sizes]\nprint(variants)\n\nnumbers = range(10)\nfiltered = [n for n in numbers if n % 2 == 0 if n > 2]\nprint(filtered)\n```\n:::\n\n:::cell\nMultiple `for` clauses behave like nested loops. The leftmost `for` is the outer loop, and the next `for` runs inside it.\n\n```python\ncolors = ["red", "blue"]\nsizes = ["S", "M"]\nvariants = [(color, size) for color in colors for size in sizes]\nprint(variants)\n```\n\n```output\n[(\'red\', \'S\'), (\'red\', \'M\'), (\'blue\', \'S\'), (\'blue\', \'M\')]\n```\n:::\n\n:::cell\nMultiple `if` clauses filter values. They are useful for simple conditions, but an explicit loop is clearer when the rules need names or explanation.\n\n```python\nnumbers = range(10)\nfiltered = [n for n in numbers if n % 2 == 0 if n > 2]\nprint(filtered)\n```\n\n```output\n[4, 6, 8]\n```\n:::\n\n:::note\n- Read comprehension clauses from left to right.\n- Multiple `for` clauses act like nested loops.\n- Prefer an explicit loop when the comprehension stops being obvious.\n:::\n', 'comprehensions.md': '+++\nslug = "comprehensions"\ntitle = "Comprehensions"\nsection = "Collections"\nsummary = "Comprehensions build collections by mapping and filtering iterables."\ndoc_path = "/tutorial/datastructures.html#list-comprehensions"\n+++\n\nComprehensions are expression forms for building concrete collections from iterables. Read them from left to right: produce this value, for each item, optionally only when a condition is true.\n\nThey are best for direct transformations where the expression is still easy to scan. When the work needs several statements or names, an explicit loop is usually clearer.\n\nList, dictionary, and set comprehensions are eager: they build collections immediately. Generator expressions use similar syntax to stream values later and are covered in the Iteration section.\n\n:::program\n```python\nnames = ["ada", "guido", "grace"]\ntitled = [name.title() for name in names]\nprint(titled)\n\nscores = {"Ada": 10, "Guido": 8, "Grace": 10}\nhigh_scores = {name: score for name, score in scores.items() if score >= 10}\nprint(high_scores)\n\nunique_scores = {score for score in scores.values()}\nprint(unique_scores)\n```\n:::\n\n:::cell\nA list comprehension maps each input item to one output item. This one calls `title()` for every name and collects the results in a new list.\n\n```python\nnames = ["ada", "guido", "grace"]\ntitled = [name.title() for name in names]\nprint(titled)\n```\n\n```output\n[\'Ada\', \'Guido\', \'Grace\']\n```\n:::\n\n:::cell\nAdd an `if` clause when only some items should appear. A dictionary comprehension can transform key/value pairs while preserving the dictionary shape.\n\n```python\nscores = {"Ada": 10, "Guido": 8, "Grace": 10}\nhigh_scores = {name: score for name, score in scores.items() if score >= 10}\nprint(high_scores)\n```\n\n```output\n{\'Ada\': 10, \'Grace\': 10}\n```\n:::\n\n:::cell\nA set comprehension keeps only unique results. Here two people have the same score, so the resulting set has two values.\n\n```python\nunique_scores = {score for score in scores.values()}\nprint(unique_scores)\n```\n\n```output\n{8, 10}\n```\n:::\n\n:::note\n- The left side says what to produce; the `for` clause says where values come from.\n- Use an `if` clause for simple filters.\n- List, dict, and set comprehensions build concrete collections immediately.\n- Switch to a loop when the transformation needs multiple steps or explanations.\n:::\n', 'conditionals.md': '+++\nslug = "conditionals"\ntitle = "Conditionals"\nsection = "Control Flow"\nsummary = "if, elif, and else choose which block runs."\ndoc_path = "/tutorial/controlflow.html#if-statements"\n+++\n\n`if`, `elif`, and `else` let a program choose one path based on a condition. Python uses indentation to show which statements belong to each branch.\n\nConditions use Python truthiness: booleans work directly, and many objects such as empty lists or empty strings are considered false. Order branches from most specific to most general.\n\nUse `elif` to keep one decision flat instead of nested. Use Python\'s ternary expression only when you are choosing between two values.\n\n:::program\n```python\ntemperature = 72\n\nif temperature < 60:\n print("cold")\nelif temperature < 80:\n print("comfortable")\nelse:\n print("hot")\n\nitems = ["coat", "hat"]\nif items:\n print(f"packing {len(items)} items")\n\nstatus = "ok" if temperature < 90 else "danger"\nprint(status)\n```\n:::\n\n:::cell\nStart with the value that the branches will test. A conditional is only useful when the branch condition is visible and meaningful.\n\nUse `if`, `elif`, and `else` for one ordered choice. Python tests the branches from top to bottom and runs only the first matching block.\n\n```python\ntemperature = 72\n\nif temperature < 60:\n print("cold")\nelif temperature < 80:\n print("comfortable")\nelse:\n print("hot")\n```\n\n```output\ncomfortable\n```\n:::\n\n:::cell\nTruthiness is part of conditional flow. Empty collections are false, so `if items:` reads as “if there is anything to work with.”\n\n```python\nitems = ["coat", "hat"]\nif items:\n print(f"packing {len(items)} items")\n```\n\n```output\npacking 2 items\n```\n:::\n\n:::cell\nUse the ternary expression when you are choosing a value. If either side needs multiple statements, use a normal `if` block instead.\n\n```python\nstatus = "ok" if temperature < 90 else "danger"\nprint(status)\n```\n\n```output\nok\n```\n:::\n\n:::note\n- Python has no mandatory parentheses around conditions; the colon and indentation define the block.\n- Comparison operators such as `<` and `==` can be chained, as in `0 < value < 10`.\n- Keep branch bodies short; move larger work into functions so the decision remains easy to scan.\n:::\n', 'constants.md': '+++\nslug = "constants"\ntitle = "Constants"\nsection = "Basics"\nsummary = "Python uses naming conventions for values that should not change."\ndoc_path = "/tutorial/classes.html#python-scopes-and-namespaces"\n+++\n\nPython has no `const` keyword for ordinary variables. Instead, modules use all-caps names to mark values that should be treated as constants by convention.\n\nThe interpreter will not stop rebinding, but the convention is important API communication. Readers understand that `MAX_RETRIES` is configuration, not loop state.\n\nNamed constants remove magic values from code and give repeated literals one place to change.\n\n:::program\n```python\nMAX_RETRIES = 3\nAPI_VERSION = "2026-05"\n\nfor attempt in range(1, MAX_RETRIES + 1):\n print(f"attempt {attempt} of {MAX_RETRIES}")\n\nprint(API_VERSION)\n```\n:::\n\n:::cell\nPython does not have a `const` declaration like Go or Rust. Instead, modules use all-caps names for values callers should treat as fixed.\n\nThe interpreter will still let you rebind the name, but the convention is strong enough that readers understand the design intent.\n\n```python\nMAX_RETRIES = 3\nAPI_VERSION = "2026-05"\n\nfor attempt in range(1, MAX_RETRIES + 1):\n print(f"attempt {attempt} of {MAX_RETRIES}")\n```\n\n```output\nattempt 1 of 3\nattempt 2 of 3\nattempt 3 of 3\n```\n:::\n\n:::cell\nConstants are useful for configuration values that should be named once and reused instead of repeated as magic literals.\n\n```python\nprint(API_VERSION)\n```\n\n```output\n2026-05\n```\n:::\n\n:::note\n- Python has no `const` keyword for ordinary names.\n- All-caps names such as `MAX_RETRIES` communicate that a value is intended to stay fixed.\n:::\n', 'container-protocols.md': '+++\nslug = "container-protocols"\ntitle = "Container Protocols"\nsection = "Data Model"\nsummary = "Container methods connect objects to indexing, membership, and item assignment."\ndoc_path = "/reference/datamodel.html#emulating-container-types"\nsee_also = [\n "lists",\n "dicts",\n "special-methods",\n]\n+++\n\nContainer protocols let a class behave like the collection it represents. Instead of inventing method names such as `has()` or `lookup()`, the object can support `in`, indexing, and assignment.\n\nThe key methods are small and familiar: `__contains__` powers `in`, `__getitem__` powers `obj[key]`, and `__setitem__` powers `obj[key] = value`. Add only the operations the object can honestly support.\n\nThis keeps the public interface aligned with Python\'s built-in containers. Callers can use the same syntax for custom records, caches, tables, and sequence-like objects.\n\n:::program\n```python\nclass Scores:\n def __init__(self):\n self._scores = {}\n\n def __contains__(self, name):\n return name in self._scores\n\n def __getitem__(self, name):\n return self._scores[name]\n\n def __setitem__(self, name, score):\n self._scores[name] = score\n\nscores = Scores()\nscores["Ada"] = 98\nprint("Ada" in scores)\nprint(scores["Ada"])\n```\n:::\n\n:::cell\n`__setitem__` gives assignment syntax to a custom container.\n\n```python\nclass Scores:\n def __init__(self):\n self._scores = {}\n\n def __setitem__(self, name, score):\n self._scores[name] = score\n\nscores = Scores()\nscores["Ada"] = 98\nprint(scores._scores)\n```\n\n```output\n{\'Ada\': 98}\n```\n:::\n\n:::cell\n`__contains__` answers membership tests written with `in`.\n\n```python\nclass Scores:\n def __init__(self):\n self._scores = {"Ada": 98}\n\n def __contains__(self, name):\n return name in self._scores\n\nscores = Scores()\nprint("Ada" in scores)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\n`__getitem__` connects bracket lookup to your internal storage.\n\n```python\nclass Scores:\n def __init__(self):\n self._scores = {"Ada": 98}\n\n def __getitem__(self, name):\n return self._scores[name]\n\nscores = Scores()\nprint(scores["Ada"])\n```\n\n```output\n98\n```\n:::\n\n:::note\n- Implement the narrowest container protocol your object needs.\n- Use `KeyError` and `IndexError` consistently with built-in containers.\n- If a plain `dict` or `list` is enough, prefer it over a custom container.\n:::\n', 'context-managers.md': '+++\nslug = "context-managers"\ntitle = "Context Managers"\nsection = "Data Model"\nsummary = "with ensures setup and cleanup happen together."\ndoc_path = "/reference/datamodel.html#context-managers"\nsee_also = [\n "exceptions",\n "special-methods",\n "descriptors",\n]\n+++\n\nContext managers define setup and cleanup around a block of code. The `with` statement guarantees that cleanup runs when the block exits, even when an exception is raised.\n\nThe protocol is powered by `__enter__` and `__exit__`. The `contextlib.contextmanager` decorator is a concise way to write the same idea as a generator when a full class would be noisy.\n\nProduction code often uses `with` for files, locks, transactions, temporary state, and resources that need reliable release.\n\n:::program\n```python\nfrom contextlib import contextmanager\n\nclass Tag:\n def __init__(self, name):\n self.name = name\n\n def __enter__(self):\n print(f"<{self.name}>")\n return self\n\n def __exit__(self, exc_type, exc, tb):\n print(f"")\n return False\n\n@contextmanager\ndef tag(name):\n print(f"<{name}>")\n try:\n yield\n finally:\n print(f"")\n\nwith Tag("section"):\n print("content")\n\ntry:\n with tag("error"):\n raise ValueError("boom")\nexcept ValueError:\n print("handled")\n```\n:::\n\n:::cell\nA class-based context manager implements `__enter__` and `__exit__`. The value returned by `__enter__` is bound by `as` when the `with` statement uses it.\n\n```python\nclass Tag:\n def __init__(self, name):\n self.name = name\n\n def __enter__(self):\n print(f"<{self.name}>")\n return self\n\n def __exit__(self, exc_type, exc, tb):\n print(f"")\n return False\n\nwith Tag("section"):\n print("content")\n```\n\n```output\n
    \ncontent\n
    \n```\n:::\n\n:::cell\n`contextlib.contextmanager` writes the same setup/cleanup shape as a generator. Code before `yield` is setup, and code after `yield` is cleanup.\n\n```python\nfrom contextlib import contextmanager\n\n@contextmanager\ndef tag(name):\n print(f"<{name}>")\n try:\n yield\n finally:\n print(f"")\n\nwith tag("note"):\n print("body")\n```\n\n```output\n\nbody\n\n```\n:::\n\n:::cell\nCleanup still runs when the block raises. Returning `False` from `__exit__`, or letting a generator context manager re-raise, allows the exception to keep propagating.\n\n```python\ntry:\n with tag("error"):\n raise ValueError("boom")\nexcept ValueError:\n print("handled")\n```\n\n```output\n\n\nhandled\n```\n:::\n\n:::note\n- Files, locks, and temporary state commonly use context managers.\n- `__enter__` and `__exit__` power the protocol.\n- Use `finally` when cleanup must happen after errors too.\n- Returning true from `__exit__` suppresses an exception; do that only intentionally.\n:::\n', 'copying-collections.md': '+++\nslug = "copying-collections"\ntitle = "Copying Collections"\nsection = "Collections"\nsummary = "Copies can duplicate a container while still sharing nested objects."\ndoc_path = "/library/copy.html"\n+++\n\nCopies can duplicate a container while still sharing nested objects. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nimport copy\n\nrows = [["Ada"], ["Grace"]]\nshallow = rows.copy()\ndeep = copy.deepcopy(rows)\n\nrows[0].append("Lovelace")\n\nprint(shallow)\nprint(deep)\nprint(rows[0] is shallow[0])\nprint(rows[0] is deep[0])\n```\n:::\n\n:::cell\nA shallow copy makes a new outer container.\n\n```python\nimport copy\n\nrows = [["Ada"], ["Grace"]]\nshallow = rows.copy()\ndeep = copy.deepcopy(rows)\n\nrows[0].append("Lovelace")\n\nprint(shallow)\nprint(deep)\nprint(rows[0] is shallow[0])\nprint(rows[0] is deep[0])\n```\n\n```output\n[[\'Ada\', \'Lovelace\'], [\'Grace\']]\n[[\'Ada\'], [\'Grace\']]\nTrue\nFalse\n```\n:::\n\n:::note\n- A shallow copy makes a new outer container.\n- Nested objects are still shared by a shallow copy.\n- Use `copy.deepcopy()` only when nested independence is required.\n:::\n', 'csv-data.md': '+++\nslug = "csv-data"\ntitle = "CSV Data"\nsection = "Standard Library"\nsummary = "csv reads and writes row-shaped text data."\ndoc_path = "/library/csv.html"\n+++\n\ncsv reads and writes row-shaped text data. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nimport csv\nimport io\n\ntext = "name,score\\nAda,98\\nGrace,95\\n"\nreader = csv.DictReader(io.StringIO(text))\nrows = list(reader)\n\nprint(rows[0]["name"])\nprint(sum(int(row["score"]) for row in rows))\n\noutput = io.StringIO()\nwriter = csv.DictWriter(output, fieldnames=["name", "passed"])\nwriter.writeheader()\nwriter.writerow({"name": "Ada", "passed": True})\nprint(output.getvalue().splitlines()[1])\n```\n:::\n\n:::cell\nUse `DictReader` when column names should become dictionary keys.\n\n```python\nimport csv\nimport io\n\ntext = "name,score\\nAda,98\\nGrace,95\\n"\nreader = csv.DictReader(io.StringIO(text))\nrows = list(reader)\n\nprint(rows[0]["name"])\nprint(sum(int(row["score"]) for row in rows))\n\noutput = io.StringIO()\nwriter = csv.DictWriter(output, fieldnames=["name", "passed"])\nwriter.writeheader()\nwriter.writerow({"name": "Ada", "passed": True})\nprint(output.getvalue().splitlines()[1])\n```\n\n```output\nAda\n193\nAda,True\n```\n:::\n\n:::note\n- Use `DictReader` when column names should become dictionary keys.\n- CSV fields arrive as text, so convert numbers explicitly.\n- `DictWriter` writes dictionaries back to row-shaped text.\n:::\n', 'custom-exceptions.md': '+++\nslug = "custom-exceptions"\ntitle = "Custom Exceptions"\nsection = "Errors"\nsummary = "Custom exception classes name failures that belong to your domain."\ndoc_path = "/tutorial/errors.html#user-defined-exceptions"\n+++\n\nCustom exceptions give names to failures in your problem domain. A named exception is easier to catch and explain than a generic error with only a string message.\n\nRaise the custom exception at the point where the invalid state is discovered. Include a message for the specific occurrence.\n\nCatch custom exceptions at the boundary where recovery makes sense, such as returning an error response or asking for corrected input.\n\n:::program\n```python\nclass EmptyCartError(Exception):\n pass\n\nprint(EmptyCartError.__name__)\n\n\ndef checkout(items):\n if not items:\n raise EmptyCartError("cart is empty")\n return "paid"\n\nprint(checkout(["book"]))\n\ntry:\n checkout([])\nexcept EmptyCartError as error:\n print(error)\n```\n:::\n\n:::cell\nCreate a custom exception when a failure has a name in your problem domain. The class can be empty at first.\n\n```python\nclass EmptyCartError(Exception):\n pass\n\nprint(EmptyCartError.__name__)\n```\n\n```output\nEmptyCartError\n```\n:::\n\n:::cell\nRaise the custom exception where the invalid state is detected. Normal inputs still follow the ordinary success path.\n\n```python\ndef checkout(items):\n if not items:\n raise EmptyCartError("cart is empty")\n return "paid"\n\nprint(checkout(["book"]))\n```\n\n```output\npaid\n```\n:::\n\n:::cell\nCallers can catch the precise error type without accidentally catching unrelated failures.\n\n```python\ntry:\n checkout([])\nexcept EmptyCartError as error:\n print(error)\n```\n\n```output\ncart is empty\n```\n:::\n\n:::note\n- Subclass `Exception` for errors callers are expected to catch.\n- A custom exception name can be clearer than reusing a generic `ValueError` everywhere.\n- Catch custom exceptions at a boundary that can recover or report clearly.\n:::\n', 'dataclasses.md': '+++\nslug = "dataclasses"\ntitle = "Dataclasses"\nsection = "Classes"\nsummary = "dataclass generates common class methods for data containers."\ndoc_path = "/library/dataclasses.html"\nsee_also = [\n "structured-data-shapes",\n "classes",\n "type-hints",\n]\n+++\n\n`dataclass` is a standard-library decorator for classes that mainly store data. It generates methods such as `__init__` and `__repr__` from type-annotated fields.\n\nDataclasses reduce boilerplate while keeping classes explicit. They are a good fit for simple records, configuration objects, and values passed between layers.\n\nType annotations define fields. Defaults work like normal class attributes and appear in the generated initializer.\n\n:::program\n```python\nfrom dataclasses import dataclass\n\n@dataclass\nclass User:\n name: str\n active: bool = True\n\nuser = User("Ada")\nprint(user)\nprint(user.name)\n\ninactive = User("Guido", active=False)\nprint(inactive)\nprint(inactive.active)\n```\n:::\n\n:::cell\nA dataclass uses annotations to define fields. Python generates an initializer, so the class can be constructed without writing `__init__` by hand.\n\n```python\nfrom dataclasses import dataclass\n\n@dataclass\nclass User:\n name: str\n active: bool = True\n\nuser = User("Ada")\nprint(user)\n```\n\n```output\nUser(name=\'Ada\', active=True)\n```\n:::\n\n:::cell\nThe generated instance still exposes ordinary attributes. A dataclass is a regular class with useful methods filled in.\n\n```python\nprint(user.name)\n```\n\n```output\nAda\n```\n:::\n\n:::cell\nDefaults can be overridden by keyword. The generated representation includes the field names, which is useful during debugging.\n\n```python\ninactive = User("Guido", active=False)\nprint(inactive)\nprint(inactive.active)\n```\n\n```output\nUser(name=\'Guido\', active=False)\nFalse\n```\n:::\n\n:::note\n- Type annotations define dataclass fields.\n- Dataclasses generate methods but remain normal Python classes.\n- Use `field()` for advanced defaults such as per-instance lists or dictionaries.\n:::\n', 'datetime.md': '+++\nslug = "datetime"\ntitle = "Dates and Times"\nsection = "Standard Library"\nsummary = "datetime represents dates, times, durations, formatting, and parsing."\ndoc_path = "/library/datetime.html"\n+++\n\nThe `datetime` module covers several related ideas: `date` for calendar days, `time` for clock times, `datetime` for both together, and `timedelta` for durations.\n\nTimezone-aware datetimes avoid ambiguity in real systems. `timezone.utc` is a clear default for examples because output stays stable and portable.\n\nUse ISO formatting for interchange, `strftime()` for display, and parsing helpers such as `fromisoformat()` to turn text back into datetime objects.\n\n:::program\n```python\nfrom datetime import date, datetime, time, timedelta, timezone\n\nrelease_day = date(2026, 5, 4)\nmeeting_time = time(12, 30)\ncreated_at = datetime.combine(release_day, meeting_time, tzinfo=timezone.utc)\n\nprint(release_day.isoformat())\nprint(meeting_time.isoformat())\nprint(created_at.isoformat())\n\nexpires_at = created_at + timedelta(days=7, hours=2)\nprint(expires_at.isoformat())\n\nprint(created_at.strftime("%Y-%m-%d %H:%M %Z"))\nparsed = datetime.fromisoformat("2026-05-04T12:30:00+00:00")\nprint(parsed == created_at)\n```\n:::\n\n:::cell\nThe `datetime` module separates calendar dates, clock times, combined datetimes, and durations. Import the types you need explicitly.\n\nUse `date` for a calendar day and `time` for a time of day. Combine them into a timezone-aware `datetime` when you mean an instant.\n\n`isoformat()` produces stable machine-readable text. It is a good default for examples, APIs, and logs.\n\n```python\nfrom datetime import date, datetime, time, timedelta, timezone\n\nrelease_day = date(2026, 5, 4)\nmeeting_time = time(12, 30)\ncreated_at = datetime.combine(release_day, meeting_time, tzinfo=timezone.utc)\n\nprint(release_day.isoformat())\nprint(meeting_time.isoformat())\nprint(created_at.isoformat())\n```\n\n```output\n2026-05-04\n12:30:00\n2026-05-04T12:30:00+00:00\n```\n:::\n\n:::cell\nUse `timedelta` for durations. Adding one to a `datetime` produces another `datetime` without manually changing calendar fields.\n\n```python\nexpires_at = created_at + timedelta(days=7, hours=2)\nprint(expires_at.isoformat())\n```\n\n```output\n2026-05-11T14:30:00+00:00\n```\n:::\n\n:::cell\nUse `strftime()` for human-facing formatting and `fromisoformat()` when reading ISO 8601 text back into a `datetime`.\n\n```python\nprint(created_at.strftime("%Y-%m-%d %H:%M %Z"))\nparsed = datetime.fromisoformat("2026-05-04T12:30:00+00:00")\nprint(parsed == created_at)\n```\n\n```output\n2026-05-04 12:30 UTC\nTrue\n```\n:::\n\n:::note\n- Use timezone-aware datetimes for instants that cross system or user boundaries.\n- Use `date` for calendar days, `time` for clock times, `datetime` for both, and `timedelta` for durations.\n- Prefer ISO 8601 strings for interchange; use `strftime` for human-facing display.\n:::\n', 'decorators.md': '+++\nslug = "decorators"\ntitle = "Decorators"\nsection = "Functions"\nsummary = "Decorators wrap or register functions using @ syntax."\ndoc_path = "/glossary.html#term-decorator"\nsee_also = [\n "closures",\n "functions",\n "callable-types",\n "classmethods-and-staticmethods",\n]\n+++\n\nA decorator is a callable that receives a function and returns a replacement. The `@` syntax applies that transformation at function definition time.\n\nDecorators are common in frameworks because they can register handlers or add behavior while keeping the decorated function focused on the core action.\n\n`@decorator` is shorthand for rebinding a function to the decorator\'s return value. Production wrappers usually use `functools.wraps` so debugging, help text, and framework introspection still see the original function metadata.\n\n:::program\n```python\nfrom functools import wraps\n\n\ndef loud(func):\n @wraps(func)\n def wrapper(name):\n return func(name).upper()\n return wrapper\n\n\ndef greet(name):\n return f"hello {name}"\n\nmanual_greet = loud(greet)\nprint(manual_greet("python"))\n\n@loud\ndef welcome(name):\n """Return a welcome message."""\n return f"welcome {name}"\n\nprint(welcome("workers"))\nprint(welcome.__name__)\n```\n:::\n\n:::cell\nA decorator is just a function that takes a function and returns another callable. Applying it manually shows the wrapping step.\n\n```python\nfrom functools import wraps\n\n\ndef loud(func):\n @wraps(func)\n def wrapper(name):\n return func(name).upper()\n return wrapper\n\n\ndef greet(name):\n return f"hello {name}"\n\nmanual_greet = loud(greet)\nprint(manual_greet("python"))\n```\n\n```output\nHELLO PYTHON\n```\n:::\n\n:::cell\nThe `@loud` syntax performs the same rebinding at definition time. After decoration, `welcome` refers to the wrapper returned by `loud`.\n\n```python\n@loud\ndef welcome(name):\n """Return a welcome message."""\n return f"welcome {name}"\n\nprint(welcome("workers"))\n```\n\n```output\nWELCOME WORKERS\n```\n:::\n\n:::cell\n`functools.wraps` copies useful metadata from the original function onto the wrapper.\n\n```python\nprint(welcome.__name__)\nprint(welcome.__doc__)\n```\n\n```output\nwelcome\nReturn a welcome message.\n```\n:::\n\n:::note\n- `@decorator` is shorthand for assigning `func = decorator(func)`.\n- Decorators can wrap, replace, or register functions.\n- Use `functools.wraps` in production wrappers that should preserve metadata.\n:::\n', 'delete-statements.md': '+++\nslug = "delete-statements"\ntitle = "Delete Statements"\nsection = "Data Model"\nsummary = "del removes bindings, items, and attributes rather than producing a value."\ndoc_path = "/reference/simple_stmts.html#the-del-statement"\nsee_also = [\n "variables",\n "dicts",\n "mutability",\n]\n+++\n\n`del` removes a binding or an item. It is a statement, not a function, and it does not return the removed value.\n\nUse `del name` when a name should no longer be bound. Use `del mapping[key]` or `del sequence[index]` when mutating a container by removing one part.\n\nThis is different from assigning `None`: `None` is still a value, while `del` removes the binding or slot.\n\n:::program\n```python\nprofile = {"name": "Ada", "temporary": True}\ndel profile["temporary"]\nprint(profile)\n\nitems = ["a", "b", "c"]\ndel items[1]\nprint(items)\n\nvalue = "cached"\ndel value\nprint("value" in locals())\n```\n:::\n\n:::cell\nDeleting a dictionary key mutates the dictionary. The key is gone; it has not been set to `None`.\n\n```python\nprofile = {"name": "Ada", "temporary": True}\ndel profile["temporary"]\nprint(profile)\n```\n\n```output\n{\'name\': \'Ada\'}\n```\n:::\n\n:::cell\nDeleting a list item removes that position and shifts later items left.\n\n```python\nitems = ["a", "b", "c"]\ndel items[1]\nprint(items)\n```\n\n```output\n[\'a\', \'c\']\n```\n:::\n\n:::cell\nDeleting a name removes the binding from the current namespace. It is different from rebinding the name to `None`.\n\n```python\nvalue = "cached"\ndel value\nprint("value" in locals())\n```\n\n```output\nFalse\n```\n:::\n\n:::note\n- `del` removes bindings or container entries.\n- Assign `None` when absence should remain an explicit value.\n- Use container methods such as `pop()` when you need the removed value back.\n:::\n', 'descriptors.md': '+++\nslug = "descriptors"\ntitle = "Descriptors"\nsection = "Data Model"\nsummary = "Descriptors customize attribute access through __get__, __set__, or __delete__."\ndoc_path = "/howto/descriptor.html"\nsee_also = [\n "attribute-access",\n "properties",\n "bound-and-unbound-methods",\n]\n+++\n\nDescriptors customize attribute access through __get__, __set__, or __delete__. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nclass Positive:\n def __set_name__(self, owner, name):\n self.private_name = "_" + name\n\n def __get__(self, obj, owner):\n return getattr(obj, self.private_name)\n\n def __set__(self, obj, value):\n if value <= 0:\n raise ValueError("must be positive")\n setattr(obj, self.private_name, value)\n\nclass Product:\n price = Positive()\n\n def __init__(self, price):\n self.price = price\n\nitem = Product(10)\nprint(item.price)\ntry:\n item.price = -1\nexcept ValueError as error:\n print(error)\n```\n:::\n\n:::cell\nA descriptor object lives on the class.\n\n```python\nclass Positive:\n def __set_name__(self, owner, name):\n self.private_name = "_" + name\n\n def __get__(self, obj, owner):\n return getattr(obj, self.private_name)\n\n def __set__(self, obj, value):\n if value <= 0:\n raise ValueError("must be positive")\n setattr(obj, self.private_name, value)\n\nclass Product:\n price = Positive()\n\n def __init__(self, price):\n self.price = price\n\nitem = Product(10)\nprint(item.price)\ntry:\n item.price = -1\nexcept ValueError as error:\n print(error)\n```\n\n```output\n10\nmust be positive\n```\n:::\n\n:::note\n- A descriptor object lives on the class.\n- Attribute access on instances calls descriptor methods.\n- Properties, methods, and many ORMs build on the descriptor protocol.\n:::\n', 'dicts.md': '+++\nslug = "dicts"\ntitle = "Dictionaries"\nsection = "Collections"\nsummary = "Dictionaries map keys to values for records, lookup, and structured data."\ndoc_path = "/tutorial/datastructures.html#dictionaries"\n+++\n\nDictionaries are Python\'s built-in mapping type. They exist for data where names or keys are more meaningful than numeric positions: records, lookup tables, counters, and JSON-like payloads.\n\nUse direct indexing when a key is required. Use `get()` when absence is expected and the code has a reasonable fallback.\n\nUnlike lists, dictionaries answer “what value belongs to this key?” rather than “what value is at this position?” Iterating with `items()` keeps each key next to its value.\n\n:::program\n```python\nprofile = {"name": "Ada", "language": "Python"}\nprofile["year"] = 1843\nprint(profile["name"])\nprint(profile.get("timezone", "UTC"))\n\nscores = {"Ada": 10, "Grace": 9}\nprint(scores["Grace"])\nprint(scores.get("Guido", 0))\n\nfor name, score in scores.items():\n print(f"{name}: {score}")\n\ninventory = {"apple": 0, "pear": 3, "plum": 0}\nfor name in list(inventory.keys()):\n if inventory[name] == 0:\n del inventory[name]\nprint(inventory)\n```\n:::\n\n:::cell\nUse a dictionary as a small record when fields have names. Direct indexing communicates that the key is required, while `get()` communicates that a missing key has a fallback.\n\n```python\nprofile = {"name": "Ada", "language": "Python"}\nprofile["year"] = 1843\nprint(profile["name"])\nprint(profile.get("timezone", "UTC"))\n```\n\n```output\nAda\nUTC\n```\n:::\n\n:::cell\nUse a dictionary as a lookup table when keys identify values. This is different from a list, where numeric position is the lookup key.\n\n```python\nscores = {"Ada": 10, "Grace": 9}\nprint(scores["Grace"])\nprint(scores.get("Guido", 0))\n```\n\n```output\n9\n0\n```\n:::\n\n:::cell\nUse `items()` when the loop needs both keys and values. It avoids looping over keys and then indexing back into the dictionary.\n\n```python\nfor name, score in scores.items():\n print(f"{name}: {score}")\n```\n\n```output\nAda: 10\nGrace: 9\n```\n:::\n\n:::cell\nMutating a dictionary while iterating it raises `RuntimeError`. Snapshot the keys with `list(d.keys())` (or build a list of changes and apply them after the loop) so the iteration sees a stable view.\n\n```python\ninventory = {"apple": 0, "pear": 3, "plum": 0}\nfor name in list(inventory.keys()):\n if inventory[name] == 0:\n del inventory[name]\nprint(inventory)\n```\n\n```output\n{\'pear\': 3}\n```\n:::\n\n:::note\n- Dictionaries preserve insertion order in modern Python.\n- Use `get()` when a missing key has a reasonable default.\n- Use direct indexing when a missing key should be treated as an error.\n- Snapshot keys with `list(d.keys())` before deleting items in a loop; mutating during iteration raises `RuntimeError`.\n:::\n', 'enums.md': '+++\nslug = "enums"\ntitle = "Enums"\nsection = "Types"\nsummary = "Enum defines symbolic names for a fixed set of values."\ndoc_path = "/library/enum.html"\n+++\n\n`Enum` defines a fixed set of named values. This makes states and modes easier to read than raw strings scattered through a program.\n\nEach enum member has a name and a value. Comparing enum members is explicit and helps avoid typos that plain strings would allow.\n\nUse enums when a value must be one of a small known set: statuses, modes, directions, roles, and similar choices.\n\n:::program\n```python\nfrom enum import Enum\n\nclass Status(Enum):\n PENDING = "pending"\n DONE = "done"\n\ncurrent = Status.PENDING\nprint(current.name)\nprint(current.value)\nprint(current is Status.PENDING)\nprint(current == "pending")\n```\n:::\n\n:::cell\nAn enum member has a symbolic name and an underlying value. The symbolic name is what readers usually care about in code.\n\n```python\nfrom enum import Enum\n\nclass Status(Enum):\n PENDING = "pending"\n DONE = "done"\n\ncurrent = Status.PENDING\nprint(current.name)\nprint(current.value)\n```\n\n```output\nPENDING\npending\n```\n:::\n\n:::cell\nCompare enum members with enum members, not with raw strings. This keeps the set of valid states explicit.\n\n```python\nprint(current is Status.PENDING)\nprint(current == "pending")\n```\n\n```output\nTrue\nFalse\n```\n:::\n\n:::note\n- Enums make states and choices explicit.\n- Members have names and values.\n- Comparing enum members avoids string typo bugs.\n- Prefer raw strings for open-ended text; prefer enums for a closed set of named choices.\n:::\n', 'equality-and-identity.md': '+++\nslug = "equality-and-identity"\ntitle = "Equality and Identity"\nsection = "Data Model"\nsummary = "== compares values, while is compares object identity."\ndoc_path = "/reference/expressions.html#is-not"\n+++\n\nPython separates equality from identity. Equality asks whether two objects should be considered the same value, while identity asks whether two names point to the same object.\n\nThis distinction matters for mutable containers because two equal lists can still be independent objects. Mutating one should not imply mutating the other unless they share identity.\n\nThe `is` operator is best reserved for identity checks against singletons such as `None`. For ordinary values, `==` is the comparison readers expect.\n\n:::program\n```python\nleft = [1, 2, 3]\nright = [1, 2, 3]\nprint(left == right)\nprint(left is right)\n\nsame = left\nsame.append(4)\nprint(left)\nprint(same is left)\n\nvalue = None\nprint(value is None)\n\nsmall_a = 100\nsmall_b = 100\nprint(small_a is small_b)\n\nbig_a = int("1000")\nbig_b = int("1000")\nprint(big_a is big_b)\nprint(big_a == big_b)\n```\n:::\n\n:::cell\nEqual containers can be different objects. `==` compares list contents, while `is` checks whether both names refer to the same list object.\n\n```python\nleft = [1, 2, 3]\nright = [1, 2, 3]\nprint(left == right)\nprint(left is right)\n```\n\n```output\nTrue\nFalse\n```\n:::\n\n:::cell\nIdentity matters when objects are mutable. `same` is another name for `left`, so mutating through one name changes the object seen through the other.\n\n```python\nsame = left\nsame.append(4)\nprint(left)\nprint(same is left)\n```\n\n```output\n[1, 2, 3, 4]\nTrue\n```\n:::\n\n:::cell\nUse `is` for singleton identity checks such as `None`. This asks whether the value is the one special `None` object.\n\n```python\nvalue = None\nprint(value is None)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\n`is` for integers is unreliable because CPython caches small integers (roughly `-5` to `256`) but not larger ones. Two equal large integers can be different objects. Use `==` for value comparisons; reserve `is` for singletons.\n\n```python\nsmall_a = 100\nsmall_b = 100\nprint(small_a is small_b)\n\nbig_a = int("1000")\nbig_b = int("1000")\nprint(big_a is big_b)\nprint(big_a == big_b)\n```\n\n```output\nTrue\nFalse\nTrue\n```\n:::\n\n:::note\n- Use `==` for ordinary value comparisons.\n- Use `is` primarily for identity checks against singletons such as `None`.\n- Equal mutable containers can still be independent objects.\n- Never use `is` to compare numbers; CPython\'s small-integer cache makes the result an implementation detail.\n:::\n', 'exception-chaining.md': '+++\nslug = "exception-chaining"\ntitle = "Exception Chaining"\nsection = "Errors"\nsummary = "raise from preserves the original cause when translating exceptions."\ndoc_path = "/tutorial/errors.html#exception-chaining"\nsee_also = [\n "exceptions",\n "custom-exceptions",\n "assertions",\n]\n+++\n\nException chaining connects a higher-level error to the lower-level exception that caused it. The syntax is `raise NewError(...) from error`.\n\nUse chaining when translating implementation details into a domain-specific error while preserving the original cause for debugging.\n\nThis is different from hiding the original exception. The caller can catch the domain error, and tooling can still inspect `__cause__`.\n\n:::program\n```python\nclass ConfigError(Exception):\n pass\n\n\ndef read_port(text):\n try:\n return int(text)\n except ValueError as error:\n raise ConfigError("port must be a number") from error\n\nprint(ConfigError.__name__)\n\ntry:\n read_port("abc")\nexcept ConfigError as error:\n print(error)\n print(type(error.__cause__).__name__)\n```\n:::\n\n:::cell\nCatch the low-level exception where it happens, then raise a domain-specific exception from it.\n\n```python\nclass ConfigError(Exception):\n pass\n\n\ndef read_port(text):\n try:\n return int(text)\n except ValueError as error:\n raise ConfigError("port must be a number") from error\n\nprint(ConfigError.__name__)\n```\n\n```output\nConfigError\n```\n:::\n\n:::cell\nThe caller handles the domain error. The original `ValueError` remains available as `__cause__`.\n\n```python\ntry:\n read_port("abc")\nexcept ConfigError as error:\n print(error)\n print(type(error.__cause__).__name__)\n```\n\n```output\nport must be a number\nValueError\n```\n:::\n\n:::note\n- Use `raise ... from error` when translating exceptions across a boundary.\n- The new exception\'s `__cause__` points to the original exception.\n- Chaining keeps user-facing errors clear without losing debugging context.\n:::\n', 'exception-groups.md': '+++\nslug = "exception-groups"\ntitle = "Exception Groups"\nsection = "Errors"\nsummary = "except* handles matching exceptions inside an ExceptionGroup."\ndoc_path = "/tutorial/errors.html#raising-and-handling-multiple-unrelated-exceptions"\nsee_also = [\n "exceptions",\n "exception-chaining",\n "async-await",\n]\n+++\n\n`ExceptionGroup` represents several unrelated exceptions raised together. `except*` exists for code that may receive multiple failures at once, especially concurrent work.\n\nUse ordinary `except` for one exception. Use `except*` only when the value being handled is an exception group and each matching subgroup needs its own handling.\n\nEach `except*` clause receives a smaller exception group containing the matching exceptions.\n\n:::program\n```python\nerrors = ExceptionGroup(\n "batch failed",\n [ValueError("bad port"), TypeError("bad mode")],\n)\nprint(len(errors.exceptions))\n\ntry:\n raise errors\nexcept* ValueError as group:\n print(type(group).__name__)\n print(group.exceptions[0])\nexcept* TypeError as group:\n print(group.exceptions[0])\n```\n:::\n\n:::cell\nAn exception group bundles several exception objects. This is different from an ordinary exception because more than one failure is present.\n\n```python\nerrors = ExceptionGroup(\n "batch failed",\n [ValueError("bad port"), TypeError("bad mode")],\n)\nprint(len(errors.exceptions))\n```\n\n```output\n2\n```\n:::\n\n:::cell\n`except*` handles matching members of the group. The `ValueError` handler sees the value error, and the `TypeError` handler sees the type error.\n\n```python\ntry:\n raise errors\nexcept* ValueError as group:\n print(type(group).__name__)\n print(group.exceptions[0])\nexcept* TypeError as group:\n print(group.exceptions[0])\n```\n\n```output\nExceptionGroup\nbad port\nbad mode\n```\n:::\n\n:::note\n- `except*` is for `ExceptionGroup`, not ordinary single exceptions.\n- Each `except*` clause handles matching members of the group.\n- Exception groups often appear around concurrent work.\n:::\n', 'exceptions.md': '+++\nslug = "exceptions"\ntitle = "Exceptions"\nsection = "Errors"\nsummary = "Use try, except, else, and finally to separate success, recovery, and cleanup."\ndoc_path = "/tutorial/errors.html"\n+++\n\nExceptions represent errors or unusual conditions that interrupt normal control flow. `try` marks the operation that may fail, and `except` handles a specific failure where recovery makes sense.\n\nKeep the successful path separate from the recovery path. `else` runs only when no exception was raised, while `finally` runs either way for cleanup or bookkeeping.\n\nUse exceptions when an operation cannot produce a valid result. Prefer ordinary conditionals for expected branches that are not errors.\n\nCatch specific exceptions whenever possible. A broad catch can hide programming mistakes, while a targeted `ValueError` handler documents exactly what failure is expected.\n\n:::program\n```python\ndef parse_int(text):\n return int(text)\n\nfor text in ["42", "python"]:\n try:\n number = parse_int(text)\n except ValueError:\n print(f"{text}: invalid")\n else:\n print(f"{text}: {number}")\n finally:\n print(f"checked {text}")\n\n\ndef safe_parse_broken(text):\n try:\n return int(text)\n except Exception:\n return None\n\ndef safe_parse_fixed(text):\n try:\n return int(text)\n except ValueError:\n return None\n\nprint(safe_parse_broken("42"))\nprint(safe_parse_fixed("42"))\n```\n:::\n\n:::cell\nWhen no exception is raised, the `else` block runs. Keeping success in `else` makes the `try` block contain only the operation that might fail.\n\n```python\ndef parse_int(text):\n return int(text)\n\ntext = "42"\ntry:\n number = parse_int(text)\nexcept ValueError:\n print(f"{text}: invalid")\nelse:\n print(f"{text}: {number}")\nfinally:\n print(f"checked {text}")\n```\n\n```output\n42: 42\nchecked 42\n```\n:::\n\n:::cell\nWhen parsing fails, `int()` raises `ValueError`. Catching that specific exception makes the expected recovery path explicit.\n\n```python\ntext = "python"\ntry:\n number = parse_int(text)\nexcept ValueError:\n print(f"{text}: invalid")\nelse:\n print(f"{text}: {number}")\nfinally:\n print(f"checked {text}")\n```\n\n```output\npython: invalid\nchecked python\n```\n:::\n\n:::cell\nBare `except:` and broad `except Exception:` swallow far more than the failure you meant to handle, including `KeyboardInterrupt` (bare) and most programming bugs (broad). Catch the specific class — `ValueError` here — so unexpected failures still surface.\n\n```python\ndef safe_parse_broken(text):\n try:\n return int(text)\n except Exception:\n return None\n\ndef safe_parse_fixed(text):\n try:\n return int(text)\n except ValueError:\n return None\n\nprint(safe_parse_broken("42"))\nprint(safe_parse_fixed("42"))\n```\n\n```output\n42\n42\n```\n:::\n\n:::note\n- Catch the most specific exception you can.\n- `else` is for success code that should run only if the `try` block did not fail.\n- `finally` runs whether the operation succeeded or failed.\n- Avoid bare `except:` and broad `except Exception:` — they hide bugs and absorb signals like `KeyboardInterrupt`.\n:::\n', 'for-loops.md': '+++\nslug = "for-loops"\ntitle = "For Loops"\nsection = "Control Flow"\nsummary = "for iterates over any iterable object."\ndoc_path = "/tutorial/controlflow.html#for-statements"\n+++\n\nPython for loops iterate over values from an iterable. This is different from languages where for primarily means incrementing a numeric counter.\n\nrange() is itself an iterable that produces numbers lazily. Use it when you need a sequence of integers, but prefer direct iteration when you already have a collection.\n\nBlocks are defined by indentation. range(3) yields 0, 1, and 2.\n\n:::program\n```python\nfor name in ["Ada", "Grace", "Guido"]:\n print(name)\n\nfor number in range(3):\n print(number)\n```\n:::\n\n:::cell\nPython for loops iterate over values from an iterable. This is different from languages where for primarily means incrementing a numeric counter.\n\n```python\nfor name in ["Ada", "Grace", "Guido"]:\n print(name)\n```\n\n```output\nAda\nGrace\nGuido\n```\n:::\n\n:::cell\nrange() is itself an iterable that produces numbers lazily. Use it when you need a sequence of integers, but prefer direct iteration when you already have a collection.\n\n```python\nfor number in range(3):\n print(number)\n```\n\n```output\n0\n1\n2\n```\n:::\n\n:::note\n- Blocks are defined by indentation.\n- range(3) yields 0, 1, and 2.\n:::\n', 'functions.md': '+++\nslug = "functions"\ntitle = "Functions"\nsection = "Functions"\nsummary = "Use def to name reusable behavior and return results."\ndoc_path = "/tutorial/controlflow.html#defining-functions"\n+++\n\nFunctions package behavior behind a name. `def` creates a function object that can accept arguments, compute values, and return a result.\n\nDefault arguments make common calls short, and keyword arguments make call sites easier to read. A function that reaches the end without `return` produces `None`.\n\nUse functions when a calculation has a useful name, when code repeats, or when a piece of behavior should be tested independently.\n\n:::program\n```python\ndef greet(name):\n return f"Hello, {name}."\n\nprint(greet("Python"))\n\n\ndef format_total(amount, currency="USD"):\n return f"{amount} {currency}"\n\nprint(format_total(10))\nprint(format_total(10, currency="EUR"))\n\n\ndef log(message):\n print(f"log: {message}")\n\nresult = log("saved")\nprint(result)\n\n\ndef append_broken(item, items=[]):\n items.append(item)\n return items\n\nprint(append_broken("a"))\nprint(append_broken("b"))\n\n\ndef append_fixed(item, items=None):\n if items is None:\n items = []\n items.append(item)\n return items\n\nprint(append_fixed("a"))\nprint(append_fixed("b"))\n```\n:::\n\n:::cell\n`return` sends a value back to the caller. The caller can print it, store it, or pass it to another function.\n\n```python\ndef greet(name):\n return f"Hello, {name}."\n\nprint(greet("Python"))\n```\n\n```output\nHello, Python.\n```\n:::\n\n:::cell\nDefault arguments provide common values. Keyword arguments make it clear which option is being overridden.\n\n```python\ndef format_total(amount, currency="USD"):\n return f"{amount} {currency}"\n\nprint(format_total(10))\nprint(format_total(10, currency="EUR"))\n```\n\n```output\n10 USD\n10 EUR\n```\n:::\n\n:::cell\nA function without an explicit `return` returns `None`. That makes side-effect-only functions easy to distinguish from value-producing ones.\n\n```python\ndef log(message):\n print(f"log: {message}")\n\nresult = log("saved")\nprint(result)\n```\n\n```output\nlog: saved\nNone\n```\n:::\n\n:::cell\nMutable default arguments are evaluated once when the function is defined, not on each call. The same list is shared across calls, so successive calls see each other\'s mutations. Use `None` as the sentinel and create a fresh container inside the body.\n\n```python\ndef append_broken(item, items=[]):\n items.append(item)\n return items\n\nprint(append_broken("a"))\nprint(append_broken("b"))\n\n\ndef append_fixed(item, items=None):\n if items is None:\n items = []\n items.append(item)\n return items\n\nprint(append_fixed("a"))\nprint(append_fixed("b"))\n```\n\n```output\n[\'a\']\n[\'a\', \'b\']\n[\'a\']\n[\'b\']\n```\n:::\n\n:::note\n- Use `return` for values the caller should receive.\n- Defaults keep common calls concise.\n- Keyword arguments make options readable at the call site.\n- Never use a mutable value as a default argument; use `None` and build the container inside the function body.\n:::\n', 'generator-expressions.md': '+++\nslug = "generator-expressions"\ntitle = "Generator Expressions"\nsection = "Iteration"\nsummary = "Generator expressions use comprehension-like syntax to stream values lazily."\ndoc_path = "/tutorial/classes.html#generator-expressions"\n+++\n\nGenerator expressions look like list comprehensions with parentheses, but they produce an iterator instead of building a concrete collection immediately.\n\nUse them when a consumer such as `sum()`, `any()`, or a `for` loop can use values one at a time. This keeps the transformation close to the consumer and avoids storing intermediate lists.\n\nLike other iterators, a generator expression is consumed as values are requested. Create a new generator expression when you need another pass.\n\n:::program\n```python\nnumbers = [1, 2, 3, 4]\nlist_squares = [number * number for number in numbers]\nprint(list_squares)\n\nstream_squares = (number * number for number in numbers)\nprint(next(stream_squares))\nprint(next(stream_squares))\nprint(list(stream_squares))\n\nprint(sum(number * number for number in numbers))\n```\n:::\n\n:::cell\nA list comprehension is eager: it builds a list immediately. That is useful when you need to store or reuse the results.\n\n```python\nnumbers = [1, 2, 3, 4]\nlist_squares = [number * number for number in numbers]\nprint(list_squares)\n```\n\n```output\n[1, 4, 9, 16]\n```\n:::\n\n:::cell\nA generator expression is lazy: it creates an iterator that produces values as they are consumed. After two `next()` calls, only the remaining squares are left.\n\n```python\nstream_squares = (number * number for number in numbers)\nprint(next(stream_squares))\nprint(next(stream_squares))\nprint(list(stream_squares))\n```\n\n```output\n1\n4\n[9, 16]\n```\n:::\n\n:::cell\nGenerator expressions are common inside reducing functions. When a generator expression is the only argument, the extra parentheses can be omitted.\n\n```python\nprint(sum(number * number for number in numbers))\n```\n\n```output\n30\n```\n:::\n\n:::note\n- List, dict, and set comprehensions build concrete collections.\n- Generator expressions produce one-pass iterators.\n- Use generator expressions when the consumer can process values one at a time.\n:::\n', 'generators.md': '+++\nslug = "generators"\ntitle = "Generators"\nsection = "Iteration"\nsummary = "yield creates an iterator that produces values on demand."\ndoc_path = "/tutorial/classes.html#generators"\nsee_also = [\n "iterators",\n "iterator-vs-iterable",\n "generator-expressions",\n]\n+++\n\nA generator function is a convenient way to write your own iterator. `yield` produces one value, pauses the function, and resumes when the next value is requested.\n\nGenerators are useful for pipelines, large inputs, and infinite sequences because they avoid building an entire collection in memory.\n\nUse `next()` to request one value manually, or loop over the generator to consume values until it is exhausted.\n\n:::program\n```python\ndef countdown(n):\n while n > 0:\n yield n\n n -= 1\n\nnumbers = countdown(3)\nprint(next(numbers))\nprint(next(numbers))\n\nfor value in countdown(3):\n print(value)\n\ndef countdown_eager(n):\n result = []\n while n > 0:\n result.append(n)\n n -= 1\n return result\n\nvalues = countdown_eager(3)\nprint(values)\nprint(values)\n\nstream = countdown(3)\nprint(list(stream))\nprint(list(stream))\n\nclass Countdown:\n def __init__(self, n):\n self.n = n\n\n def __iter__(self):\n return self\n\n def __next__(self):\n if self.n <= 0:\n raise StopIteration\n value = self.n\n self.n -= 1\n return value\n\nprint(list(Countdown(3)))\n```\n:::\n\n:::cell\nCalling a generator function returns an iterator. `next()` asks for one value and resumes the function until the next `yield`.\n\n```python\ndef countdown(n):\n while n > 0:\n yield n\n n -= 1\n\nnumbers = countdown(3)\nprint(next(numbers))\nprint(next(numbers))\n```\n\n```output\n3\n2\n```\n:::\n\n:::cell\nA `for` loop repeatedly calls `next()` for you. The loop stops when the generator is exhausted.\n\n```python\nfor value in countdown(3):\n print(value)\n```\n\n```output\n3\n2\n1\n```\n:::\n\n:::cell\n`return` builds the entire result before handing it back; `yield` produces values on demand. The list keeps its values for repeated use, while the generator is exhausted after one pass.\n\n```python\ndef countdown_eager(n):\n result = []\n while n > 0:\n result.append(n)\n n -= 1\n return result\n\nvalues = countdown_eager(3)\nprint(values)\nprint(values)\n\nstream = countdown(3)\nprint(list(stream))\nprint(list(stream))\n```\n\n```output\n[3, 2, 1]\n[3, 2, 1]\n[3, 2, 1]\n[]\n```\n:::\n\n:::cell\nEvery generator is an iterator. The same countdown written by hand needs `__iter__` and `__next__` and an explicit `StopIteration`. The generator function expresses the same protocol with one `yield`.\n\n```python\nclass Countdown:\n def __init__(self, n):\n self.n = n\n\n def __iter__(self):\n return self\n\n def __next__(self):\n if self.n <= 0:\n raise StopIteration\n value = self.n\n self.n -= 1\n return value\n\nprint(list(Countdown(3)))\n```\n\n```output\n[3, 2, 1]\n```\n:::\n\n:::note\n- Generator functions are a concise way to create custom iterators; every generator is an iterator.\n- `yield` defers work and streams values; `return` produces the whole result up front.\n- A generator is consumed as you iterate over it.\n- Prefer a list when you need to reuse stored results; prefer a generator when values can be streamed once.\n:::\n', 'generics-and-typevar.md': '+++\nslug = "generics-and-typevar"\ntitle = "Generics and TypeVar"\nsection = "Types"\nsummary = "Generics preserve type information across reusable functions and classes."\ndoc_path = "/library/typing.html#generics"\nsee_also = [\n "type-hints",\n "collections-module",\n "casts-and-any",\n]\n+++\n\nGenerics connect types across an API. A plain function that returns `object` loses information; a generic function can say that the returned value has the same type as the input element.\n\nA `TypeVar` stands for a type chosen by the caller. In `list[T] -> T`, the same `T` says that a list of strings produces a string and a list of integers produces an integer.\n\nUse generics when a function or class is reusable but still preserves a relationship between input and output types.\n\n:::program\n```python\nfrom typing import TypeVar\n\nT = TypeVar("T")\n\n\ndef first(items: list[T]) -> T:\n return items[0]\n\n\ndef pair(left: T, right: T) -> tuple[T, T]:\n return (left, right)\n\nprint(first([1, 2, 3]))\nprint(first(["Ada", "Grace"]))\nprint(pair("x", "y"))\nprint(T.__name__)\n```\n:::\n\n:::cell\nA `TypeVar` stands for a type chosen by the caller. The return type follows the list element type.\n\n```python\nfrom typing import TypeVar\n\nT = TypeVar("T")\n\n\ndef first(items: list[T]) -> T:\n return items[0]\n\nprint(first([1, 2, 3]))\nprint(first(["Ada", "Grace"]))\n```\n\n```output\n1\nAda\n```\n:::\n\n:::cell\nReusing the same `TypeVar` expresses a relationship between parameters and results.\n\n```python\ndef pair(left: T, right: T) -> tuple[T, T]:\n return (left, right)\n\nprint(pair("x", "y"))\n```\n\n```output\n(\'x\', \'y\')\n```\n:::\n\n:::cell\n`TypeVar` is visible at runtime, but the relationship is mainly for type checkers.\n\n```python\nprint(T.__name__)\nprint(first.__annotations__)\n```\n\n```output\nT\n{\'items\': list[~T], \'return\': ~T}\n```\n:::\n\n:::note\n- A `TypeVar` stands for a type chosen by the caller.\n- Generic functions avoid losing information to `object` or `Any`.\n- Use generics when input and output types are connected.\n:::\n', 'guard-clauses.md': '+++\nslug = "guard-clauses"\ntitle = "Guard Clauses"\nsection = "Control Flow"\nsummary = "Guard clauses handle exceptional cases early so the main path stays flat."\ndoc_path = "/tutorial/controlflow.html#if-statements"\n+++\n\nGuard clauses handle exceptional cases early so the main path stays flat. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\ndef price_after_discount(price, percent):\n if price < 0:\n return "invalid price"\n if not 0 <= percent <= 100:\n return "invalid discount"\n\n discount = price * percent / 100\n return round(price - discount, 2)\n\nprint(price_after_discount(100, 15))\nprint(price_after_discount(-5, 10))\nprint(price_after_discount(100, 120))\n```\n:::\n\n:::cell\nReturn early when inputs cannot be handled.\n\n```python\ndef price_after_discount(price, percent):\n if price < 0:\n return "invalid price"\n if not 0 <= percent <= 100:\n return "invalid discount"\n\n discount = price * percent / 100\n return round(price - discount, 2)\n\nprint(price_after_discount(100, 15))\nprint(price_after_discount(-5, 10))\nprint(price_after_discount(100, 120))\n```\n\n```output\n85.0\ninvalid price\ninvalid discount\n```\n:::\n\n:::note\n- Return early when inputs cannot be handled.\n- After the guards, the remaining code can read as the normal path.\n- Guard clauses are a style choice, not new syntax.\n:::\n', 'hello-world.md': '+++\nslug = "hello-world"\ntitle = "Hello World"\nsection = "Basics"\nsummary = "The first Python program prints a line of text."\ndoc_path = "/tutorial/introduction.html"\n+++\n\nEvery Python program starts by executing statements from top to bottom. Calling `print()` is the smallest useful program because it shows how Python evaluates an expression and sends text to standard output.\n\nStrings are ordinary values, so the message passed to `print()` can be changed, stored in a variable, or produced by a function. This example keeps the first program intentionally small.\n\n`print()` writes text followed by a newline. Strings can be delimited with single or double quotes.\n\n:::program\n```python\nprint("hello world")\n```\n:::\n\n:::cell\nEvery Python program starts by executing statements from top to bottom. Calling `print()` is the smallest useful program because it shows how Python evaluates an expression and sends text to standard output.\n\n```python\nprint("hello world")\n```\n\n```output\nhello world\n```\n:::\n\n:::note\n- `print()` writes text followed by a newline.\n- Strings can be delimited with single or double quotes.\n:::\n', 'import-aliases.md': '+++\nslug = "import-aliases"\ntitle = "Import Aliases"\nsection = "Modules"\nsummary = "as gives imported modules or names a local alias."\ndoc_path = "/reference/simple_stmts.html#the-import-statement"\nsee_also = [\n "modules",\n "functions",\n]\n+++\n\n`as` gives an imported module or imported name a local alias. Use it when a conventional short name improves readability or when two imports would otherwise collide.\n\nThe alternative is a plain import, which is usually better when the module name is already clear. Avoid aliases that make readers guess where a name came from.\n\nAvoid star imports in examples and production modules because they hide dependencies and blur the boundary between modules.\n\n:::program\n```python\nimport statistics as stats\nfrom math import sqrt as square_root\n\nscores = [8, 10, 9]\nprint(stats.mean(scores))\nprint(stats.__name__)\n\nprint(square_root(81))\nprint(square_root.__name__)\n```\n:::\n\n:::cell\nA module alias keeps the namespace but changes the local name. Here `stats` is shorter, but readers can still see that `mean` belongs to the statistics module.\n\n```python\nimport statistics as stats\n\nscores = [8, 10, 9]\nprint(stats.mean(scores))\nprint(stats.__name__)\n```\n\n```output\n9\nstatistics\n```\n:::\n\n:::cell\nA name imported with `from` can also be aliased. Use this when the local name explains the role better than the original name.\n\n```python\nfrom math import sqrt as square_root\n\nprint(square_root(81))\nprint(square_root.__name__)\n```\n\n```output\n9.0\nsqrt\n```\n:::\n\n:::note\n- `import module as alias` keeps module-style access under a shorter or clearer name.\n- `from module import name as alias` imports one name under a local alias.\n- Prefer plain imports unless an alias improves clarity or follows a strong convention.\n- Avoid `from module import *` because it makes dependencies harder to see.\n:::\n', 'inheritance-and-super.md': '+++\nslug = "inheritance-and-super"\ntitle = "Inheritance and Super"\nsection = "Classes"\nsummary = "Inheritance reuses behavior, and super delegates to a parent implementation."\ndoc_path = "/tutorial/classes.html#inheritance"\nsee_also = [\n "classes",\n "abstract-base-classes",\n "classmethods-and-staticmethods",\n "special-methods",\n]\n+++\n\nInheritance lets one class specialize another class. The child class gets parent behavior and can add or override methods.\n\nUse `super()` when the child method should extend the parent implementation instead of replacing it entirely.\n\nPrefer composition when objects merely collaborate. Inheritance is best when the child really is a specialized version of the parent.\n\n:::program\n```python\nclass Animal:\n def __init__(self, name):\n self.name = name\n\n def speak(self):\n return f"{self.name} makes a sound"\n\nclass Dog(Animal):\n def speak(self):\n base = super().speak()\n return f"{base}; {self.name} barks"\n\npet = Dog("Nina")\nprint(pet.name)\nprint(pet.speak())\nprint(isinstance(pet, Animal))\n```\n:::\n\n:::cell\nA child class names its parent in parentheses. `Dog` instances get the `Animal.__init__` method because `Dog` does not define its own initializer.\n\n```python\nclass Animal:\n def __init__(self, name):\n self.name = name\n\n def speak(self):\n return f"{self.name} makes a sound"\n\nclass Dog(Animal):\n def speak(self):\n base = super().speak()\n return f"{base}; {self.name} barks"\n\npet = Dog("Nina")\nprint(pet.name)\n```\n\n```output\nNina\n```\n:::\n\n:::cell\n`super()` delegates to the parent implementation. The child method can reuse the parent result and then add specialized behavior.\n\n```python\nprint(pet.speak())\nprint(isinstance(pet, Animal))\n```\n\n```output\nNina makes a sound; Nina barks\nTrue\n```\n:::\n\n:::note\n- Inheritance models an “is a specialized kind of” relationship.\n- `super()` calls the next implementation in the method resolution order.\n- Prefer composition when an object only needs to use another object.\n:::\n', 'iterating-over-iterables.md': '+++\nslug = "iterating-over-iterables"\ntitle = "Iterating over Iterables"\nsection = "Iteration"\nsummary = "for loops consume values from any iterable object."\ndoc_path = "/tutorial/controlflow.html#for-statements"\nsee_also = [\n "iterators",\n "iterator-vs-iterable",\n "for-loops",\n]\n+++\n\nPython\'s `for` statement consumes values from any iterable object: lists, strings, dictionaries, ranges, generators, files, and many standard-library helpers.\n\nThis makes iteration a value-stream protocol rather than a special case for arrays. The producer decides how values are made, and the loop consumes them one at a time.\n\nUse `enumerate()` when you need positions and values together, and `dict.items()` when you need keys and values. These helpers express intent better than manual indexing.\n\n:::program\n```python\nnames = ["Ada", "Grace", "Guido"]\n\nfor name in names:\n print(name)\n\nfor index, name in enumerate(names):\n print(index, name)\n\nscores = {"Ada": 10, "Grace": 9}\nfor name, score in scores.items():\n print(name, score)\n```\n:::\n\n:::cell\nStart with an ordinary list. A list stores values, and a `for` loop asks it for one value at a time.\n\nWhen you only need the values, iterate over the collection directly. There is no index variable because the loop body does not need one.\n\n```python\nnames = ["Ada", "Grace", "Guido"]\n\nfor name in names:\n print(name)\n```\n\n```output\nAda\nGrace\nGuido\n```\n:::\n\n:::cell\nWhen you need both a position and a value, use `enumerate()`. It produces index/value pairs without manual indexing.\n\n```python\nfor index, name in enumerate(names):\n print(index, name)\n```\n\n```output\n0 Ada\n1 Grace\n2 Guido\n```\n:::\n\n:::cell\nDictionaries are iterable too, but `dict.items()` is the clearest way to say that the loop needs keys and values together.\n\n```python\nscores = {"Ada": 10, "Grace": 9}\nfor name, score in scores.items():\n print(name, score)\n```\n\n```output\nAda 10\nGrace 9\n```\n:::\n\n:::note\n- A `for` loop consumes values from an iterable.\n- Different producers can feed the same loop protocol.\n- Prefer `enumerate()` over `range(len(...))` when you need an index.\n:::\n', 'iterator-vs-iterable.md': '+++\nslug = "iterator-vs-iterable"\ntitle = "Iterator vs Iterable"\nsection = "Iteration"\nsummary = "Iterables produce fresh iterators; iterators are one-pass."\ndoc_path = "/glossary.html#term-iterable"\nsee_also = [\n "iterators",\n "iterating-over-iterables",\n "generators",\n]\n+++\n\nAn iterable can produce values when asked. An iterator is the object that remembers where the production currently is. The distinction matters because iterables can be traversed many times, while many iterators can be traversed only once.\n\n`iter(iterable)` returns a fresh iterator each call. `iter(iterator)` returns the iterator itself. That self-iteration property is how `for` loops can accept either kind, and it is also why a function that loops over its argument twice silently breaks when called with a generator instead of a list.\n\nThe takeaway for API design: receive iterables when the caller may want a second pass, and materialize once at the boundary if you must.\n\n:::program\n```python\nnames = ["Ada", "Grace"]\n\nprint(list(names))\nprint(list(names))\n\nstream = iter(names)\nprint(list(stream))\nprint(list(stream))\n\nfirst = iter(names)\nsecond = iter(names)\nprint(first is second)\nprint(iter(first) is first)\n\ndef total_and_count(numbers):\n total = sum(numbers)\n count = sum(1 for _ in numbers)\n return total, count\n\ndef values():\n yield from [10, 9, 8]\n\nprint(total_and_count([10, 9, 8]))\nprint(total_and_count(values()))\n\ndef total_and_count_safe(numbers):\n items = list(numbers)\n return sum(items), len(items)\n\nprint(total_and_count_safe(values()))\n```\n:::\n\n:::cell\nA list is iterable. Each `for` loop or `list()` call asks the list for a fresh iterator under the hood, so the same data can be traversed many times.\n\n```python\nnames = ["Ada", "Grace"]\nprint(list(names))\nprint(list(names))\n```\n\n```output\n[\'Ada\', \'Grace\']\n[\'Ada\', \'Grace\']\n```\n:::\n\n:::cell\nAn iterator is one-pass. Calling `iter()` returns a position-tracking object; once it has been exhausted, it stays exhausted.\n\n```python\nstream = iter(names)\nprint(list(stream))\nprint(list(stream))\n```\n\n```output\n[\'Ada\', \'Grace\']\n[]\n```\n:::\n\n:::cell\nCalling `iter()` on an iterable returns a brand-new iterator each time. Calling `iter()` on an iterator returns the same object — that is the rule that lets a `for` loop accept either kind.\n\n```python\nfirst = iter(names)\nsecond = iter(names)\nprint(first is second)\nprint(iter(first) is first)\n```\n\n```output\nFalse\nTrue\n```\n:::\n\n:::cell\nThe distinction shows up at API boundaries. A function that loops over its argument twice works for an iterable but silently produces wrong answers for an iterator, because the second pass finds the iterator already exhausted. Materialize once at the boundary when both passes matter.\n\n```python\ndef total_and_count(numbers):\n total = sum(numbers)\n count = sum(1 for _ in numbers)\n return total, count\n\ndef values():\n yield from [10, 9, 8]\n\nprint(total_and_count([10, 9, 8]))\nprint(total_and_count(values()))\n\ndef total_and_count_safe(numbers):\n items = list(numbers)\n return sum(items), len(items)\n\nprint(total_and_count_safe(values()))\n```\n\n```output\n(27, 3)\n(27, 0)\n(27, 3)\n```\n:::\n\n:::note\n- An iterable produces an iterator each time `iter()` is called on it; an iterator produces values until it is exhausted.\n- `iter(iterable)` returns a fresh iterator; `iter(iterator)` returns the same iterator.\n- Functions that traverse their input more than once must accept an iterable or materialize the input at the boundary.\n:::\n', 'iterators.md': '+++\nslug = "iterators"\ntitle = "Iterators"\nsection = "Iteration"\nsummary = "iter and next expose the protocol behind for loops."\ndoc_path = "/library/stdtypes.html#iterator-types"\nsee_also = [\n "iterating-over-iterables",\n "iterator-vs-iterable",\n "generators",\n]\n+++\n\nAn iterable is an object that can produce values for a loop. An iterator is the object that remembers where that production currently is.\n\n`iter()` asks an iterable for an iterator, and `next()` consumes one value from that iterator. A `for` loop performs those steps for you until the iterator is exhausted.\n\nThis is the core value-stream protocol in Python: one object produces values, another piece of code consumes them, and many streams are one-pass.\n\n:::program\n```python\nnames = ["Ada", "Grace", "Guido"]\niterator = iter(names)\nprint(next(iterator))\nprint(next(iterator))\n\nfor name in iterator:\n print(name)\n\nagain = iter(names)\nprint(next(again))\n```\n:::\n\n:::cell\n`iter()` asks an iterable for an iterator. `next()` consumes one value and advances the iterator\'s position.\n\n```python\nnames = ["Ada", "Grace", "Guido"]\niterator = iter(names)\nprint(next(iterator))\nprint(next(iterator))\n```\n\n```output\nAda\nGrace\n```\n:::\n\n:::cell\nA `for` loop consumes the same iterator protocol. Because two values were already consumed, the loop sees only the remaining value.\n\n```python\nfor name in iterator:\n print(name)\n```\n\n```output\nGuido\n```\n:::\n\n:::cell\nThe list itself is reusable. Asking it for a fresh iterator starts a new pass over the same stored values.\n\n```python\nagain = iter(names)\nprint(next(again))\n```\n\n```output\nAda\n```\n:::\n\n:::note\n- Iterables produce iterators; iterators produce values.\n- `next()` consumes one value from an iterator.\n- Many iterators are one-pass even when the original collection is reusable.\n:::\n', 'itertools.md': '+++\nslug = "itertools"\ntitle = "Itertools"\nsection = "Iteration"\nsummary = "itertools composes lazy iterator streams."\ndoc_path = "/library/itertools.html"\n+++\n\nThe `itertools` module contains tools for composing iterator streams: combining, slicing, grouping, and repeating values without changing the consumer protocol.\n\nMany `itertools` functions are lazy. They describe work to do later instead of building a list immediately, so helpers such as `islice()` are useful when taking a finite window.\n\nIterator pipelines let each step stay small: one object produces values, another transforms them, and a final consumer such as `list()` or a loop pulls values through the pipeline.\n\n:::program\n```python\nimport itertools\n\ncounter = itertools.count(10)\nprint(list(itertools.islice(counter, 3)))\n\npages = itertools.chain(["intro", "setup"], ["deploy"])\nprint(list(pages))\n\nscores = [7, 10, 8, 10]\nhigh_scores = itertools.compress(scores, [score >= 9 for score in scores])\nprint(list(high_scores))\n```\n:::\n\n:::cell\n`count()` can produce values forever, so `islice()` takes a finite window. Nothing is materialized until `list()` consumes the iterator.\n\n```python\nimport itertools\n\ncounter = itertools.count(10)\nprint(list(itertools.islice(counter, 3)))\n```\n\n```output\n[10, 11, 12]\n```\n:::\n\n:::cell\n`chain()` presents several iterables as one stream. This avoids building an intermediate list just to loop over combined inputs.\n\n```python\npages = itertools.chain(["intro", "setup"], ["deploy"])\nprint(list(pages))\n```\n\n```output\n[\'intro\', \'setup\', \'deploy\']\n```\n:::\n\n:::cell\nIterator helpers compose with ordinary Python expressions. `compress()` keeps items whose corresponding selector is true.\n\n```python\nscores = [7, 10, 8, 10]\nhigh_scores = itertools.compress(scores, [score >= 9 for score in scores])\nprint(list(high_scores))\n```\n\n```output\n[10, 10]\n```\n:::\n\n:::note\n- `itertools` composes producer and transformer streams.\n- Iterator pipelines avoid building intermediate lists.\n- Use `islice()` to take a finite piece from an infinite iterator.\n- Convert to a list only when you need concrete results.\n:::\n', 'json.md': '+++\nslug = "json"\ntitle = "JSON"\nsection = "Standard Library"\nsummary = "json encodes Python values as JSON text and decodes them back."\ndoc_path = "/library/json.html"\nsee_also = [\n "dicts",\n "typed-dicts",\n "strings",\n]\n+++\n\nThe `json` module converts between Python values and JSON text. Dictionaries, lists, strings, numbers, booleans, and `None` map naturally to JSON structures.\n\nUse `dumps()` when you need a string and `loads()` when you need Python objects back. Options such as `sort_keys=True` and `indent=2` control stable, readable output.\n\nJSON is a data format, not a way to preserve arbitrary Python objects. Encode simple data structures at service boundaries, and expect decode errors when the incoming text is not valid JSON.\n\n:::program\n```python\nimport json\n\npayload = {"language": "Python", "versions": [3, 13], "stable": True, "missing": None}\ntext = json.dumps(payload, sort_keys=True)\nprint(text)\n\npretty = json.dumps({"language": "Python", "stable": True}, indent=2, sort_keys=True)\nprint(pretty.splitlines()[0])\nprint(pretty.splitlines()[1])\n\ndecoded = json.loads(text)\nprint(decoded["language"])\nprint(decoded["missing"] is None)\n\ntry:\n json.loads("{bad json}")\nexcept json.JSONDecodeError as error:\n print(error.__class__.__name__)\n```\n:::\n\n:::cell\n`dumps()` encodes Python data as JSON text. `sort_keys=True` keeps dictionary keys in a stable order for reproducible output.\n\n```python\nimport json\n\npayload = {"language": "Python", "versions": [3, 13], "stable": True, "missing": None}\ntext = json.dumps(payload, sort_keys=True)\nprint(text)\n```\n\n```output\n{"language": "Python", "missing": null, "stable": true, "versions": [3, 13]}\n```\n:::\n\n:::cell\nFormatting options change the JSON text, not the Python value. `indent=2` is useful for human-readable output.\n\n```python\npretty = json.dumps({"language": "Python", "stable": True}, indent=2, sort_keys=True)\nprint(pretty.splitlines()[0])\nprint(pretty.splitlines()[1])\n```\n\n```output\n{\n "language": "Python",\n```\n:::\n\n:::cell\n`loads()` decodes JSON text back into Python values. JSON `null` becomes Python `None`.\n\n```python\ndecoded = json.loads(text)\nprint(decoded["language"])\nprint(decoded["missing"] is None)\n```\n\n```output\nPython\nTrue\n```\n:::\n\n:::cell\nInvalid JSON raises `JSONDecodeError`, so input boundaries should handle decode failures explicitly.\n\n```python\ntry:\n json.loads("{bad json}")\nexcept json.JSONDecodeError as error:\n print(error.__class__.__name__)\n```\n\n```output\nJSONDecodeError\n```\n:::\n\n:::note\n- `dumps()` returns a string; `loads()` accepts a string.\n- JSON `true`, `false`, and `null` become Python `True`, `False`, and `None`.\n- Use `sort_keys=True` when stable text output matters.\n- JSON only represents data shapes, not arbitrary Python objects or behavior.\n:::\n', 'keyword-only-arguments.md': '+++\nslug = "keyword-only-arguments"\ntitle = "Keyword-only Arguments"\nsection = "Functions"\nsummary = "Use * to require selected function arguments to be named."\ndoc_path = "/tutorial/controlflow.html#special-parameters"\n+++\n\nA bare `*` in a function signature marks the following parameters as keyword-only. Callers must name those arguments explicitly.\n\nKeyword-only arguments are useful for options such as timeouts, flags, and modes where positional calls would be ambiguous or easy to misread.\n\nThey let the required data stay positional while optional controls remain self-documenting at the call site.\n\n:::program\n```python\ndef connect(host, *, timeout=5, secure=True):\n scheme = "https" if secure else "http"\n print(f"{scheme}://{host} timeout={timeout}")\n\nconnect("example.com")\nconnect("example.com", timeout=10)\nconnect("localhost", secure=False)\n```\n:::\n\n:::cell\nParameters after `*` must be named. The default options still apply when the caller omits them.\n\n```python\ndef connect(host, *, timeout=5, secure=True):\n scheme = "https" if secure else "http"\n print(f"{scheme}://{host} timeout={timeout}")\n\nconnect("example.com")\n```\n\n```output\nhttps://example.com timeout=5\n```\n:::\n\n:::cell\nNaming the option makes the call site explicit. A reader does not have to remember which positional slot controls the timeout.\n\n```python\nconnect("example.com", timeout=10)\n```\n\n```output\nhttps://example.com timeout=10\n```\n:::\n\n:::cell\nFlags are especially good keyword-only arguments because a bare positional `False` is hard to interpret.\n\n```python\nconnect("localhost", secure=False)\n```\n\n```output\nhttp://localhost timeout=5\n```\n:::\n\n:::note\n- Put `*` before options that callers should name.\n- Keyword-only flags avoid mysterious positional `True` and `False` arguments.\n- Defaults work normally for keyword-only parameters.\n:::\n', 'lambdas.md': '+++\nslug = "lambdas"\ntitle = "Lambdas"\nsection = "Functions"\nsummary = "lambda creates small anonymous function expressions."\ndoc_path = "/tutorial/controlflow.html#lambda-expressions"\n+++\n\n`lambda` creates a small anonymous function expression. It is most useful when Python asks for a function and the behavior is short enough to read inline.\n\nA lambda can only contain one expression. Use `def` when the behavior deserves a name, needs statements, or would be easier to test separately.\n\nLambdas often appear as key functions, callbacks, and tiny adapters. Keep them simple enough that the call site remains clearer than a named helper.\n\n:::program\n```python\nadd_tax = lambda price: round(price * 1.08, 2)\nprint(add_tax(10))\n\nitems = [("notebook", 5), ("pen", 2), ("bag", 20)]\nby_price = sorted(items, key=lambda item: item[1])\nprint(by_price)\n\ndef price(item):\n return item[1]\n\nprint(sorted(items, key=price))\n```\n:::\n\n:::cell\nA lambda is a function expression. Assigning one to a name works, although `def` is usually clearer for reusable behavior.\n\n```python\nadd_tax = lambda price: round(price * 1.08, 2)\nprint(add_tax(10))\n```\n\n```output\n10.8\n```\n:::\n\n:::cell\nLambdas are most idiomatic when passed directly to another function. `sorted()` calls this key function once for each item.\n\n```python\nitems = [("notebook", 5), ("pen", 2), ("bag", 20)]\nby_price = sorted(items, key=lambda item: item[1])\nprint(by_price)\n```\n\n```output\n[(\'pen\', 2), (\'notebook\', 5), (\'bag\', 20)]\n```\n:::\n\n:::cell\nA named function is better when the behavior should be reused or explained. It produces the same sort key, but gives the operation a name.\n\n```python\ndef price(item):\n return item[1]\n\nprint(sorted(items, key=price))\n```\n\n```output\n[(\'pen\', 2), (\'notebook\', 5), (\'bag\', 20)]\n```\n:::\n\n:::note\n- Lambdas are expressions, not statements.\n- Prefer `def` for multi-step or reused behavior.\n- Lambdas are common as `key=` functions because the behavior is local to one call.\n:::\n', 'lists.md': '+++\nslug = "lists"\ntitle = "Lists"\nsection = "Collections"\nsummary = "Lists are ordered, mutable collections."\ndoc_path = "/tutorial/datastructures.html#more-on-lists"\n+++\n\nLists are Python\'s general-purpose mutable sequence type. Use them when order matters and the collection may grow, shrink, or be rearranged.\n\nIndexing reads individual positions. `0` is the first item, and negative indexes count backward from the end.\n\nMutation and copying matter: `append()` changes the list, while `sorted()` returns a new ordered list and leaves the original alone.\n\n:::program\n```python\nnumbers = [3, 1, 4]\nnumbers.append(1)\n\nprint(numbers)\nprint(numbers[0])\nprint(numbers[-1])\nprint(sorted(numbers))\nprint(numbers)\n```\n:::\n\n:::cell\nCreate a list with square brackets. Because lists are mutable, `append()` changes this same list object.\n\n```python\nnumbers = [3, 1, 4]\nnumbers.append(1)\n\nprint(numbers)\n```\n\n```output\n[3, 1, 4, 1]\n```\n:::\n\n:::cell\nUse indexes to read positions. Negative indexes are convenient for reading from the end.\n\n```python\nprint(numbers[0])\nprint(numbers[-1])\n```\n\n```output\n3\n1\n```\n:::\n\n:::cell\nUse `sorted()` when you want an ordered copy and still need the original order afterward.\n\n```python\nprint(sorted(numbers))\nprint(numbers)\n```\n\n```output\n[1, 1, 3, 4]\n[3, 1, 4, 1]\n```\n:::\n\n:::note\n- Lists are mutable sequences: methods such as `append()` change the list in place.\n- Negative indexes count from the end.\n- `sorted()` returns a new list; `list.sort()` sorts the existing list in place.\n:::\n', 'literal-and-final.md': '+++\nslug = "literal-and-final"\ntitle = "Literal and Final"\nsection = "Types"\nsummary = "Literal restricts values, while Final marks names that should not be rebound."\ndoc_path = "/library/typing.html#typing.Literal"\n+++\n\n`Literal` restricts a value to one of a small set of exact options, and `Final` tells the type checker that a name should not be rebound. Both are static promises that type checkers enforce; Python\'s runtime assignment rules still permit any value through if a program ignores the annotation.\n\nUse them when an annotation makes a constant or a small option set explicit at the API boundary. Prefer simpler neighboring tools when the extra machinery would hide the intent.\n\n`Literal` pairs naturally with type aliases and overloads when a function should accept only a known set of strings or numbers. `Final` is most useful for module-level constants and class attributes that the rest of the codebase should treat as immutable.\n\n:::program\n```python\nfrom typing import Final, Literal\n\nMode = Literal["read", "write"]\nDEFAULT_MODE: Final[Mode] = "read"\n\n\ndef open_label(mode: Mode) -> str:\n return f"opening for {mode}"\n\nprint(open_label(DEFAULT_MODE))\nprint(open_label("write"))\nprint(DEFAULT_MODE)\n```\n:::\n\n:::cell\n`Literal` describes a small set of exact allowed values.\n\n```python\nfrom typing import Final, Literal\n\nMode = Literal["read", "write"]\nDEFAULT_MODE: Final[Mode] = "read"\n\n\ndef open_label(mode: Mode) -> str:\n return f"opening for {mode}"\n\nprint(open_label(DEFAULT_MODE))\nprint(open_label("write"))\nprint(DEFAULT_MODE)\n```\n\n```output\nopening for read\nopening for write\nread\n```\n:::\n\n:::note\n- `Literal` describes a small set of exact allowed values.\n- `Final` tells type checkers that a name should not be rebound.\n- Both are static promises; ordinary runtime assignment rules still apply.\n:::\n', 'literals.md': '+++\nslug = "literals"\ntitle = "Literals"\nsection = "Basics"\nsummary = "Literals write values directly in Python source code."\ndoc_path = "/reference/lexical_analysis.html#literals"\nsee_also = [\n "values",\n "strings",\n "numbers",\n "string-formatting",\n]\n+++\n\nLiterals are source-code forms for values: numbers, text, bytes, containers, booleans, `None`, and a few specialized markers. They are how a program writes small values directly.\n\nThe literal form is only the beginning. Later examples explain each value family in depth: strings are Unicode text, bytes are binary data, lists and dicts are containers, and `None` represents intentional absence.\n\nUse literals when the value is small and local. Give repeated or meaningful values a name so the program explains why that value matters.\n\n:::program\n```python\nwhole = 42\nfraction = 3.5\ncomplex_number = 2 + 3j\nprint(whole, fraction, complex_number.imag)\n\nflags = 0xFF\nmask = 0b1010\nmillion = 1_000_000\nprint(flags, mask, million)\n\ntext = "python"\nraw_pattern = r"\\d+"\ndata = b"py"\nscore = 98\nformatted = f"score={score}"\nprint(text)\nprint(raw_pattern)\nprint(data)\nprint(formatted)\n\npoint = (2, 3)\nnames = ["Ada", "Grace"]\nscores = {"Ada": 98}\nunique = {"py", "go"}\nprint(point)\nprint(names[0])\nprint(scores["Ada"])\nprint(sorted(unique))\n\nprint(True, False, None)\nprint(...)\n\nprint(type({}).__name__)\nprint(type(set()).__name__)\nprint(type({1, 2}).__name__)\n```\n:::\n\n:::cell\nNumeric literals write numbers directly. Complex literals use `j` for the imaginary part.\n\n```python\nwhole = 42\nfraction = 3.5\ncomplex_number = 2 + 3j\nprint(whole, fraction, complex_number.imag)\n```\n\n```output\n42 3.5 3.0\n```\n:::\n\n:::cell\nInteger literals also accept hexadecimal (`0x`), binary (`0b`), and octal (`0o`) prefixes. Underscores group digits visually without changing the value.\n\n```python\nflags = 0xFF\nmask = 0b1010\nmillion = 1_000_000\nprint(flags, mask, million)\n```\n\n```output\n255 10 1000000\n```\n:::\n\n:::cell\nString literals write Unicode text. Raw strings keep backslashes literal, bytes literals write binary data rather than text, and f-strings (`f"..."`) embed expressions inline.\n\n```python\ntext = "python"\nraw_pattern = r"\\d+"\ndata = b"py"\nscore = 98\nformatted = f"score={score}"\nprint(text)\nprint(raw_pattern)\nprint(data)\nprint(formatted)\n```\n\n```output\npython\n\\d+\nb\'py\'\nscore=98\n```\n:::\n\n:::cell\nContainer literals create tuples, lists, dictionaries, and sets. Each container answers a different question about order, position, lookup, or uniqueness.\n\n```python\npoint = (2, 3)\nnames = ["Ada", "Grace"]\nscores = {"Ada": 98}\nunique = {"py", "go"}\nprint(point)\nprint(names[0])\nprint(scores["Ada"])\nprint(sorted(unique))\n```\n\n```output\n(2, 3)\nAda\n98\n[\'go\', \'py\']\n```\n:::\n\n:::cell\n`True`, `False`, `None`, and `...` are singleton literal-like constants used for truth values, absence, and placeholders.\n\n```python\nprint(True, False, None)\nprint(...)\n```\n\n```output\nTrue False None\nEllipsis\n```\n:::\n\n:::cell\nCurly-brace literals are dictionaries by default. The empty form `{}` is an empty dictionary, not an empty set; use `set()` for that. A non-empty `{1, 2}` is a set because keyless items can only be a set.\n\n```python\nprint(type({}).__name__)\nprint(type(set()).__name__)\nprint(type({1, 2}).__name__)\n```\n\n```output\ndict\nset\nset\n```\n:::\n\n:::note\n- Literals are good for small local values; constants are better for repeated values with meaning.\n- `{}` is an empty dictionary. Use `set()` for an empty set.\n- Bytes literals are binary data; string literals are Unicode text.\n- `...` evaluates to the `Ellipsis` object.\n:::\n', 'logging.md': '+++\nslug = "logging"\ntitle = "Logging"\nsection = "Standard Library"\nsummary = "logging records operational events without using print as infrastructure."\ndoc_path = "/library/logging.html"\n+++\n\n`logging` records operational events without using `print` as infrastructure. Loggers name where each event came from, handlers route records to outputs, and levels (`DEBUG`, `INFO`, `WARNING`, `ERROR`) let operators choose how much detail they want to see.\n\nUse it when a program needs structured records with thresholds — production services, command-line tools, scheduled jobs. Prefer plain `print` when a small script just needs to show a line of human output.\n\nThe example wires a single stream handler to stdout to keep the output deterministic. Real applications usually configure logging once at startup and then call `logging.getLogger(__name__)` from each module.\n\n:::program\n```python\nimport logging\nimport sys\n\nlogger = logging.getLogger("example")\nlogger.setLevel(logging.INFO)\nhandler = logging.StreamHandler(sys.stdout)\nhandler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))\nlogger.handlers[:] = [handler]\n\nlogger.debug("hidden")\nlogger.info("service started")\nlogger.warning("disk almost full")\n```\n:::\n\n:::cell\nLoggers name where an event came from.\n\n```python\nimport logging\nimport sys\n\nlogger = logging.getLogger("example")\nlogger.setLevel(logging.INFO)\nhandler = logging.StreamHandler(sys.stdout)\nhandler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))\nlogger.handlers[:] = [handler]\n\nlogger.debug("hidden")\nlogger.info("service started")\nlogger.warning("disk almost full")\n```\n\n```output\nINFO:service started\nWARNING:disk almost full\n```\n:::\n\n:::note\n- Loggers name where an event came from.\n- Handlers decide where records go.\n- Levels let operators choose how much detail to see.\n:::\n', 'loop-else.md': '+++\nslug = "loop-else"\ntitle = "Loop Else"\nsection = "Control Flow"\nsummary = "A loop else block runs only when the loop did not end with break."\ndoc_path = "/tutorial/controlflow.html#else-clauses-on-loops"\nsee_also = [\n "break-and-continue",\n "for-loops",\n "while-loops",\n]\n+++\n\nPython loops can have an `else` clause. The name is surprising at first: loop `else` means “no `break` happened,” not “the loop condition was false.”\n\nThis is useful for searches. Put the successful early exit in `break`, then put the not-found path in `else`.\n\nUse loop `else` sparingly. It is clearest when the loop is visibly searching for something.\n\n:::program\n```python\nnames = ["Ada", "Grace", "Guido"]\n\nfor name in names:\n if name == "Grace":\n print("found")\n break\nelse:\n print("missing")\n\nfor name in names:\n if name == "Linus":\n print("found")\n break\nelse:\n print("missing")\n```\n:::\n\n:::cell\nIf the loop reaches `break`, the `else` block is skipped. This branch means the search succeeded early.\n\n```python\nnames = ["Ada", "Grace", "Guido"]\n\nfor name in names:\n if name == "Grace":\n print("found")\n break\nelse:\n print("missing")\n```\n\n```output\nfound\n```\n:::\n\n:::cell\nIf the loop finishes without `break`, the `else` block runs. This branch means the search examined every value and found nothing.\n\n```python\nfor name in names:\n if name == "Linus":\n print("found")\n break\nelse:\n print("missing")\n```\n\n```output\nmissing\n```\n:::\n\n:::note\n- Loop `else` runs when the loop was not ended by `break`.\n- It is best for search loops with a clear found/not-found split.\n- It works with both `for` and `while` loops.\n:::\n', 'match-statements.md': '+++\nslug = "match-statements"\ntitle = "Match Statements"\nsection = "Control Flow"\nsummary = "match selects cases using structural pattern matching."\ndoc_path = "/tutorial/controlflow.html#match-statements"\n+++\n\nStructural pattern matching lets a program choose a branch based on the shape of data. It is especially useful when commands, messages, or parsed data have a few known forms.\n\nA `case` pattern can both check constants and bind names. The move case checks the action and extracts `x` and `y` in one readable step.\n\nOrder matters because Python tries cases from top to bottom. Specific shapes should appear before broad fallback cases such as `_`.\n\n:::program\n```python\ncommand = {"action": "move", "x": 3, "y": 4}\n\nmatch command:\n case {"action": "move", "x": x, "y": y}:\n print(f"move to {x},{y}")\n case {"action": "quit"}:\n print("quit")\n case {"action": action}:\n print(f"unknown action: {action}")\n case _:\n print("invalid command")\n```\n:::\n\n:::cell\nUse `match` when the shape of a value is the decision. This command is a dictionary with an action and coordinates; the first case checks that shape and binds `x` and `y`.\n\n```python\ncommand = {"action": "move", "x": 3, "y": 4}\n\nmatch command:\n case {"action": "move", "x": x, "y": y}:\n print(f"move to {x},{y}")\n```\n\n```output\nmove to 3,4\n```\n:::\n\n:::cell\nOther cases describe other valid shapes. This complete fragment changes the command so the `quit` case is the first matching pattern.\n\n```python\ncommand = {"action": "quit"}\n\nmatch command:\n case {"action": "move", "x": x, "y": y}:\n print(f"move to {x},{y}")\n case {"action": "quit"}:\n print("quit")\n```\n\n```output\nquit\n```\n:::\n\n:::cell\nBroader patterns and the `_` catch-all belong after specific cases. This fragment extracts an unknown action before the final fallback would run.\n\n```python\ncommand = {"action": "jump"}\n\nmatch command:\n case {"action": "move", "x": x, "y": y}:\n print(f"move to {x},{y}")\n case {"action": "quit"}:\n print("quit")\n case {"action": action}:\n print(f"unknown action: {action}")\n case _:\n print("invalid command")\n```\n\n```output\nunknown action: jump\n```\n:::\n\n:::note\n- `match` compares structure, not just equality.\n- Patterns can bind names such as `x` and `y` while matching.\n- Put the catch-all `_` case last, because cases are tried from top to bottom.\n:::\n', 'metaclasses.md': '+++\nslug = "metaclasses"\ntitle = "Metaclasses"\nsection = "Classes"\nsummary = "A metaclass customizes how classes themselves are created."\ndoc_path = "/reference/datamodel.html#metaclasses"\nsee_also = [\n "classes",\n "inheritance-and-super",\n "special-methods",\n]\n+++\n\nA metaclass is the class of a class. Most Python code never needs one, but the syntax appears in frameworks that register, validate, or modify classes as they are created.\n\nThe `metaclass=` keyword in a class statement chooses the object that builds the class. This is advanced machinery; decorators and ordinary functions are usually simpler.\n\nUse metaclasses only when class creation itself is the problem being solved.\n\n:::program\n```python\nclass Tagged(type):\n def __new__(mcls, name, bases, namespace):\n namespace["tag"] = name.lower()\n return super().__new__(mcls, name, bases, namespace)\n\nclass Event(metaclass=Tagged):\n pass\n\nprint(Event.tag)\nprint(type(Event).__name__)\n```\n:::\n\n:::cell\nA metaclass customizes class creation. `__new__` receives the class name, bases, and namespace before the class object exists.\n\n```python\nclass Tagged(type):\n def __new__(mcls, name, bases, namespace):\n namespace["tag"] = name.lower()\n return super().__new__(mcls, name, bases, namespace)\n\nprint(Tagged.__name__)\n```\n\n```output\nTagged\n```\n:::\n\n:::cell\nThe `metaclass=` keyword applies that class-building rule. Here the metaclass adds a `tag` attribute to the new class.\n\n```python\nclass Event(metaclass=Tagged):\n pass\n\nprint(Event.tag)\nprint(type(Event).__name__)\n```\n\n```output\nevent\nTagged\n```\n:::\n\n:::note\n- Metaclasses customize class creation, not instance behavior directly.\n- Most code should prefer class decorators, functions, or ordinary inheritance.\n- You are most likely to meet metaclasses inside frameworks and ORMs.\n:::\n', 'modules.md': '+++\nslug = "modules"\ntitle = "Modules"\nsection = "Modules"\nsummary = "Modules organize code into namespaces and expose reusable definitions."\ndoc_path = "/tutorial/modules.html"\nsee_also = [\n "import-aliases",\n "packages",\n]\n+++\n\nModules organize Python code into files and namespaces. `import` executes a module once, stores it in Python\'s import cache, and gives your program access to its definitions.\n\nThis page focuses on import forms and module namespaces. Package layout, aliases, and dynamic imports have their own neighboring examples.\n\nUse module namespaces such as `math.sqrt` when the source of a name should stay visible. Use focused imports such as `from statistics import mean` when the imported name is clear at the call site.\n\n:::program\n```python\nimport math\nimport sys\nfrom statistics import mean\n\nradius = 3\narea = math.pi * radius ** 2\nprint(round(area, 2))\n\nscores = [8, 10, 9]\nprint(mean(scores))\n\nprint(math.__name__)\nprint("math" in sys.modules)\n```\n:::\n\n:::cell\nImporting a module gives access to its namespace. The `math.` prefix makes it clear where `pi` came from.\n\n```python\nimport math\n\nradius = 3\narea = math.pi * radius ** 2\nprint(round(area, 2))\n```\n\n```output\n28.27\n```\n:::\n\n:::cell\nA focused `from ... import ...` brings one definition into the current namespace. This keeps a common operation concise without importing every name.\n\n```python\nfrom statistics import mean\n\nscores = [8, 10, 9]\nprint(mean(scores))\n```\n\n```output\n9\n```\n:::\n\n:::cell\nModules are objects too. Their attributes include metadata such as `__name__`, which records the module\'s import name.\n\n```python\nprint(math.__name__)\n```\n\n```output\nmath\n```\n:::\n\n:::cell\nImported modules are cached in `sys.modules`. Later imports reuse the module object instead of executing the file again.\n\n```python\nimport sys\nprint("math" in sys.modules)\n```\n\n```output\nTrue\n```\n:::\n\n:::note\n- Prefer plain `import module` when the namespace improves readability.\n- Use focused imports for a small number of clear names.\n- Place imports near the top of the file.\n- Imports execute module top-level code once, then reuse the cached module object.\n:::\n', 'multiple-return-values.md': '+++\nslug = "multiple-return-values"\ntitle = "Multiple Return Values"\nsection = "Functions"\nsummary = "Python returns multiple values by returning a tuple and unpacking it."\ndoc_path = "/tutorial/datastructures.html#tuples-and-sequences"\n+++\n\nPython multiple return values are tuple return values with friendly syntax. `return a, b` creates one tuple containing two positions.\n\nMost callers unpack that tuple immediately. Good target names make the meaning of each returned position explicit.\n\nUse this for small, fixed groups of results. For larger records, a dataclass or named tuple usually communicates better.\n\n:::program\n```python\ndef divide_with_remainder(total, size):\n quotient = total // size\n remainder = total % size\n return quotient, remainder\n\nresult = divide_with_remainder(17, 5)\nprint(result)\n\nboxes, leftover = result\nprint(boxes)\nprint(leftover)\n```\n:::\n\n:::cell\nReturning values separated by commas returns one tuple. The tuple is visible if the caller stores the result directly.\n\n```python\ndef divide_with_remainder(total, size):\n quotient = total // size\n remainder = total % size\n return quotient, remainder\n\nresult = divide_with_remainder(17, 5)\nprint(result)\n```\n\n```output\n(3, 2)\n```\n:::\n\n:::cell\nCallers usually unpack the tuple immediately or soon after. The names at the call site document what each position means.\n\n```python\nboxes, leftover = result\nprint(boxes)\nprint(leftover)\n```\n\n```output\n3\n2\n```\n:::\n\n:::note\n- A comma creates a tuple; `return a, b` returns one tuple containing two values.\n- Unpacking at the call site gives each returned position a meaningful name.\n- Use a class-like record when the result has many fields.\n:::\n', 'mutability.md': '+++\nslug = "mutability"\ntitle = "Mutability"\nsection = "Data Model"\nsummary = "Some objects change in place, while others return new values."\ndoc_path = "/reference/datamodel.html#objects-values-and-types"\n+++\n\nObjects in Python can be mutable or immutable. Mutable objects such as lists and dictionaries can change in place, while immutable objects such as strings and tuples produce new values instead.\n\nNames can share one mutable object, so a change through one name is visible through another. This is powerful, but it is also the source of many beginner surprises.\n\nThe boundary matters across Python: `append()` mutates a list, string methods return new strings, and `sorted()` returns a new list while `list.sort()` mutates an existing one.\n\n:::program\n```python\nfirst = ["python"]\nsecond = first\nsecond.append("workers")\nprint(first)\nprint(second)\n\ntext = "python"\nupper_text = text.upper()\nprint(text)\nprint(upper_text)\n\nnumbers = [3, 1, 2]\nordered = sorted(numbers)\nprint(ordered)\nprint(numbers)\n```\n:::\n\n:::cell\nMutable objects can change in place. `first` and `second` point to the same list, so appending through one name changes the object seen through both names.\n\n```python\nfirst = ["python"]\nsecond = first\nsecond.append("workers")\nprint(first)\nprint(second)\n```\n\n```output\n[\'python\', \'workers\']\n[\'python\', \'workers\']\n```\n:::\n\n:::cell\nImmutable objects do not change in place. String methods such as `upper()` return a new string, leaving the original string unchanged.\n\n```python\ntext = "python"\nupper_text = text.upper()\nprint(text)\nprint(upper_text)\n```\n\n```output\npython\nPYTHON\n```\n:::\n\n:::cell\nSome APIs make the boundary explicit. `sorted()` returns a new list, while methods such as `append()` and `list.sort()` mutate an existing list.\n\n```python\nnumbers = [3, 1, 2]\nordered = sorted(numbers)\nprint(ordered)\nprint(numbers)\n```\n\n```output\n[1, 2, 3]\n[3, 1, 2]\n```\n:::\n\n:::note\n- Lists and dictionaries are mutable; strings and tuples are immutable.\n- Aliasing is useful, but copy mutable containers when independent changes are needed.\n- Pay attention to whether an operation mutates in place or returns a new value.\n:::\n', 'networking.md': '+++\nslug = "networking"\ntitle = "Networking"\nsection = "Standard Library"\nsummary = "Networking code exchanges bytes across explicit protocol boundaries."\ndoc_path = "/library/socket.html"\nexpected_output = "b\'ping\'\\nping\\n"\n+++\n\nNetworking code sends and receives bytes. Higher-level HTTP clients hide many details, but the core boundary is still explicit: text must be encoded before sending and decoded after receiving.\n\nThis example uses `socket.socketpair()` so it stays local and deterministic in ordinary Python. Real network clients often use `socket.create_connection()` or a higher-level HTTP library, but the same byte boundary applies.\n\nThe useful mental model is endpoint plus bytes plus cleanup. A socket connects two endpoints, transfers byte strings, and must be closed when the conversation is finished.\n\n:::program\n```python\nimport socket\n\nleft, right = socket.socketpair()\ntry:\n message = "ping"\n left.sendall(message.encode("utf-8"))\n data = right.recv(16)\n print(data)\n print(data.decode("utf-8"))\nfinally:\n left.close()\n right.close()\n```\n:::\n\n:::unsupported\nDynamic Workers do not provide arbitrary low-level sockets, and this app disables Dynamic Worker outbound access.\n\n```python\nleft, right = socket.socketpair()\nleft.sendall("ping".encode("utf-8"))\ndata = right.recv(16)\n```\n:::\n\n:::cell\nSockets exchange bytes. Encoding and decoding make the boundary between Python text and network data visible.\n\n```python\nimport socket\n\nleft, right = socket.socketpair()\ntry:\n message = "ping"\n left.sendall(message.encode("utf-8"))\n data = right.recv(16)\n print(data)\n print(data.decode("utf-8"))\nfinally:\n left.close()\n right.close()\n```\n\n```output\nb\'ping\'\nping\n```\n:::\n\n:::note\n- Network protocols move bytes, not Python `str` objects.\n- Close real sockets when finished, usually with a context manager or `finally` block.\n- Use high-level HTTP libraries for application HTTP unless socket-level control is the lesson.\n:::\n', 'newtype.md': '+++\nslug = "newtype"\ntitle = "NewType"\nsection = "Types"\nsummary = "NewType creates distinct static identities for runtime-compatible values."\ndoc_path = "/library/typing.html#typing.NewType"\nsee_also = [\n "type-aliases",\n "type-hints",\n "runtime-type-checks",\n]\n+++\n\n`NewType` creates a distinct static identity for a value that is represented by an existing runtime type. It is useful for IDs, units, and other values that should not be mixed accidentally.\n\nThe key boundary is static versus runtime behavior. A type checker can distinguish `UserId` from `OrderId`, but at runtime both values are plain integers.\n\nUse a type alias when you only want a clearer name for a shape. Use `NewType` when mixing two compatible shapes should be treated as a mistake by static analysis.\n\n:::program\n```python\nfrom typing import NewType\n\nUserId = NewType("UserId", int)\nOrderId = NewType("OrderId", int)\n\n\ndef load_user(user_id: UserId) -> str:\n return f"user {user_id}"\n\nuid = UserId(42)\noid = OrderId(42)\nprint(load_user(uid))\nprint(uid == oid)\nprint(type(uid).__name__)\nprint(UserId.__name__)\n```\n:::\n\n:::cell\n`NewType` helps type checkers distinguish values that share a runtime representation.\n\n```python\nfrom typing import NewType\n\nUserId = NewType("UserId", int)\nOrderId = NewType("OrderId", int)\n\n\ndef load_user(user_id: UserId) -> str:\n return f"user {user_id}"\n\nuid = UserId(42)\nprint(load_user(uid))\n```\n\n```output\nuser 42\n```\n:::\n\n:::cell\nAt runtime, a `NewType` value is the underlying value. It compares like that value and has the same runtime type.\n\n```python\noid = OrderId(42)\nprint(uid == oid)\nprint(type(uid).__name__)\n```\n\n```output\nTrue\nint\n```\n:::\n\n:::cell\nThe `NewType` constructor keeps a name for static tools and introspection.\n\n```python\nprint(UserId.__name__)\nprint(OrderId.__name__)\n```\n\n```output\nUserId\nOrderId\n```\n:::\n\n:::note\n- `NewType` helps type checkers distinguish values that share a runtime representation.\n- At runtime, the value is still the underlying type.\n- Use aliases for readability; use `NewType` for static separation.\n:::\n', 'none.md': '+++\nslug = "none"\ntitle = "None"\nsection = "Basics"\nsummary = "None represents expected absence, distinct from missing keys and errors."\ndoc_path = "/library/constants.html#None"\n+++\n\n`None` represents the absence of a value. It is the usual sentinel when a function has no result to return but the absence itself is meaningful.\n\nBecause `None` is a singleton, idiomatic Python checks it with `is None` or `is not None`. This avoids confusing identity with value equality.\n\nAbsence has several nearby shapes in Python. A function can return `None`, a dictionary lookup can supply a default for a missing key, and an invalid operation can raise an exception.\n\n:::program\n```python\nresult = None\nprint(result is None)\n\ndef find_score(name):\n if name == "Ada":\n return 10\n return None\n\nscore = find_score("Grace")\nprint(score is None)\n\nprofile = {"name": "Ada"}\nprint(profile.get("timezone", "UTC"))\n\ntry:\n int("python")\nexcept ValueError:\n print("invalid number")\n```\n:::\n\n:::cell\n`None` is Python\'s value for “nothing here.” Check it with `is None` because it is a singleton identity value.\n\n```python\nresult = None\nprint(result is None)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\nFunctions often return `None` when absence is expected and callers can continue. The function name and surrounding code should make that possibility clear.\n\n```python\ndef find_score(name):\n if name == "Ada":\n return 10\n return None\n\nscore = find_score("Grace")\nprint(score is None)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\nA missing dictionary key is another absence boundary. Use `get()` when the mapping can supply a default, and use exceptions for invalid operations that cannot produce a value.\n\n```python\nprofile = {"name": "Ada"}\nprint(profile.get("timezone", "UTC"))\n\ntry:\n int("python")\nexcept ValueError:\n print("invalid number")\n```\n\n```output\nUTC\ninvalid number\n```\n:::\n\n:::note\n- Use `is None` rather than `== None`; `None` is a singleton identity value.\n- Use `None` for expected absence that callers can test.\n- Use dictionary defaults for missing mapping keys and exceptions for invalid operations.\n:::\n', 'number-parsing.md': '+++\nslug = "number-parsing"\ntitle = "Number Parsing"\nsection = "Standard Library"\nsummary = "int() and float() parse text into numbers and raise ValueError on bad input."\ndoc_path = "/library/functions.html#int"\n+++\n\nParsing turns text into numeric values. `int()` parses whole-number text and `float()` parses decimal or scientific-notation text.\n\nInvalid numeric text raises `ValueError`. Catching that specific exception makes it clear that bad input is expected and recoverable.\n\nAfter parsing, the result is a number and can participate in arithmetic; before parsing, it is just text.\n\n:::program\n```python\nprint(int("42"))\nprint(float("3.5"))\n\ntry:\n int("python")\nexcept ValueError as error:\n print(type(error).__name__)\n```\n:::\n\n:::cell\nUse `int()` for whole numbers and `float()` for decimal text. Parsed values are real numbers, not strings.\n\n```python\nprint(int("42"))\nprint(float("3.5"))\n```\n\n```output\n42\n3.5\n```\n:::\n\n:::cell\nBad numeric text raises `ValueError`. Catch that specific exception when invalid input is part of the normal flow.\n\n```python\ntry:\n int("python")\nexcept ValueError as error:\n print(type(error).__name__)\n```\n\n```output\nValueError\n```\n:::\n\n:::note\n- `int()` and `float()` are constructors that also parse strings.\n- Catch `ValueError` when bad user input is expected and recoverable.\n:::\n', 'numbers.md': '+++\nslug = "numbers"\ntitle = "Numbers"\nsection = "Basics"\nsummary = "Python numbers include integers, floats, and complex values."\ndoc_path = "/library/stdtypes.html#numeric-types-int-float-complex"\nsee_also = [\n "literals",\n "operators",\n]\n+++\n\nPython\'s numeric model starts with `int`, `float`, and `complex`. Integers are arbitrary precision, floats are approximate double-precision values, and complex numbers carry real and imaginary parts.\n\nOperators encode different numeric questions. `/` means true division and returns a float, `//` means floor division, `%` gives the remainder, and `**` computes powers.\n\nUse rounding for display, not as a substitute for understanding floating-point approximation. Financial code usually needs `decimal.Decimal`, which is a separate precision topic.\n\n:::program\n```python\nimport math\n\ncount = 10\nratio = 0.25\nz = 2 + 3j\n\nprint(count + 5)\nprint(count / 4)\nprint(count // 4)\nprint(count % 4)\nprint(2 ** 5)\nprint(z.real, z.imag)\nprint(0.1 + 0.2)\nprint(0.1 + 0.2 == 0.3)\nprint(math.isclose(0.1 + 0.2, 0.3))\nprint(round(3.14159, 2))\n```\n:::\n\n:::cell\nPython has `int` for whole numbers and `float` for approximate real-valued arithmetic. True division with `/` returns a `float`, even when both inputs are integers.\n\n```python\ncount = 10\nratio = 0.25\n\nprint(count + 5)\nprint(count / 4)\nprint(ratio * 2)\n```\n\n```output\n15\n2.5\n0.5\n```\n:::\n\n:::cell\nFloor division and modulo are useful when you need quotient and remainder behavior. Powers use `**`, not `^`.\n\n```python\nprint(count // 4)\nprint(count % 4)\nprint(2 ** 5)\n```\n\n```output\n2\n2\n32\n```\n:::\n\n:::cell\nComplex numbers are built in. The literal suffix `j` marks the imaginary part.\n\n```python\nz = 2 + 3j\nprint(z.real, z.imag)\n```\n\n```output\n2.0 3.0\n```\n:::\n\n:::cell\nFloating-point values are approximate, so `==` between expected and computed floats is rarely the right test. Compare with `math.isclose` (or work in `decimal.Decimal`) when the question is "are these the same number to within tolerance".\n\n```python\nimport math\n\nprint(0.1 + 0.2)\nprint(0.1 + 0.2 == 0.3)\nprint(math.isclose(0.1 + 0.2, 0.3))\nprint(round(3.14159, 2))\n```\n\n```output\n0.30000000000000004\nFalse\nTrue\n3.14\n```\n:::\n\n:::note\n- Python\'s `int` has arbitrary precision; it grows as large as memory allows.\n- Python\'s `float` is approximate double-precision floating point.\n- Use `/` for true division and `//` for floor division.\n- Use `math.isclose` instead of `==` for floating-point comparison; reach for `decimal.Decimal` when exact decimal precision is the domain requirement.\n:::\n', 'object-lifecycle.md': '+++\nslug = "object-lifecycle"\ntitle = "Object Lifecycle"\nsection = "Basics"\nsummary = "References keep objects alive until Python can reclaim them."\ndoc_path = "/reference/datamodel.html#objects-values-and-types"\n+++\n\nReferences keep objects alive until Python can reclaim them. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nimport gc\n\nnames = []\nalias = names\nalias.append("Ada")\n\nprint(names is alias)\nprint(names)\n\nobject_id = id(names)\ndel alias\nprint(id(names) == object_id)\n\ndel names\nprint("object can be reclaimed")\ngc.collect()\n```\n:::\n\n:::cell\nUse `is` and `id()` to observe identity while two names refer to the same object.\n\n```python\nimport gc\n\nnames = []\nalias = names\nalias.append("Ada")\n\nprint(names is alias)\nprint(names)\n\nobject_id = id(names)\ndel alias\nprint(id(names) == object_id)\n\ndel names\nprint("object can be reclaimed")\ngc.collect()\n```\n\n```output\nTrue\n[\'Ada\']\nTrue\nobject can be reclaimed\n```\n:::\n\n:::note\n- Use `is` and `id()` to observe identity while two names refer to the same object.\n- Deleting a name removes one reference; it does not directly destroy the object if another reference still exists.\n- Python reclaims unreachable objects automatically, so programs usually manage ownership by controlling references.\n:::\n', 'operator-overloading.md': '+++\nslug = "operator-overloading"\ntitle = "Operator Overloading"\nsection = "Data Model"\nsummary = "Operator methods let objects define arithmetic and comparison syntax."\ndoc_path = "/reference/datamodel.html#emulating-numeric-types"\nsee_also = [\n "operators",\n "special-methods",\n "equality-and-identity",\n]\n+++\n\nOperator overloading lets a class define what expressions such as `a + b` mean for its objects. This is useful when the operation is part of the domain vocabulary.\n\nThe method should preserve the meaning readers expect from the operator. Vectors can add component by component; money can add amounts in the same currency; surprising overloads make code harder to trust.\n\nPython also has reflected methods such as `__radd__` for cases where the left operand does not know how to handle the right operand. That keeps mixed operations possible without making every type know every other type.\n\n:::program\n```python\nclass Vector:\n def __init__(self, x, y):\n self.x = x\n self.y = y\n\n def __add__(self, other):\n return Vector(self.x + other.x, self.y + other.y)\n\n def __eq__(self, other):\n return (self.x, self.y) == (other.x, other.y)\n\n def __repr__(self):\n return f"Vector({self.x}, {self.y})"\n\nprint(Vector(2, 3) + Vector(4, 5))\nprint(Vector(1, 1) == Vector(1, 1))\n```\n:::\n\n:::cell\n`__add__` defines how the `+` operator combines two objects.\n\n```python\nclass Vector:\n def __init__(self, x, y):\n self.x = x\n self.y = y\n\n def __add__(self, other):\n return Vector(self.x + other.x, self.y + other.y)\n\n def __repr__(self):\n return f"Vector({self.x}, {self.y})"\n\nprint(Vector(2, 3) + Vector(4, 5))\n```\n\n```output\nVector(6, 8)\n```\n:::\n\n:::cell\n`__eq__` defines value equality for `==`. Without it, user-defined objects compare by identity.\n\n```python\nclass Vector:\n def __init__(self, x, y):\n self.x = x\n self.y = y\n\n def __eq__(self, other):\n return (self.x, self.y) == (other.x, other.y)\n\nprint(Vector(1, 1) == Vector(1, 1))\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\nA useful `__repr__` makes operator results inspectable while debugging.\n\n```python\nclass Vector:\n def __init__(self, x, y):\n self.x = x\n self.y = y\n\n def __add__(self, other):\n return Vector(self.x + other.x, self.y + other.y)\n\n def __repr__(self):\n return f"Vector({self.x}, {self.y})"\n\nprint(repr(Vector(2, 3) + Vector(4, 5)))\n```\n\n```output\nVector(6, 8)\n```\n:::\n\n:::note\n- Overload operators only when the operation is unsurprising.\n- Return `NotImplemented` when an operand type is unsupported.\n- Implement equality deliberately when value comparison matters.\n:::\n', 'operators.md': '+++\nslug = "operators"\ntitle = "Operators"\nsection = "Basics"\nsummary = "Operators combine, compare, and test values in expressions."\ndoc_path = "/reference/expressions.html#operator-precedence"\nsee_also = [\n "numbers",\n "equality-and-identity",\n "assignment-expressions",\n "operator-overloading",\n]\n+++\n\nOperators are the punctuation and keywords that combine values into expressions. Some operators compute new values, some compare values, and some ask relationship questions such as membership or identity.\n\nThis page is the surface map. Focused examples explain the deeper behavior of numbers, booleans, conditions, sets, assignment expressions, and operator overloading.\n\nRead operators by the question they ask: arithmetic computes, comparison answers true or false, boolean operators combine truth values, membership searches a container, and specialized operators should only appear when the data shape calls for them.\n\n:::program\n```python\ncount = 10\nprint(count + 5)\nprint(count // 4)\nprint(count % 4)\nprint(2 ** 5)\n\nscore = 91\nprint(80 <= score < 100)\nprint(score == 100 or score >= 90)\nprint("py" in "python")\n\nflags = 0b0011\nprint(flags & 0b0101)\nprint(flags | 0b0100)\nprint(flags ^ 0b0101)\nprint(flags << 1)\n\nclass Scale:\n def __init__(self, value):\n self.value = value\n\n def __matmul__(self, other):\n return self.value * other.value\n\nprint(Scale(2) @ Scale(3))\n\nitems = ["a", "b"]\nif (size := len(items)) > 0:\n print(size)\n```\n:::\n\n:::cell\nArithmetic operators compute new values. Use `//` for floor division, `%` for remainder, and `**` for powers.\n\n```python\ncount = 10\nprint(count + 5)\nprint(count // 4)\nprint(count % 4)\nprint(2 ** 5)\n```\n\n```output\n15\n2\n2\n32\n```\n:::\n\n:::cell\nComparison operators produce booleans. Python comparisons can chain, which keeps range checks readable.\n\n```python\nscore = 91\nprint(80 <= score < 100)\nprint(score == 100 or score >= 90)\nprint("py" in "python")\n```\n\n```output\nTrue\nTrue\nTrue\n```\n:::\n\n:::cell\nBitwise operators work on integer bit patterns. They are useful for masks and flags, not ordinary boolean logic. `&` is bitwise AND, `|` is bitwise OR, `^` is exclusive OR, and `<<` shifts left.\n\n```python\nflags = 0b0011\nprint(flags & 0b0101)\nprint(flags | 0b0100)\nprint(flags ^ 0b0101)\nprint(flags << 1)\n```\n\n```output\n1\n7\n6\n6\n```\n:::\n\n:::cell\nThe `@` operator is reserved for matrix-like multiplication and custom types that define `__matmul__`.\n\n```python\nclass Scale:\n def __init__(self, value):\n self.value = value\n\n def __matmul__(self, other):\n return self.value * other.value\n\nprint(Scale(2) @ Scale(3))\n```\n\n```output\n6\n```\n:::\n\n:::cell\nThe walrus operator `:=` assigns inside an expression. Use it when naming a value avoids repeating work in a condition.\n\n```python\nitems = ["a", "b"]\nif (size := len(items)) > 0:\n print(size)\n```\n\n```output\n2\n```\n:::\n\n:::cell\n`and` and `or` short-circuit: the right side runs only when the left side cannot already determine the result. That makes them safe for guard expressions like `obj and obj.value` where the right side would fail on `None`.\n\n```python\ndef loud():\n print("ran")\n return True\n\nprint(False and loud())\nprint(True or loud())\nprint(True and loud())\n```\n\n```output\nFalse\nTrue\nran\nTrue\n```\n:::\n\n:::note\n- Use the clearest operator for the question: arithmetic, comparison, boolean logic, membership, identity, or bitwise manipulation.\n- `and` and `or` short-circuit, so the right side may not run.\n- Operators have precedence; use parentheses when grouping would otherwise be hard to read.\n- Custom operator behavior should make an object feel more natural, not more clever.\n:::\n', 'overloads.md': '+++\nslug = "overloads"\ntitle = "Overloads"\nsection = "Types"\nsummary = "overload describes APIs whose return type depends on argument types."\ndoc_path = "/library/typing.html#typing.overload"\n+++\n\noverload describes APIs whose return type depends on argument types. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nfrom typing import overload\n\n@overload\ndef double(value: int) -> int: ...\n\n@overload\ndef double(value: str) -> str: ...\n\ndef double(value):\n return value * 2\n\nprint(double(4))\nprint(double("ha"))\nprint(double.__name__)\n```\n:::\n\n:::cell\n`@overload` signatures are for static type checkers.\n\n```python\nfrom typing import overload\n\n@overload\ndef double(value: int) -> int: ...\n\n@overload\ndef double(value: str) -> str: ...\n\ndef double(value):\n return value * 2\n\nprint(double(4))\nprint(double("ha"))\nprint(double.__name__)\n```\n\n```output\n8\nhaha\ndouble\n```\n:::\n\n:::note\n- `@overload` signatures are for static type checkers.\n- The real implementation comes after the overload declarations.\n- Use overloads when a single runtime function has multiple precise static shapes.\n:::\n', 'packages.md': '+++\nslug = "packages"\ntitle = "Packages"\nsection = "Modules"\nsummary = "Packages organize modules into importable directories."\ndoc_path = "/tutorial/modules.html#packages"\nsee_also = [\n "modules",\n "import-aliases",\n "virtual-environments",\n]\n+++\n\nPackages are modules that can contain other modules. They let a project group related code behind dotted import paths such as `json.decoder` or `email.message`.\n\nAt runtime, importing a submodule gives Python a path through that package structure. In a project on disk, that structure is usually a directory with Python files and often an `__init__.py` file.\n\nUse packages when one module has grown into a small namespace of related modules. Keep module names boring and explicit so readers can tell where imported definitions come from.\n\n:::program\n```python\nimport importlib\nimport json\nimport json.decoder\n\nmodule = importlib.import_module("json.decoder")\n\nprint(json.__name__)\nprint(json.decoder.__name__)\nprint(module.JSONDecoder.__name__)\nprint(module is json.decoder)\n\n\nimport os\nimport sys\nimport tempfile\n\nwith tempfile.TemporaryDirectory() as tmp:\n pkg = os.path.join(tmp, "shapes")\n os.makedirs(pkg)\n with open(os.path.join(pkg, "__init__.py"), "w") as init:\n init.write("from .square import area\\n__all__ = [\'area\']\\n")\n with open(os.path.join(pkg, "square.py"), "w") as square:\n square.write("def area(side):\\n return side * side\\n")\n sys.path.insert(0, tmp)\n try:\n import shapes\n print(shapes.area(3))\n print(shapes.__all__)\n finally:\n sys.path.remove(tmp)\n sys.modules.pop("shapes", None)\n sys.modules.pop("shapes.square", None)\n```\n:::\n\n:::cell\nA package is itself a module. The `json` package exposes a namespace that can contain submodules.\n\n```python\nimport json\n\nprint(json.__name__)\n```\n\n```output\njson\n```\n:::\n\n:::cell\nDotted imports name a path through a package. Importing `json.decoder` makes that submodule available under the package namespace.\n\n```python\nimport json.decoder\n\nprint(json.decoder.__name__)\nprint(json.decoder.JSONDecoder.__name__)\n```\n\n```output\njson.decoder\nJSONDecoder\n```\n:::\n\n:::cell\n`importlib.import_module()` imports by string. It is useful for plugin systems and dynamic imports, but ordinary `import` is clearer when the dependency is known.\n\n```python\nimport importlib\n\nmodule = importlib.import_module("json.decoder")\nprint(module is json.decoder)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\nInside a package\'s `__init__.py`, `from .submodule import name` re-exports a submodule\'s name at the package root, and `__all__` lists the names that `from package import *` should make visible. This cell builds a temporary `shapes` package on disk to make both forms concrete.\n\n```python\nimport os\nimport sys\nimport tempfile\n\nwith tempfile.TemporaryDirectory() as tmp:\n pkg = os.path.join(tmp, "shapes")\n os.makedirs(pkg)\n with open(os.path.join(pkg, "__init__.py"), "w") as init:\n init.write("from .square import area\\n__all__ = [\'area\']\\n")\n with open(os.path.join(pkg, "square.py"), "w") as square:\n square.write("def area(side):\\n return side * side\\n")\n sys.path.insert(0, tmp)\n try:\n import shapes\n print(shapes.area(3))\n print(shapes.__all__)\n finally:\n sys.path.remove(tmp)\n sys.modules.pop("shapes", None)\n sys.modules.pop("shapes.square", None)\n```\n\n```output\n9\n[\'area\']\n```\n:::\n\n:::note\n- A package is a module that can contain submodules.\n- Dotted imports should mirror a meaningful project structure.\n- Use `from .submodule import name` inside a package to re-export submodule names; set `__all__` to declare the public surface.\n- Prefer ordinary imports unless the module name is truly dynamic.\n:::\n', 'paramspec.md': '+++\nslug = "paramspec"\ntitle = "ParamSpec"\nsection = "Types"\nsummary = "ParamSpec preserves callable parameter types through wrappers."\ndoc_path = "/library/typing.html#typing.ParamSpec"\n+++\n\n`ParamSpec` lets a wrapper preserve the parameter types of the function it wraps. The pressure that justifies it is decorators: a generic decorator that returns `Callable[..., R]` erases the wrapped function\'s argument types, so callers lose type-checker help on every call.\n\nUse `ParamSpec` when a decorator should be transparent to type checkers — the wrapped function and the decorated name should accept the same arguments. Reach for plain `Callable` when the wrapper deliberately changes the signature.\n\n`P.args` and `P.kwargs` annotate the `*args` and `**kwargs` of the inner wrapper, which is how the parameter spec gets bound. Pair `ParamSpec` with a `TypeVar` for the return type when the wrapper should also stay generic over what the wrapped function returns.\n\n:::program\n```python\nfrom collections.abc import Callable\nfrom typing import ParamSpec, TypeVar\n\nP = ParamSpec("P")\nR = TypeVar("R")\n\n\ndef logged(func: Callable[P, R]) -> Callable[P, R]:\n def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n print("calling", func.__name__)\n return func(*args, **kwargs)\n return wrapper\n\n@logged\ndef add(left: int, right: int) -> int:\n return left + right\n\nprint(add(2, 3))\n```\n:::\n\n:::cell\n`ParamSpec` captures the parameters of a callable.\n\n```python\nfrom collections.abc import Callable\nfrom typing import ParamSpec, TypeVar\n\nP = ParamSpec("P")\nR = TypeVar("R")\n\n\ndef logged(func: Callable[P, R]) -> Callable[P, R]:\n def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n print("calling", func.__name__)\n return func(*args, **kwargs)\n return wrapper\n\n@logged\ndef add(left: int, right: int) -> int:\n return left + right\n\nprint(add(2, 3))\n```\n\n```output\ncalling add\n5\n```\n:::\n\n:::note\n- `ParamSpec` captures the parameters of a callable.\n- Wrappers can forward `*args` and `**kwargs` without erasing the original signature for type checkers.\n- This matters most for decorators.\n:::\n', 'partial-functions.md': '+++\nslug = "partial-functions"\ntitle = "Partial Functions"\nsection = "Functions"\nsummary = "functools.partial pre-fills arguments to make a more specific callable."\ndoc_path = "/library/functools.html#functools.partial"\n+++\n\nfunctools.partial pre-fills arguments to make a more specific callable. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nfrom functools import partial\n\n\ndef apply_tax(rate, amount):\n return round(amount * (1 + rate), 2)\n\nvat = partial(apply_tax, 0.2)\nservice_tax = partial(apply_tax, rate=0.1)\n\nprint(vat(50))\nprint(service_tax(amount=80))\nprint(vat.func.__name__)\n```\n:::\n\n:::cell\nA partial object remembers some arguments.\n\n```python\nfrom functools import partial\n\n\ndef apply_tax(rate, amount):\n return round(amount * (1 + rate), 2)\n\nvat = partial(apply_tax, 0.2)\nservice_tax = partial(apply_tax, rate=0.1)\n\nprint(vat(50))\nprint(service_tax(amount=80))\nprint(vat.func.__name__)\n```\n\n```output\n60.0\n88.0\napply_tax\n```\n:::\n\n:::note\n- A partial object remembers some arguments.\n- The resulting callable can be passed where an ordinary function is expected.\n- Prefer a named function when the pre-filled behavior needs richer logic.\n:::\n', 'positional-only-parameters.md': '+++\nslug = "positional-only-parameters"\ntitle = "Positional-only Parameters"\nsection = "Functions"\nsummary = "Use / to mark parameters that callers must pass by position."\ndoc_path = "/tutorial/controlflow.html#special-parameters"\nsee_also = [\n "keyword-only-arguments",\n "functions",\n "args-and-kwargs",\n]\n+++\n\nA `/` in a function signature marks the parameters before it as positional-only. Callers must pass those arguments by position, not by keyword.\n\nThis is useful when parameter names are implementation details or when an API should match built-in functions that accept positional values.\n\nTogether, `/` and `*` let a signature draw clear boundaries: positional-only inputs, ordinary inputs, and keyword-only options.\n\n:::program\n```python\ndef scale(value, /, factor=2, *, clamp=False):\n result = value * factor\n if clamp:\n result = min(result, 10)\n return result\n\nprint(scale(4))\nprint(scale(4, factor=3))\nprint(scale(4, clamp=True))\n```\n:::\n\n:::cell\nParameters before `/` are positional-only. `value` is the main input, while `factor` remains an ordinary parameter that can be named.\n\n```python\ndef scale(value, /, factor=2, *, clamp=False):\n result = value * factor\n if clamp:\n result = min(result, 10)\n return result\n\nprint(scale(4))\nprint(scale(4, factor=3))\n```\n\n```output\n8\n12\n```\n:::\n\n:::cell\nParameters after `*` are keyword-only. That makes options such as `clamp` explicit at the call site.\n\n```python\nprint(scale(4, clamp=True))\n```\n\n```output\n8\n```\n:::\n\n:::note\n- `/` marks parameters before it as positional-only.\n- `*` marks parameters after it as keyword-only.\n- Use these markers when the call shape is part of the API design.\n:::\n', 'properties.md': '+++\nslug = "properties"\ntitle = "Properties"\nsection = "Classes"\nsummary = "@property keeps attribute syntax while adding computation or validation."\ndoc_path = "/library/functions.html#property"\n+++\n\nProperties let a class keep a simple attribute-style API while running code behind the scenes. Callers write `box.area`, but the class can compute the value from current state.\n\nA property setter can validate assignment without changing the public spelling of the attribute. This is the boundary: plain attributes are enough for plain data, while properties are for computed or protected data.\n\nUse properties for cheap, attribute-like operations. Expensive work or actions with side effects should usually remain explicit methods.\n\n:::program\n```python\nclass Rectangle:\n def __init__(self, width, height):\n self.width = width\n self.height = height\n\n @property\n def area(self):\n return self.width * self.height\n\n @property\n def width(self):\n return self._width\n\n @width.setter\n def width(self, value):\n if value <= 0:\n raise ValueError("width must be positive")\n self._width = value\n\nbox = Rectangle(3, 4)\nprint(box.area)\n\nbox.width = 5\nprint(box.area)\n\ntry:\n box.width = 0\nexcept ValueError as error:\n print(error)\n```\n:::\n\n:::cell\nA read-only property exposes computed data through attribute access. `area` stays current because it is calculated from `width` and `height` each time it is read.\n\n```python\nclass Rectangle:\n def __init__(self, width, height):\n self.width = width\n self.height = height\n\n @property\n def area(self):\n return self.width * self.height\n\n @property\n def width(self):\n return self._width\n\n @width.setter\n def width(self, value):\n if value <= 0:\n raise ValueError("width must be positive")\n self._width = value\n\nbox = Rectangle(3, 4)\nprint(box.area)\n```\n\n```output\n12\n```\n:::\n\n:::cell\nA setter lets assignment keep normal attribute syntax while the class validates or normalizes the value.\n\n```python\nbox.width = 5\nprint(box.area)\n```\n\n```output\n20\n```\n:::\n\n:::cell\nValidation belongs inside the class when every caller should obey the same rule. Invalid assignment raises an exception at the boundary.\n\n```python\ntry:\n box.width = 0\nexcept ValueError as error:\n print(error)\n```\n\n```output\nwidth must be positive\n```\n:::\n\n:::note\n- Properties let APIs start simple and grow validation or computation later.\n- Callers access a property like an attribute, not like a method.\n- Use methods instead when work is expensive or action-like.\n:::\n', 'protocols.md': '+++\nslug = "protocols"\ntitle = "Protocols"\nsection = "Types"\nsummary = "Protocol describes required behavior for structural typing."\ndoc_path = "/library/typing.html#typing.Protocol"\nsee_also = [\n "type-hints",\n "classes",\n "inheritance-and-super",\n "abstract-base-classes",\n]\n+++\n\n`Protocol` describes the methods or attributes an object must provide. It exists for structural typing: if an object has the right shape, type checkers can treat it as compatible.\n\nThis is different from inheritance. Inheritance says a class is explicitly derived from a parent; a protocol says callers only need a particular behavior.\n\nAt runtime, ordinary method lookup still applies. Protocols are mainly for static analysis, documentation, and API boundaries.\n\n:::program\n```python\nfrom typing import Protocol\n\nclass Greeter(Protocol):\n def greet(self) -> str:\n ...\n\nclass Person:\n def __init__(self, name):\n self.name = name\n\n def greet(self):\n return f"hello {self.name}"\n\n\ndef welcome(greeter: Greeter):\n print(greeter.greet())\n\nwelcome(Person("Ada"))\nprint(Greeter.__name__)\n```\n:::\n\n:::cell\nA protocol names required behavior. The ellipsis marks the method body as intentionally unspecified, similar to an interface declaration.\n\n```python\nfrom typing import Protocol\n\nclass Greeter(Protocol):\n def greet(self) -> str:\n ...\n\nprint(Greeter.__name__)\n```\n\n```output\nGreeter\n```\n:::\n\n:::cell\nA class can satisfy the protocol without inheriting from it. `Person` has a compatible `greet()` method, so it has the right shape for static type checkers.\n\n```python\nclass Person:\n def __init__(self, name):\n self.name = name\n\n def greet(self):\n return f"hello {self.name}"\n\nprint(Person("Ada").greet())\n```\n\n```output\nhello Ada\n```\n:::\n\n:::cell\nUse the protocol as an annotation at the API boundary. The function only cares that the object can greet; it does not care about the concrete class.\n\n```python\ndef welcome(greeter: Greeter):\n print(greeter.greet())\n\nwelcome(Person("Ada"))\n```\n\n```output\nhello Ada\n```\n:::\n\n:::note\n- Protocols are for structural typing: compatibility by shape rather than explicit inheritance.\n- Type checkers understand protocols; normal runtime method calls still do the work.\n- Prefer inheritance when shared implementation matters, and protocols when only required behavior matters.\n:::\n', 'recursion.md': '+++\nslug = "recursion"\ntitle = "Recursion"\nsection = "Functions"\nsummary = "Recursive functions solve nested problems by calling themselves on smaller pieces."\ndoc_path = "/tutorial/controlflow.html#defining-functions"\n+++\n\nA recursive function calls itself to solve a smaller piece of the same problem. Recursion exists for data that is naturally nested: trees, menus, expression nodes, and directory-like structures.\n\nEvery recursive function needs a base case that can be answered directly. The recursive case must move toward that base case by passing a smaller part of the data.\n\nPrefer loops for simple repetition over a flat sequence. Prefer recursion when the data shape is recursive too.\n\n:::program\n```python\ntree = {\n "value": 1,\n "children": [\n {"value": 2, "children": []},\n {"value": 3, "children": [{"value": 4, "children": []}]},\n ],\n}\n\ndef total(node):\n subtotal = node["value"]\n for child in node["children"]:\n subtotal += total(child)\n return subtotal\n\nprint(total({"value": 2, "children": []}))\nprint(total(tree))\n```\n:::\n\n:::cell\nA leaf node is the base case. It has no children, so the function can return its own value without making another recursive call.\n\n```python\ndef total(node):\n subtotal = node["value"]\n for child in node["children"]:\n subtotal += total(child)\n return subtotal\n\nprint(total({"value": 2, "children": []}))\n```\n\n```output\n2\n```\n:::\n\n:::cell\nA non-leaf node solves the same problem for each child, then combines those smaller totals with its own value.\n\n```python\ntree = {\n "value": 1,\n "children": [\n {"value": 2, "children": []},\n {"value": 3, "children": [{"value": 4, "children": []}]},\n ],\n}\n\nprint(total(tree))\n```\n\n```output\n10\n```\n:::\n\n:::note\n- Every recursive function needs a base case that stops the calls.\n- Recursion fits nested data better than flat repetition.\n- Python limits recursion depth, so loops are often better for very deep or simple repetition.\n:::\n', 'regular-expressions.md': '+++\nslug = "regular-expressions"\ntitle = "Regular Expressions"\nsection = "Text"\nsummary = "The re module searches and extracts text using regular expressions."\ndoc_path = "/library/re.html"\nsee_also = [\n "strings",\n "string-formatting",\n]\n+++\n\nRegular expressions are a compact language for searching and extracting text patterns. Python\'s `re` module provides the standard interface: `re.match` anchors at the start of the string, `re.search` finds the first occurrence anywhere, `re.findall` collects every match, `re.sub` rewrites matches, and `re.compile` reuses a pattern.\n\nUse regex when the pattern has structure: repeated records, alternatives, optional parts, or pieces you want to capture. Prefer ordinary string methods for simple substring checks because simpler code is easier to maintain.\n\nFlags such as `re.IGNORECASE` adjust matching behavior without rewriting the pattern. Pair them with `re.compile` when the same pattern is used repeatedly.\n\n:::program\n```python\nimport re\n\ntext = "Ada: 10, Grace: 9"\npattern = r"([A-Za-z]+): (\\d+)"\n\nfor name, score in re.findall(pattern, text):\n print(name, int(score))\n\nmatch = re.search(r"Grace: (\\d+)", text)\nprint(match.group(1))\nprint("Grace" in text)\n\nstart = re.match(r"Ada", text)\nprint(start is not None)\nprint(re.match(r"Grace", text))\n\nscoreline = re.compile(pattern)\nprint(scoreline.findall(text))\n\ncasey = "ADA: 11"\nprint(re.search(r"ada", casey, flags=re.IGNORECASE).group(0))\n\nprint(re.sub(r"\\d+", "?", text))\n```\n:::\n\n:::cell\nRaw strings keep backslashes readable in regex patterns. Capturing groups return just the pieces inside parentheses.\n\n```python\nimport re\n\ntext = "Ada: 10, Grace: 9"\npattern = r"([A-Za-z]+): (\\d+)"\n\nfor name, score in re.findall(pattern, text):\n print(name, int(score))\n```\n\n```output\nAda 10\nGrace 9\n```\n:::\n\n:::cell\n`re.search()` finds the first match. A match object exposes captured groups by position.\n\n```python\nmatch = re.search(r"Grace: (\\d+)", text)\nprint(match.group(1))\n```\n\n```output\n9\n```\n:::\n\n:::cell\nFor a simple substring check, ordinary string membership is clearer than regex.\n\n```python\nprint("Grace" in text)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\n`re.match` only matches at the start of the string; `re.search` finds the first match anywhere. Picking the right one keeps anchoring intent visible without an explicit `^`.\n\n```python\nstart = re.match(r"Ada", text)\nprint(start is not None)\nprint(re.match(r"Grace", text))\n```\n\n```output\nTrue\nNone\n```\n:::\n\n:::cell\n`re.compile` produces a reusable pattern object whose methods skip the parser on each call. Reach for it when the same pattern runs in a loop.\n\n```python\nscoreline = re.compile(pattern)\nprint(scoreline.findall(text))\n```\n\n```output\n[(\'Ada\', \'10\'), (\'Grace\', \'9\')]\n```\n:::\n\n:::cell\nFlags such as `re.IGNORECASE` adjust matching without changing the pattern. `re.sub` replaces every match with a replacement string and returns the rewritten text.\n\n```python\ncasey = "ADA: 11"\nprint(re.search(r"ada", casey, flags=re.IGNORECASE).group(0))\n\nprint(re.sub(r"\\d+", "?", text))\n```\n\n```output\nADA\nAda: ?, Grace: ?\n```\n:::\n\n:::note\n- Use raw strings for regex patterns so backslashes are easier to read.\n- Use capturing groups when the point is extraction, not just matching.\n- `re.match` anchors at the start; `re.search` finds the first match anywhere.\n- `re.compile` saves work when the pattern runs more than once.\n- `re.sub` rewrites matches; flags like `re.IGNORECASE` change matching behavior without rewriting the pattern.\n- Reach for string methods before regex when the pattern is simple.\n:::\n', 'runtime-type-checks.md': '+++\nslug = "runtime-type-checks"\ntitle = "Runtime Type Checks"\nsection = "Types"\nsummary = "type, isinstance, and issubclass inspect runtime relationships."\ndoc_path = "/library/functions.html#isinstance"\nsee_also = [\n "type-hints",\n "protocols",\n "casts-and-any",\n "abstract-base-classes",\n]\n+++\n\nRuntime type checks inspect real objects while the program is running. They are different from type hints, which mostly guide tools before the program runs.\n\nUse `type()` when the exact class matters, `isinstance()` when subclasses should count, and `issubclass()` when checking class relationships. Most APIs prefer behavior over type checks, but runtime checks are useful at input boundaries.\n\nDo not turn every function into a wall of `isinstance()` calls. If the code only needs an object that can perform an operation, duck typing or a protocol may be clearer.\n\n:::program\n```python\nclass Animal:\n pass\n\nclass Dog(Animal):\n pass\n\npet = Dog()\n\nprint(type(pet).__name__)\nprint(type(pet) is Animal)\nprint(isinstance(pet, Animal))\nprint(issubclass(Dog, Animal))\n```\n:::\n\n:::cell\n`type()` reports the exact runtime class. A `Dog` instance is not exactly an `Animal` instance.\n\n```python\nclass Animal:\n pass\n\nclass Dog(Animal):\n pass\n\npet = Dog()\nprint(type(pet).__name__)\nprint(type(pet) is Animal)\n```\n\n```output\nDog\nFalse\n```\n:::\n\n:::cell\n`isinstance()` accepts subclasses, which is usually what API boundaries want.\n\n```python\nprint(isinstance(pet, Dog))\nprint(isinstance(pet, Animal))\n```\n\n```output\nTrue\nTrue\n```\n:::\n\n:::cell\n`issubclass()` checks class relationships rather than individual objects.\n\n```python\nprint(issubclass(Dog, Animal))\n```\n\n```output\nTrue\n```\n:::\n\n:::note\n- `type()` is exact; `isinstance()` follows inheritance.\n- Runtime checks inspect objects, not static annotations.\n- Prefer behavior, protocols, or clear validation over scattered type checks.\n:::\n', 'scope-global-nonlocal.md': '+++\nslug = "scope-global-nonlocal"\ntitle = "Global and Nonlocal"\nsection = "Functions"\nsummary = "global and nonlocal choose which outer binding assignment should update."\ndoc_path = "/reference/simple_stmts.html#the-global-statement"\nsee_also = [\n "variables",\n "closures",\n "functions",\n]\n+++\n\nAssignment normally creates or updates a local name inside the current function. `global` and `nonlocal` are explicit escape hatches for rebinding names outside that local scope.\n\nUse `nonlocal` when an inner function should update a name in an enclosing function. Use `global` rarely; passing values and returning results is usually clearer.\n\nThese statements affect name binding, not object mutation. Mutating a shared list is different from rebinding the name itself.\n\n:::program\n```python\ncount = 0\n\ndef bump_global():\n global count\n count += 1\n\nbump_global()\nprint(count)\n\n\ndef make_counter():\n total = 0\n def bump():\n nonlocal total\n total += 1\n return total\n return bump\n\ncounter = make_counter()\nprint(counter())\nprint(counter())\n```\n:::\n\n:::cell\n`global` tells assignment to update a module-level binding. Without it, `count += 1` would try to assign a local `count`.\n\n```python\ncount = 0\n\ndef bump_global():\n global count\n count += 1\n\nbump_global()\nprint(count)\n```\n\n```output\n1\n```\n:::\n\n:::cell\n`nonlocal` tells assignment to update a binding in the nearest enclosing function scope. This is useful for small closures that keep state.\n\n```python\ndef make_counter():\n total = 0\n def bump():\n nonlocal total\n total += 1\n return total\n return bump\n\ncounter = make_counter()\nprint(counter())\nprint(counter())\n```\n\n```output\n1\n2\n```\n:::\n\n:::note\n- Assignment inside a function is local unless declared otherwise.\n- Prefer `nonlocal` for closure state and avoid `global` unless module state is truly intended.\n- Passing values and returning results is usually easier to test than rebinding outer names.\n:::\n', 'sentinel-iteration.md': '+++\nslug = "sentinel-iteration"\ntitle = "Sentinel Iteration"\nsection = "Iteration"\nsummary = "iter(callable, sentinel) repeats calls until a marker value appears."\ndoc_path = "/library/functions.html#iter"\n+++\n\n`iter(callable, sentinel)` keeps calling a zero-argument callable and yields each result until the callable returns the sentinel value. It is the right shape for repeated reads from files, sockets, or queues — sources where each call produces the next chunk and a known marker means "no more".\n\nReach for it instead of writing `while True:` plus a manual break when the loop body would do nothing else but read and check. The two-argument form turns a polling callable into something that composes with `for` loops, comprehensions, and other iterator helpers.\n\nThe callable must take no arguments. Wrap a parameterized reader in a small lambda or method that closes over the parameters when the underlying API needs them.\n\n:::program\n```python\nlines = iter(["alpha", "beta", ""])\n\ndef read_line():\n return next(lines)\n\nfor line in iter(read_line, ""):\n print(line.upper())\n```\n:::\n\n:::cell\nA zero-argument callable produces one value at a time.\n\n```python\nlines = iter(["alpha", "beta", ""])\n\ndef read_line():\n return next(lines)\n\nfor line in iter(read_line, ""):\n print(line.upper())\n```\n\n```output\nALPHA\nBETA\n```\n:::\n\n:::note\n- A zero-argument callable produces one value at a time.\n- The sentinel value stops the loop without appearing in the output.\n- This form is useful for repeated reads from files, sockets, or queues.\n:::\n', 'sets.md': '+++\nslug = "sets"\ntitle = "Sets"\nsection = "Collections"\nsummary = "Sets store unique values and make membership checks explicit."\ndoc_path = "/tutorial/datastructures.html#sets"\n+++\n\nSets store unique hashable values. Use them when membership and de-duplication matter more than order.\n\nA list can answer membership with `in`, but a set communicates that membership is the main operation. Set algebra then expresses how groups relate to each other.\n\nBecause sets are unordered, examples often wrap output in `sorted()` so the display is deterministic.\n\n:::program\n```python\nlanguages = ["python", "go", "python"]\nunique_languages = set(languages)\nprint(sorted(unique_languages))\n\nallowed = {"python", "rust"}\nprint("python" in allowed)\nprint("ruby" in allowed)\n\ncompiled = {"go", "rust"}\nprint(sorted(allowed | compiled))\nprint(sorted(allowed & compiled))\nprint(sorted(allowed - compiled))\n```\n:::\n\n:::cell\nCreating a set removes duplicates. Keep a list when order and repeated values matter; convert to a set when uniqueness is the point.\n\n```python\nlanguages = ["python", "go", "python"]\nunique_languages = set(languages)\nprint(sorted(unique_languages))\n```\n\n```output\n[\'go\', \'python\']\n```\n:::\n\n:::cell\nMembership checks are the everyday set operation. A list can also use `in`, but a set says that membership is central to the data shape.\n\n```python\nallowed = {"python", "rust"}\nprint("python" in allowed)\nprint("ruby" in allowed)\n```\n\n```output\nTrue\nFalse\n```\n:::\n\n:::cell\nUnion, intersection, and difference describe relationships between groups without manual loops.\n\n```python\ncompiled = {"go", "rust"}\nprint(sorted(allowed | compiled))\nprint(sorted(allowed & compiled))\nprint(sorted(allowed - compiled))\n```\n\n```output\n[\'go\', \'python\', \'rust\']\n[\'rust\']\n[\'python\']\n```\n:::\n\n:::note\n- Use lists when order and repeated values matter.\n- Use sets when uniqueness and membership are the main operations.\n- Prefer lists when order or repeated values are part of the meaning.\n- Sets are unordered, so sort them when examples need deterministic display.\n:::\n', 'slices.md': '+++\nslug = "slices"\ntitle = "Slices"\nsection = "Collections"\nsummary = "Slices copy meaningful ranges from ordered sequences."\ndoc_path = "/tutorial/introduction.html#lists"\n+++\n\nSlicing reads a range from an ordered sequence with `start:stop:step`. It exists because Python code often needs a meaningful piece of a sequence: a page, a prefix, a tail, a stride, or a reversed view.\n\nThe stop index is excluded. That convention makes lengths and adjacent ranges line up: `items[:3]` and `items[3:]` split a sequence without overlap.\n\nSlices return new sequence objects for built-in lists and strings. Use indexing for one item; use slicing when the result should still be a sequence.\n\n:::program\n```python\nletters = ["a", "b", "c", "d", "e", "f"]\nfirst_page = letters[:3]\nrest = letters[3:]\nprint(first_page)\nprint(rest)\n\nmiddle = letters[1:5]\nevery_other = letters[::2]\nreversed_letters = letters[::-1]\nprint(middle)\nprint(every_other)\nprint(reversed_letters)\nprint(letters)\n```\n:::\n\n:::cell\nOmitted bounds mean “from the beginning” or “through the end.” Because the stop index is excluded, adjacent slices split a sequence cleanly.\n\n```python\nletters = ["a", "b", "c", "d", "e", "f"]\nfirst_page = letters[:3]\nrest = letters[3:]\nprint(first_page)\nprint(rest)\n```\n\n```output\n[\'a\', \'b\', \'c\']\n[\'d\', \'e\', \'f\']\n```\n:::\n\n:::cell\nUse `start:stop` for a middle range and `step` when you want to skip or walk backward. These operations return new lists; the original list is unchanged.\n\n```python\nmiddle = letters[1:5]\nevery_other = letters[::2]\nreversed_letters = letters[::-1]\nprint(middle)\nprint(every_other)\nprint(reversed_letters)\nprint(letters)\n```\n\n```output\n[\'b\', \'c\', \'d\', \'e\']\n[\'a\', \'c\', \'e\']\n[\'f\', \'e\', \'d\', \'c\', \'b\', \'a\']\n[\'a\', \'b\', \'c\', \'d\', \'e\', \'f\']\n```\n:::\n\n:::note\n- Slice stop indexes are excluded, so adjacent ranges compose cleanly.\n- Omitted bounds mean the beginning or end of the sequence.\n- A negative step walks backward; `[::-1]` is a common reversed-copy idiom.\n:::\n', 'sorting.md': '+++\nslug = "sorting"\ntitle = "Sorting"\nsection = "Collections"\nsummary = "sorted returns a new ordered list and key functions choose the sort value."\ndoc_path = "/howto/sorting.html"\n+++\n\n`sorted()` accepts any iterable and returns a new list. The original collection is left untouched, which makes `sorted()` useful in expressions and pipelines.\n\nUse `key=` to say what value should be compared for each item. This is the idiomatic way to sort records, tuples, dictionaries, and objects by a field.\n\nUse `reverse=True` for descending order. Use `list.sort()` instead when you intentionally want to mutate an existing list in place.\n\n:::program\n```python\nnames = ["Guido", "Ada", "Grace"]\nprint(sorted(names))\nprint(names)\n\nusers = [\n {"name": "Ada", "score": 10},\n {"name": "Guido", "score": 8},\n {"name": "Grace", "score": 10},\n]\nranked = sorted(users, key=lambda user: user["score"], reverse=True)\nprint([user["name"] for user in ranked])\n\nusers.sort(key=lambda user: user["name"])\nprint([user["name"] for user in users])\n```\n:::\n\n:::cell\n`sorted()` returns a new list. Printing the original list afterward shows that the input order did not change.\n\n```python\nnames = ["Guido", "Ada", "Grace"]\nprint(sorted(names))\nprint(names)\n```\n\n```output\n[\'Ada\', \'Grace\', \'Guido\']\n[\'Guido\', \'Ada\', \'Grace\']\n```\n:::\n\n:::cell\nA key function computes the value to compare. Here the records are sorted by score, highest first, and the output shows the resulting order.\n\n```python\nusers = [\n {"name": "Ada", "score": 10},\n {"name": "Guido", "score": 8},\n {"name": "Grace", "score": 10},\n]\nranked = sorted(users, key=lambda user: user["score"], reverse=True)\nprint([user["name"] for user in ranked])\n```\n\n```output\n[\'Ada\', \'Grace\', \'Guido\']\n```\n:::\n\n:::cell\n`list.sort()` sorts the list in place. Use it when mutation is the point and no separate sorted copy is needed.\n\n```python\nusers.sort(key=lambda user: user["name"])\nprint([user["name"] for user in users])\n```\n\n```output\n[\'Ada\', \'Grace\', \'Guido\']\n```\n:::\n\n:::note\n- `sorted()` makes a new list; `list.sort()` mutates an existing list.\n- `key=` should return the value Python compares for each item.\n- Python\'s sort is stable, so equal keys keep their original relative order.\n:::\n', 'special-methods.md': '+++\nslug = "special-methods"\ntitle = "Special Methods"\nsection = "Data Model"\nsummary = "Special methods connect your objects to Python syntax and built-ins."\ndoc_path = "/reference/datamodel.html#special-method-names"\nsee_also = [\n "container-protocols",\n "operator-overloading",\n "callable-objects",\n "context-managers",\n]\n+++\n\nSpecial methods, often called dunder methods, connect user-defined classes to Python syntax and built-ins such as len(), iter(), and repr().\n\nImplementing these methods lets your objects participate in Python protocols rather than forcing callers to learn custom method names for common operations.\n\nGood special methods make objects feel boring in the best way: they work with the language features Python programmers already know.\n\n:::program\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __len__(self):\n return len(self.items)\n\n def __iter__(self):\n return iter(self.items)\n\n def __repr__(self):\n return f"Bag({self.items!r})"\n\n def __str__(self):\n return ", ".join(self.items)\n\n def __eq__(self, other):\n return isinstance(other, Bag) and self.items == other.items\n\n def __hash__(self):\n return hash(tuple(self.items))\n\n def __lt__(self, other):\n return len(self.items) < len(other.items)\n\n def __contains__(self, item):\n return item in self.items\n\n def __getitem__(self, index):\n return self.items[index]\n\n def __setitem__(self, index, value):\n self.items[index] = value\n\n def __bool__(self):\n return bool(self.items)\n\nbag = Bag(["a", "b"])\nprint(len(bag))\nprint(list(bag))\nprint(bag)\nprint(repr(bag))\nprint(Bag(["a", "b"]) == Bag(["a", "b"]))\nprint(Bag(["a"]) < Bag(["a", "b"]))\nprint(hash(Bag(["a"])) == hash(Bag(["a"])))\nprint("a" in bag)\nprint(bag[0])\nbag[1] = "z"\nprint(list(bag))\nprint(bool(Bag([])))\n\n\nclass Multiplier:\n def __init__(self, factor):\n self.factor = factor\n\n def __call__(self, value):\n return value * self.factor\n\ntriple = Multiplier(3)\nprint(triple(5))\n\n\nclass Trace:\n def __enter__(self):\n print("enter")\n return self\n\n def __exit__(self, *exc):\n print("exit")\n return False\n\nwith Trace():\n print("inside")\n```\n:::\n\n:::cell\nStart with a normal class that stores its data. Special methods build on ordinary instance state.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\nbag = Bag(["a", "b"])\nprint(bag.items)\n```\n\n```output\n[\'a\', \'b\']\n```\n:::\n\n:::cell\nImplement `__len__` to let `len()` ask the object for its size using Python\'s standard protocol.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __len__(self):\n return len(self.items)\n\nbag = Bag(["a", "b"])\nprint(len(bag))\n```\n\n```output\n2\n```\n:::\n\n:::cell\nImplement `__iter__` to make the object iterable. Then tools such as `list()` can consume it without a custom method name.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __len__(self):\n return len(self.items)\n\n def __iter__(self):\n return iter(self.items)\n\nbag = Bag(["a", "b"])\nprint(list(bag))\n```\n\n```output\n[\'a\', \'b\']\n```\n:::\n\n:::cell\nImplement `__repr__` to give the object a useful developer-facing representation when it is printed or inspected. With no `__str__` defined, `print()` falls back to `__repr__`.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __len__(self):\n return len(self.items)\n\n def __iter__(self):\n return iter(self.items)\n\n def __repr__(self):\n return f"Bag({self.items!r})"\n\nbag = Bag(["a", "b"])\nprint(bag)\n```\n\n```output\nBag([\'a\', \'b\'])\n```\n:::\n\n:::cell\nAdd `__str__` for an end-user representation. `print()` and `str()` prefer `__str__`; `repr()` and the REPL still use `__repr__`. Keep `__repr__` unambiguous for debugging and let `__str__` be the friendly form.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __repr__(self):\n return f"Bag({self.items!r})"\n\n def __str__(self):\n return ", ".join(self.items)\n\nbag = Bag(["a", "b"])\nprint(bag)\nprint(repr(bag))\n```\n\n```output\na, b\nBag([\'a\', \'b\'])\n```\n:::\n\n:::cell\n`__eq__` decides what equality means for the type. Defining `__eq__` removes the default `__hash__`, so add `__hash__` back when instances should work in sets or as dict keys. `__lt__` enables `<` and, with the rest of the order family, `sorted()`.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __eq__(self, other):\n return isinstance(other, Bag) and self.items == other.items\n\n def __hash__(self):\n return hash(tuple(self.items))\n\n def __lt__(self, other):\n return len(self.items) < len(other.items)\n\nprint(Bag(["a", "b"]) == Bag(["a", "b"]))\nprint(Bag(["a"]) < Bag(["a", "b"]))\nprint(hash(Bag(["a"])) == hash(Bag(["a"])))\n```\n\n```output\nTrue\nTrue\nTrue\n```\n:::\n\n:::cell\nThe container protocols make instances behave like built-in containers. `__contains__` powers `in`, `__getitem__`/`__setitem__` power subscription, and `__bool__` decides truthiness for `if` and `while`. See [container-protocols](/data-model/container-protocols) for the full surface.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __contains__(self, item):\n return item in self.items\n\n def __getitem__(self, index):\n return self.items[index]\n\n def __setitem__(self, index, value):\n self.items[index] = value\n\n def __bool__(self):\n return bool(self.items)\n\nbag = Bag(["a", "b"])\nprint("a" in bag)\nprint(bag[0])\nbag[1] = "z"\nprint(bag.items)\nprint(bool(Bag([])))\n```\n\n```output\nTrue\na\n[\'a\', \'z\']\nFalse\n```\n:::\n\n:::cell\n`__call__` makes an instance callable like a function — useful for stateful operations whose configuration deserves a name. `__enter__` and `__exit__` make a class a context manager so it can be used with `with`. The focused [callable-objects](/data-model/callable-objects) and [context-managers](/data-model/context-managers) pages go deeper.\n\n```python\nclass Multiplier:\n def __init__(self, factor):\n self.factor = factor\n\n def __call__(self, value):\n return value * self.factor\n\ntriple = Multiplier(3)\nprint(triple(5))\n\n\nclass Trace:\n def __enter__(self):\n print("enter")\n return self\n\n def __exit__(self, *exc):\n print("exit")\n return False\n\nwith Trace():\n print("inside")\n```\n\n```output\n15\nenter\ninside\nexit\n```\n:::\n\n:::note\n- Dunder methods are looked up by Python\'s data model protocols.\n- `__repr__` is the developer-facing form; `__str__` is the user-facing form. `print()` falls back to `__repr__` when `__str__` is missing.\n- Defining `__eq__` removes the default `__hash__`; restore it when the type should be hashable.\n- Container protocols (`__contains__`, `__getitem__`, `__setitem__`, `__bool__`) make instances behave like built-in containers.\n- `__call__` makes instances callable; `__enter__`/`__exit__` make them context managers.\n- Implement the smallest protocol that makes your object feel native.\n:::\n', 'string-formatting.md': '+++\nslug = "string-formatting"\ntitle = "String Formatting"\nsection = "Text"\nsummary = "f-strings turn values into readable text at the point of use."\ndoc_path = "/tutorial/inputoutput.html#formatted-string-literals"\n+++\n\nFormatted string literals, or f-strings, exist because programs constantly need to turn values into human-readable text. They keep the expression next to the words it explains.\n\nFormat specifications after `:` control presentation details such as width, alignment, padding, and precision. This separates the value being computed from the way it should be displayed.\n\nUse f-strings for most new formatting code. They relate directly to expressions: anything inside braces is evaluated, then formatted into the surrounding string.\n\n:::program\n```python\nname = "Ada"\nscore = 9.5\nrank = 1\n\nmessage = f"{name} scored {score}"\nprint(message)\n\nrow = f"{rank:>2} | {name:<8} | {score:05.1f}"\nprint(row)\n\nprint(f"{score = }")\n```\n:::\n\n:::cell\nAn f-string evaluates expressions inside braces and inserts their string form into the surrounding text. This is clearer than joining several converted values by hand.\n\n```python\nname = "Ada"\nscore = 9.5\nrank = 1\n\nmessage = f"{name} scored {score}"\nprint(message)\n```\n\n```output\nAda scored 9.5\n```\n:::\n\n:::cell\nFormat specifications after `:` control display without changing the underlying values. Here the rank is right-aligned, the name is left-aligned, and the score is padded to one decimal place.\n\n```python\nrow = f"{rank:>2} | {name:<8} | {score:05.1f}"\nprint(row)\n```\n\n```output\n 1 | Ada | 009.5\n```\n:::\n\n:::cell\nThe debug form with `=` is useful while learning or logging because it prints both the expression and the value.\n\n```python\nprint(f"{score = }")\n```\n\n```output\nscore = 9.5\n```\n:::\n\n:::note\n- Use `f"..."` strings for most new formatting code.\n- Expressions inside braces are evaluated before formatting.\n- Format specifications after `:` control alignment, width, padding, and precision.\n:::\n', 'strings.md': '+++\nslug = "strings"\ntitle = "Strings"\nsection = "Text"\nsummary = "Strings are immutable Unicode text sequences."\ndoc_path = "/library/stdtypes.html#text-sequence-type-str"\n+++\n\nPython strings are immutable Unicode text sequences. A `str` stores text as Unicode code points, so it can represent English, Thai, accented letters, emoji, and ordinary ASCII with the same type.\n\nUnicode matters because text length and byte length are different questions. The English word `"hello"` uses five code points and five UTF-8 bytes because ASCII characters encode as one byte each. The Thai greeting `"สวัสดี"` has six code points but needs eighteen UTF-8 bytes.\n\nUse `str` when you mean text, and encode to `bytes` only at boundaries such as files, network protocols, and binary APIs. String operations such as `upper()` and `strip()` return new strings instead of changing the original.\n\n:::program\n```python\nenglish = "hello"\nthai = "สวัสดี"\n\nfor label, word in [("English", english), ("Thai", thai)]:\n print(label, word, len(word), len(word.encode("utf-8")))\n\nprint(thai[0])\nprint([hex(ord(char)) for char in thai[:2]])\n\ntext = " café "\nclean = text.strip()\nprint(clean)\nprint(clean.upper())\nprint(clean.encode("utf-8"))\n```\n:::\n\n:::cell\nCompare an English greeting with a Thai greeting. Both are Python `str` values, but UTF-8 uses one byte for each ASCII code point and multiple bytes for many non-ASCII code points.\n\n```python\nenglish = "hello"\nthai = "สวัสดี"\n\nfor label, word in [("English", english), ("Thai", thai)]:\n print(label, word, len(word), len(word.encode("utf-8")))\n```\n\n```output\nEnglish hello 5 5\nThai สวัสดี 6 18\n```\n:::\n\n:::cell\nIndexing and iteration work with Unicode code points, not encoded bytes. `ord()` returns the integer code point, which is often displayed in hexadecimal when teaching text encoding.\n\n```python\nprint(thai[0])\nprint([hex(ord(char)) for char in thai[:2]])\n```\n\n```output\nส\n[\'0xe2a\', \'0xe27\']\n```\n:::\n\n:::cell\nString methods return new strings because strings are immutable. Encoding turns text into bytes when another system needs a byte representation.\n\n```python\ntext = " café "\nclean = text.strip()\nprint(clean)\nprint(clean.upper())\nprint(clean.encode("utf-8"))\n```\n\n```output\ncafé\nCAFÉ\nb\'caf\\xc3\\xa9\'\n```\n:::\n\n:::note\n- Use `str` for text and `bytes` for binary data.\n- `len(text)` counts Unicode code points; `len(text.encode("utf-8"))` counts encoded bytes.\n- ASCII text is a useful baseline because each ASCII code point is one UTF-8 byte.\n- String methods return new strings because strings are immutable.\n- User-visible “characters” can be more subtle than code points; combining marks and emoji sequences may need specialized text handling.\n:::\n', 'structured-data-shapes.md': '+++\nslug = "structured-data-shapes"\ntitle = "Structured Data Shapes"\nsection = "Classes"\nsummary = "dataclass, NamedTuple, and TypedDict each model records with different trade-offs."\ndoc_path = "/library/dataclasses.html"\nsee_also = [\n "dataclasses",\n "typed-dicts",\n "tuples",\n "classes",\n]\n+++\n\n`@dataclass`, `typing.NamedTuple`, and `typing.TypedDict` are three ways to give a record a name and a schema. They model the same data but differ in mutability, access syntax, and what the type information costs at runtime.\n\nA dataclass is a regular class with `__init__` and `__repr__` generated for you, so instances are mutable and attribute-accessed. A `NamedTuple` is a tuple subclass with named positions, so instances are immutable and support both `obj.field` and `obj[index]`. A `TypedDict` is a plain dict at runtime; the schema lives only in the type checker.\n\nPick the shape that matches the problem: a dataclass when methods or mutability help; a `NamedTuple` for small immutable records that benefit from unpacking; a `TypedDict` for JSON-shaped data that should stay as a dict at the boundary.\n\n:::program\n```python\nfrom dataclasses import dataclass\nfrom typing import NamedTuple, TypedDict\n\n@dataclass\nclass UserClass:\n name: str\n score: int\n\nclass UserTuple(NamedTuple):\n name: str\n score: int\n\nclass UserDict(TypedDict):\n name: str\n score: int\n\na = UserClass("Ada", 98)\nprint(a)\na.score = 100\nprint(a.score)\n\nb = UserTuple("Ada", 98)\nprint(b)\nprint(b.name, b[1])\nprint(b._replace(score=100))\n\nc: UserDict = {"name": "Ada", "score": 98}\nprint(c)\nprint(c["name"])\nprint(type(c).__name__)\n\nprint(isinstance(a, UserClass))\nprint(isinstance(b, tuple))\nprint(isinstance(c, dict))\n```\n:::\n\n:::cell\nA dataclass is a normal class with `__init__` and `__repr__` generated from the annotated fields. Instances are mutable, support attribute access, and can carry methods like any other class.\n\n```python\nfrom dataclasses import dataclass\n\n@dataclass\nclass UserClass:\n name: str\n score: int\n\na = UserClass("Ada", 98)\nprint(a)\na.score = 100\nprint(a.score)\n```\n\n```output\nUserClass(name=\'Ada\', score=98)\n100\n```\n:::\n\n:::cell\nA `NamedTuple` is a tuple subclass with named positions. Instances are immutable, support both `obj.field` and `obj[index]`, and the helper `_replace` produces a modified copy without mutating the original (since assigning to a field would fail).\n\n```python\nfrom typing import NamedTuple\n\nclass UserTuple(NamedTuple):\n name: str\n score: int\n\nb = UserTuple("Ada", 98)\nprint(b)\nprint(b.name, b[1])\nprint(b._replace(score=100))\n```\n\n```output\nUserTuple(name=\'Ada\', score=98)\nAda 98\nUserTuple(name=\'Ada\', score=100)\n```\n:::\n\n:::cell\nA `TypedDict` is a plain dictionary at runtime. The annotations exist only for the type checker, so the value behaves like any `dict` — useful for JSON-shaped data that crosses an API boundary as a mapping.\n\n```python\nfrom typing import TypedDict\n\nclass UserDict(TypedDict):\n name: str\n score: int\n\nc: UserDict = {"name": "Ada", "score": 98}\nprint(c)\nprint(c["name"])\nprint(type(c).__name__)\n```\n\n```output\n{\'name\': \'Ada\', \'score\': 98}\nAda\ndict\n```\n:::\n\n:::cell\nSame record, three runtime identities. The dataclass is its own class. The `NamedTuple` is literally a tuple. The `TypedDict` is literally a dict. That difference drives the choice: pick the form whose runtime behavior matches what the rest of the program already expects.\n\n```python\nprint(isinstance(a, UserClass))\nprint(isinstance(b, tuple))\nprint(isinstance(c, dict))\n```\n\n```output\nTrue\nTrue\nTrue\n```\n:::\n\n:::note\n- `@dataclass` — mutable, attribute access, methods; good default when behavior travels with data.\n- `typing.NamedTuple` — immutable, attribute + index access, tuple semantics; good for small records that flow through unpacking.\n- `typing.TypedDict` — runtime is `dict`, schema is type-checker-only; good for JSON-shaped data.\n- `collections.namedtuple` is the older, untyped form of `NamedTuple`; prefer the `typing` version in new code.\n:::\n', 'subprocesses.md': '+++\nslug = "subprocesses"\ntitle = "Subprocesses"\nsection = "Standard Library"\nsummary = "subprocess runs external commands with explicit arguments and captured outputs."\ndoc_path = "/library/subprocess.html"\nexpected_output = "child process\\n0\\n"\n+++\n\n`subprocess` is the standard boundary for running external commands. It starts another program, waits for it, and gives you a result object with the exit code and captured output.\n\nUse a list of arguments when possible, capture output when the parent program needs to inspect it, and treat a non-zero return code as a failure. The same ideas apply whether the child program is Python, Git, a compiler, or another command-line tool.\n\nThe important boundary is between Python objects and the operating system process table. Python prepares arguments and environment, then the child program runs independently and reports back through streams and an exit status.\n\n:::program\n```python\nimport subprocess\nimport sys\n\nresult = subprocess.run(\n [sys.executable, "-c", "print(\'child process\')"],\n text=True,\n capture_output=True,\n check=True,\n)\n\nprint(result.stdout.strip())\nprint(result.returncode)\n```\n:::\n\n:::unsupported\nDynamic Workers do not provide child processes.\n\n```python\nresult = subprocess.run(\n [sys.executable, "-c", "print(\'child process\')"],\n text=True,\n capture_output=True,\n check=True,\n)\n```\n:::\n\n:::cell\n`subprocess.run()` starts a child process and waits for it. `capture_output=True` stores the child\'s standard output and error streams on the result object.\n\n```python\nimport subprocess\nimport sys\n\nresult = subprocess.run(\n [sys.executable, "-c", "print(\'child process\')"],\n text=True,\n capture_output=True,\n check=True,\n)\n\nprint(result.stdout.strip())\nprint(result.returncode)\n```\n\n```output\nchild process\n0\n```\n:::\n\n:::note\n- Use a list of arguments instead of shell strings when possible.\n- Capture output when the parent program needs to inspect it.\n- `check=True` turns non-zero exits into exceptions.\n:::\n', 'testing.md': '+++\nslug = "testing"\ntitle = "Testing"\nsection = "Standard Library"\nsummary = "Tests make expected behavior executable and repeatable."\ndoc_path = "/library/unittest.html"\nsee_also = [\n "assertions",\n "exceptions",\n "modules",\n]\n+++\n\nTests turn expected behavior into code that can be run again. The useful unit is usually a small example of behavior with clear input, action, and assertion.\n\nPython\'s `unittest` library provides test cases, assertions, suites, and runners. Projects often use `pytest` for ergonomics, but the same idea remains: a test names behavior and fails when the behavior changes.\n\nA broad testing practice also includes fixtures, integration tests, property tests, and coverage. This example stays on the smallest standard-library loop: define behavior, assert the result, run the suite, inspect success.\n\n:::program\n```python\nimport io\nimport unittest\n\n\ndef add(left, right):\n return left + right\n\n\ndef divide(left, right):\n if right == 0:\n raise ZeroDivisionError("denominator is zero")\n return left / right\n\n\nclass AddTests(unittest.TestCase):\n def setUp(self):\n self.zero = 0\n\n def test_adds_numbers(self):\n self.assertEqual(add(self.zero + 2, 3), 5)\n\n def test_adds_empty_strings(self):\n self.assertEqual(add("", "py"), "py")\n\n def test_divide_by_zero_raises(self):\n with self.assertRaises(ZeroDivisionError):\n divide(1, 0)\n\nsuite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)\nstream = io.StringIO()\nresult = unittest.TextTestRunner(stream=stream, verbosity=0).run(suite)\nprint(result.testsRun)\nprint(result.wasSuccessful())\n```\n:::\n\n:::cell\nA test starts with behavior small enough to name. The function can be ordinary code; the test supplies a representative input and expected result.\n\n```python\ndef add(left, right):\n return left + right\n\nprint(add(2, 3))\n```\n\n```output\n5\n```\n:::\n\n:::cell\n`unittest.TestCase` groups test methods. `setUp` runs before each test method to build per-test fixtures, `assertEqual` checks values, and `assertRaises` asserts that a block raises the expected exception type.\n\n```python\nimport unittest\n\n\ndef divide(left, right):\n if right == 0:\n raise ZeroDivisionError("denominator is zero")\n return left / right\n\n\nclass AddTests(unittest.TestCase):\n def setUp(self):\n self.zero = 0\n\n def test_adds_numbers(self):\n self.assertEqual(add(self.zero + 2, 3), 5)\n\n def test_adds_empty_strings(self):\n self.assertEqual(add("", "py"), "py")\n\n def test_divide_by_zero_raises(self):\n with self.assertRaises(ZeroDivisionError):\n divide(1, 0)\n\nprint([name for name in dir(AddTests) if name.startswith("test_")])\n```\n\n```output\n[\'test_adds_empty_strings\', \'test_adds_numbers\', \'test_divide_by_zero_raises\']\n```\n:::\n\n:::cell\nA runner executes the suite and records whether every assertion passed. Capturing the runner stream keeps this page\'s output deterministic.\n\n```python\nimport io\n\nsuite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)\nstream = io.StringIO()\nresult = unittest.TextTestRunner(stream=stream, verbosity=0).run(suite)\nprint(result.testsRun)\nprint(result.wasSuccessful())\n```\n\n```output\n3\nTrue\n```\n:::\n\n:::note\n- Test method names should describe behavior, not implementation details.\n- A good unit test is deterministic and independent of test order.\n- Use broader integration tests when the behavior depends on several components working together.\n:::\n', 'threads-and-processes.md': '+++\nslug = "threads-and-processes"\ntitle = "Threads and Processes"\nsection = "Standard Library"\nsummary = "Threads share memory, while processes run in separate interpreters."\ndoc_path = "/library/concurrent.futures.html"\nexpected_output = "[1, 4, 9]\\nProcessPoolExecutor\\n"\n+++\n\nThreads and processes are two ways to run work outside the current control path. Threads are useful for overlapping I/O-shaped waits, while processes are useful when CPU-bound work needs separate interpreter processes.\n\nThis is different from `asyncio`: threads and processes run callables through executors, while `async` code cooperatively awaits coroutines. Choose the smallest concurrency model that matches the bottleneck.\n\nThe executor interface lets callers submit ordinary functions without committing the rest of the code to one scheduling strategy. That makes it easier to compare thread and process boundaries at the call site.\n\n:::program\n```python\nfrom concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor\n\n\ndef square(number):\n return number * number\n\nwith ThreadPoolExecutor(max_workers=2) as pool:\n print(list(pool.map(square, [1, 2, 3])))\n\nprint(ProcessPoolExecutor.__name__)\n```\n:::\n\n:::unsupported\nDynamic Workers do not provide native threads or child processes.\n\n```python\nwith ThreadPoolExecutor(max_workers=2) as pool:\n print(list(pool.map(square, [1, 2, 3])))\n\nwith ProcessPoolExecutor(max_workers=2) as pool:\n print(list(pool.map(pow, [4, 5], [2, 2])))\n```\n:::\n\n:::cell\nA thread pool runs ordinary callables while sharing memory with the current process. `map()` returns results in input order.\n\n```python\nfrom concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor\n\n\ndef square(number):\n return number * number\n\nwith ThreadPoolExecutor(max_workers=2) as pool:\n print(list(pool.map(square, [1, 2, 3])))\n```\n\n```output\n[1, 4, 9]\n```\n:::\n\n:::cell\nA process pool uses separate Python processes. That boundary is heavier, but it can run CPU-bound work outside the current interpreter.\n\n```python\nprint(ProcessPoolExecutor.__name__)\n```\n\n```output\nProcessPoolExecutor\n```\n:::\n\n:::note\n- Threads share memory, so mutable shared state needs care.\n- Processes avoid shared interpreter state but require values to cross a process boundary.\n- Prefer `asyncio` for coroutine-based I/O and executors for ordinary blocking callables.\n:::\n', 'truth-and-size.md': '+++\nslug = "truth-and-size"\ntitle = "Truth and Size"\nsection = "Data Model"\nsummary = "__bool__ and __len__ decide how objects behave in truth tests and len()."\ndoc_path = "/reference/datamodel.html#object.__bool__"\nsee_also = [\n "truthiness",\n "special-methods",\n "container-protocols",\n]\n+++\n\nTruth tests ask an object whether it should count as true. Containers usually answer through their size, while domain objects can answer with `__bool__` when emptiness is not the right idea.\n\n`__len__` supports `len(obj)` and also provides a fallback truth value: length zero is false, non-zero length is true. `__bool__` is more direct and wins when both are present.\n\nUse these methods to match the meaning of your object. A queue can be false when it has no items; an account might be true only when it is active, regardless of its balance.\n\n:::program\n```python\nclass Inbox:\n def __init__(self, messages):\n self.messages = list(messages)\n\n def __len__(self):\n return len(self.messages)\n\nclass Account:\n def __init__(self, active):\n self.active = active\n\n def __bool__(self):\n return self.active\n\nprint(len(Inbox(["hi", "bye"])))\nprint(bool(Inbox([])))\nprint(bool(Account(False)))\n```\n:::\n\n:::cell\n`__len__` lets `len()` ask an object for its size.\n\n```python\nclass Inbox:\n def __init__(self, messages):\n self.messages = list(messages)\n\n def __len__(self):\n return len(self.messages)\n\nprint(len(Inbox(["hi", "bye"])))\n```\n\n```output\n2\n```\n:::\n\n:::cell\nIf a class has `__len__` but no `__bool__`, Python uses zero length as false.\n\n```python\nprint(bool(Inbox([])))\n```\n\n```output\nFalse\n```\n:::\n\n:::cell\n`__bool__` expresses truth directly when the answer is not just container size.\n\n```python\nclass Account:\n def __init__(self, active):\n self.active = active\n\n def __bool__(self):\n return self.active\n\nprint(bool(Account(False)))\n```\n\n```output\nFalse\n```\n:::\n\n:::note\n- Prefer `__len__` for sized containers.\n- Prefer `__bool__` when truth has domain meaning.\n- Keep truth tests unsurprising; surprising falsy objects make conditionals harder to read.\n:::\n', 'truthiness.md': '+++\nslug = "truthiness"\ntitle = "Truthiness"\nsection = "Basics"\nsummary = "Python conditions use truthiness, not only explicit booleans."\ndoc_path = "/library/stdtypes.html#truth-value-testing"\n+++\n\nTruthiness is one of Python\'s most important conveniences: conditions can test objects directly instead of requiring explicit boolean comparisons everywhere.\n\nEmpty containers, numeric zero, None, and False are false; most other values are true. This makes common checks such as if items: concise and idiomatic.\n\nUse truthiness when it reads naturally, but choose explicit comparisons when the distinction matters, such as checking whether a value is exactly None.\n\n:::program\n```python\nitems = []\nname = "Ada"\n\nif not items:\n print("no items")\n\nif name:\n print("has a name")\n\nprint(bool(0))\nprint(bool(42))\n```\n:::\n\n:::cell\nTruthiness is one of Python\'s most important conveniences: conditions can test objects directly instead of requiring explicit boolean comparisons everywhere.\n\nEmpty containers, numeric zero, None, and False are false; most other values are true. This makes common checks such as if items: concise and idiomatic.\n\n```python\nitems = []\nname = "Ada"\n\nif not items:\n print("no items")\n```\n\n```output\nno items\n```\n:::\n\n:::cell\nUse truthiness when it reads naturally, but choose explicit comparisons when the distinction matters, such as checking whether a value is exactly None.\n\n```python\nif name:\n print("has a name")\n```\n\n```output\nhas a name\n```\n:::\n\n:::cell\nUse truthiness when it reads naturally, but choose explicit comparisons when the distinction matters, such as checking whether a value is exactly None.\n\n```python\nprint(bool(0))\nprint(bool(42))\n```\n\n```output\nFalse\nTrue\n```\n:::\n\n:::note\n- Empty containers and zero-like numbers are false in conditions.\n- Use explicit comparisons when they communicate intent better than truthiness.\n:::\n', 'tuples.md': '+++\nslug = "tuples"\ntitle = "Tuples"\nsection = "Collections"\nsummary = "Tuples group a fixed number of positional values."\ndoc_path = "/tutorial/datastructures.html#tuples-and-sequences"\nsee_also = [\n "lists",\n "unpacking",\n "structured-data-shapes",\n]\n+++\n\nTuples are ordered, immutable sequences. They exist for small fixed groups where position has meaning: coordinates, RGB colors, database rows, and multiple return values.\n\nUse lists for variable-length collections of similar items. Use tuples when the number of positions is part of the data shape and unpacking can give each position a useful name.\n\nBecause tuples are immutable, you cannot append or replace positions in place. If the shape needs to grow or change, a list or dataclass is usually a better fit.\n\n:::program\n```python\npoint = (3, 4)\nx, y = point\nprint(x + y)\n\nred = (255, 0, 0)\nprint(red[0])\nprint(len(red))\n\nrecord = ("Ada", 10)\nname, score = record\nprint(f"{name}: {score}")\n\nscores = [10, 9, 8]\nscores.append(7)\nprint(scores)\n\nstudent = ("Ada", 2024, "math")\nname, year, subject = student\nprint(name, year, subject)\n```\n:::\n\n:::cell\nUse a tuple for a fixed-size record where each position has a known meaning. Unpacking turns those positions into names at the point of use.\n\n```python\npoint = (3, 4)\nx, y = point\nprint(x + y)\n```\n\n```output\n7\n```\n:::\n\n:::cell\nTuples are sequences, so indexing and `len()` work. They are different from lists because their length and item references are fixed after creation.\n\n```python\nred = (255, 0, 0)\nprint(red[0])\nprint(len(red))\n```\n\n```output\n255\n3\n```\n:::\n\n:::cell\nTuples pair naturally with multiple return values and unpacking. If the fields need names everywhere, graduate to a dataclass or named tuple.\n\n```python\nrecord = ("Ada", 10)\nname, score = record\nprint(f"{name}: {score}")\n```\n\n```output\nAda: 10\n```\n:::\n\n:::cell\nLists and tuples carry different intent. A list holds a variable number of similar items and grows with `append`; a tuple has a fixed shape where each position has its own meaning, and unpacking gives those positions names.\n\n```python\nscores = [10, 9, 8]\nscores.append(7)\nprint(scores)\n\nstudent = ("Ada", 2024, "math")\nname, year, subject = student\nprint(name, year, subject)\n```\n\n```output\n[10, 9, 8, 7]\nAda 2024 math\n```\n:::\n\n:::note\n- Tuples are immutable sequences with fixed length.\n- Use tuples for small records where position has meaning.\n- Use lists for variable-length collections of similar items.\n- Reach for a dataclass or `NamedTuple` when fields deserve names everywhere they\'re used.\n:::\n', 'type-aliases.md': '+++\nslug = "type-aliases"\ntitle = "Type Aliases"\nsection = "Types"\nsummary = "Type aliases give a meaningful name to a repeated type shape."\ndoc_path = "/library/typing.html#type-aliases"\nsee_also = [\n "type-hints",\n "newtype",\n "union-and-optional-types",\n]\n+++\n\nA type alias gives a name to an annotation shape. It helps readers and type checkers understand the role of a value without repeating a long type expression everywhere.\n\nPython 3.13 supports the `type` statement for explicit aliases. Older assignment-style aliases still appear in code, but the `type` statement makes the intent clear and creates a `TypeAliasType` object at runtime.\n\nAn alias does not create a new runtime type. If you need a static distinction between compatible values such as user IDs and order IDs, use `NewType` instead.\n\n:::program\n```python\ntype UserId = int\ntype Scores = dict[UserId, int]\nLegacyName = str\n\n\ndef best_user(scores: Scores) -> UserId:\n return max(scores, key=scores.get)\n\nscores: Scores = {1: 98, 2: 91}\nprint(best_user(scores))\nprint(UserId.__name__)\nprint(LegacyName("Ada"))\n```\n:::\n\n:::cell\nThe `type` statement names an annotation shape. Here `Scores` means a dictionary from user IDs to integer scores.\n\n```python\ntype UserId = int\ntype Scores = dict[UserId, int]\n\n\ndef best_user(scores: Scores) -> UserId:\n return max(scores, key=scores.get)\n\nscores: Scores = {1: 98, 2: 91}\nprint(best_user(scores))\n```\n\n```output\n1\n```\n:::\n\n:::cell\nModern aliases are runtime objects that keep their alias name for introspection.\n\n```python\nprint(UserId.__name__)\nprint(Scores.__name__)\n```\n\n```output\nUserId\nScores\n```\n:::\n\n:::cell\nAssignment-style aliases are still common, but they are just ordinary names bound to existing objects.\n\n```python\nLegacyName = str\nprint(LegacyName("Ada"))\nprint(LegacyName is str)\n```\n\n```output\nAda\nTrue\n```\n:::\n\n:::note\n- Use aliases to name repeated or domain-specific annotation shapes.\n- A type alias does not validate values at runtime.\n- Use `NewType` when two values share a runtime representation but should not be mixed statically.\n:::\n', 'type-hints.md': '+++\nslug = "type-hints"\ntitle = "Type Hints"\nsection = "Types"\nsummary = "Annotations document expected types and power static analysis."\ndoc_path = "/library/typing.html"\nsee_also = [\n "union-and-optional-types",\n "type-aliases",\n "generics-and-typevar",\n "runtime-type-checks",\n]\n+++\n\nType hints are annotations that document expected shapes for values, parameters, and return results. They exist so tools and readers can understand API boundaries before the program runs.\n\nPython stores many annotations but does not enforce most of them at runtime. Use type hints for communication and static analysis; use validation or exceptions when runtime checks are required.\n\nThe alternative to an annotation is prose, tests, or runtime validation. Good Python code often uses all three at important boundaries.\n\n:::program\n```python\nfrom typing import TypeAlias\n\ndef total(numbers: list[int]) -> int:\n return sum(numbers)\n\nprint(total([1, 2, 3]))\nprint(total.__annotations__)\n\n\ndef label(score: int) -> str:\n return f"score={score}"\n\nprint(label("high"))\n\n\ndef find(name: str, options: list[str]) -> str | None:\n return name if name in options else None\n\nprint(find("Ada", ["Ada", "Grace"]))\nprint(find("Guido", ["Ada", "Grace"]))\n\n\nfrom typing import Optional\n\ndef lookup(name: str) -> Optional[int]:\n table = {"Ada": 1815, "Grace": 1906}\n return table.get(name)\n\nprint(lookup("Ada"))\nprint(lookup("Guido"))\n\n\nScore: TypeAlias = int\n\ndef grade(score: Score) -> str:\n return "pass" if score >= 50 else "fail"\n\nprint(grade(72))\n```\n:::\n\n:::cell\nType hints document expected parameter and return shapes. Python still runs the function normally at runtime.\n\n```python\ndef total(numbers: list[int]) -> int:\n return sum(numbers)\n\nprint(total([1, 2, 3]))\n```\n\n```output\n6\n```\n:::\n\n:::cell\nPython stores annotations on the function object for tools and introspection. Type checkers use this information without changing the function call syntax.\n\n```python\nprint(total.__annotations__)\n```\n\n```output\n{\'numbers\': list[int], \'return\': }\n```\n:::\n\n:::cell\nMost hints are not runtime validation. This call passes a string where the hint says `int`; Python still calls the function because the body can format any value.\n\n```python\ndef label(score: int) -> str:\n return f"score={score}"\n\nprint(label("high"))\n```\n\n```output\nscore=high\n```\n:::\n\n:::cell\nUse `X | Y` (PEP 604) to express "either type". `str | None` says the result is a string or absent. `typing.Optional[X]` is the older, still-supported spelling for the same idea — `Optional[X]` is equivalent to `X | None`.\n\n```python\ndef find(name: str, options: list[str]) -> str | None:\n return name if name in options else None\n\nprint(find("Ada", ["Ada", "Grace"]))\nprint(find("Guido", ["Ada", "Grace"]))\n\n\nfrom typing import Optional\n\ndef lookup(name: str) -> Optional[int]:\n table = {"Ada": 1815, "Grace": 1906}\n return table.get(name)\n\nprint(lookup("Ada"))\nprint(lookup("Guido"))\n```\n\n```output\nAda\nNone\n1815\nNone\n```\n:::\n\n:::cell\n`TypeAlias` names a type so it can be reused with intent. `Score: TypeAlias = int` keeps the underlying type at runtime but lets the API talk about a domain concept rather than a primitive.\n\n```python\nfrom typing import TypeAlias\n\nScore: TypeAlias = int\n\ndef grade(score: Score) -> str:\n return "pass" if score >= 50 else "fail"\n\nprint(grade(72))\n```\n\n```output\npass\n```\n:::\n\n:::note\n- Python does not enforce most type hints at runtime.\n- Tools like type checkers and editors use annotations to catch mistakes earlier.\n- Use `X | Y` for unions and `Optional[X]` for "X or None"; both spellings mean the same thing.\n- Reach for `TypeAlias` when a domain name reads better than a raw primitive type.\n- Use runtime validation when untrusted input must be rejected while the program runs.\n:::\n', 'typed-dicts.md': '+++\nslug = "typed-dicts"\ntitle = "TypedDict"\nsection = "Types"\nsummary = "TypedDict describes dictionaries with known string keys."\ndoc_path = "/library/typing.html#typing.TypedDict"\nsee_also = [\n "dicts",\n "json",\n "dataclasses",\n "structured-data-shapes",\n]\n+++\n\n`TypedDict` describes dictionary records with known keys. It is useful for JSON-like data that should remain a dictionary instead of becoming a class instance.\n\nThe important boundary is static versus runtime behavior. Type checkers can know that `name` is a string and `score` is an integer, but at runtime the value is still an ordinary `dict`.\n\nUse `TypedDict` for external records and `dataclass` when your own program wants attribute access, methods, and construction behavior.\n\n:::program\n```python\nfrom typing import NotRequired, TypedDict\n\nclass User(TypedDict):\n name: str\n score: int\n nickname: NotRequired[str]\n\n\ndef describe(user: User) -> str:\n return f"{user[\'name\']}: {user[\'score\']}"\n\nrecord: User = {"name": "Ada", "score": 98}\nprint(describe(record))\nprint(isinstance(record, dict))\nprint(record.get("nickname", "none"))\n```\n:::\n\n:::cell\nUse `TypedDict` for JSON-like records that remain dictionaries.\n\n```python\nfrom typing import TypedDict\n\nclass User(TypedDict):\n name: str\n score: int\n\n\ndef describe(user: User) -> str:\n return f"{user[\'name\']}: {user[\'score\']}"\n\nrecord: User = {"name": "Ada", "score": 98}\nprint(describe(record))\n```\n\n```output\nAda: 98\n```\n:::\n\n:::cell\nAt runtime, a `TypedDict` value is still a plain dictionary.\n\n```python\nprint(isinstance(record, dict))\nprint(type(record).__name__)\n```\n\n```output\nTrue\ndict\n```\n:::\n\n:::cell\n`NotRequired` marks a key that type checkers should treat as optional. Runtime lookup still uses normal dictionary tools such as `get()`.\n\n```python\nfrom typing import NotRequired\n\nclass UserWithNickname(TypedDict):\n name: str\n score: int\n nickname: NotRequired[str]\n\nrecord: UserWithNickname = {"name": "Ada", "score": 98}\nprint(record.get("nickname", "none"))\n```\n\n```output\nnone\n```\n:::\n\n:::note\n- Use `TypedDict` for dictionary records from JSON or APIs.\n- Type checkers understand required and optional keys.\n- Runtime behavior is still ordinary dictionary behavior.\n:::\n', 'union-and-optional-types.md': '+++\nslug = "union-and-optional-types"\ntitle = "Union and Optional Types"\nsection = "Types"\nsummary = "The | operator describes values that may have more than one static type."\ndoc_path = "/library/typing.html#typing.Optional"\nsee_also = [\n "none",\n "type-hints",\n "match-statements",\n]\n+++\n\nA union type says that a value may have one of several static shapes. `int | str` means callers may pass either an integer or a string.\n\n`T | None` is the modern spelling for an optional value. The annotation documents that absence is expected, but the code still needs to handle `None` before using the non-optional behavior.\n\nUnions are useful at boundaries where input is flexible. Inside a function, narrow the value with an `is None`, `isinstance()`, or pattern check so the rest of the code has one clear shape.\n\n:::program\n```python\ndef label(value: int | str) -> str:\n return f"item-{value}"\n\n\ndef greeting(name: str | None) -> str:\n if name is None:\n return "hello guest"\n return f"hello {name.upper()}"\n\nprint(label(3))\nprint(label("A"))\nprint(greeting(None))\nprint(greeting("Ada"))\nprint(greeting.__annotations__)\n```\n:::\n\n:::cell\nUse `A | B` when a value may have either type. The function body should use operations that make sense for every member of the union.\n\n```python\ndef label(value: int | str) -> str:\n return f"item-{value}"\n\nprint(label(3))\nprint(label("A"))\n```\n\n```output\nitem-3\nitem-A\n```\n:::\n\n:::cell\n`str | None` means the function accepts either a string or explicit absence. Check for `None` before calling string methods.\n\n```python\ndef greeting(name: str | None) -> str:\n if name is None:\n return "hello guest"\n return f"hello {name.upper()}"\n\nprint(greeting(None))\nprint(greeting("Ada"))\n```\n\n```output\nhello guest\nhello ADA\n```\n:::\n\n:::cell\nUnion annotations are visible at runtime, but Python does not enforce them when the function is called.\n\n```python\nprint(greeting.__annotations__)\n```\n\n```output\n{\'name\': str | None, \'return\': }\n```\n:::\n\n:::note\n- Use `A | B` when a value may have either type.\n- `T | None` means absence is an expected case, not an error by itself.\n- Narrow unions before using behavior that belongs to only one member type.\n:::\n', 'unpacking.md': '+++\nslug = "unpacking"\ntitle = "Unpacking"\nsection = "Collections"\nsummary = "Unpacking binds names from sequences and mappings concisely."\ndoc_path = "/tutorial/datastructures.html#tuples-and-sequences"\n+++\n\nUnpacking binds multiple names from one iterable or mapping. It makes the structure of data visible at the point where values are introduced.\n\nStarred unpacking handles variable-length sequences by collecting the middle or remaining values. This keeps common head-tail patterns readable.\n\nDictionary unpacking with ** connects structured data to function calls. It is widely used in configuration, adapters, and code that bridges APIs.\n\n:::program\n```python\npoint = (3, 4)\nx, y = point\nprint(x, y)\n\nfirst, *middle, last = [1, 2, 3, 4]\nprint(first, middle, last)\n\ndef describe(name, language):\n print(name, language)\n\ndata = {"name": "Ada", "language": "Python"}\ndescribe(**data)\n```\n:::\n\n:::cell\nUnpacking binds multiple names from one iterable or mapping. It makes the structure of data visible at the point where values are introduced.\n\n```python\npoint = (3, 4)\nx, y = point\nprint(x, y)\n```\n\n```output\n3 4\n```\n:::\n\n:::cell\nStarred unpacking handles variable-length sequences by collecting the middle or remaining values. This keeps common head-tail patterns readable.\n\n```python\nfirst, *middle, last = [1, 2, 3, 4]\nprint(first, middle, last)\n```\n\n```output\n1 [2, 3] 4\n```\n:::\n\n:::cell\nDictionary unpacking with ** connects structured data to function calls. It is widely used in configuration, adapters, and code that bridges APIs.\n\nDictionary unpacking with ** connects structured data to function calls. It is widely used in configuration, adapters, and code that bridges APIs.\n\n```python\ndef describe(name, language):\n print(name, language)\n\ndata = {"name": "Ada", "language": "Python"}\ndescribe(**data)\n```\n\n```output\nAda Python\n```\n:::\n\n:::note\n- Starred unpacking collects the remaining values into a list.\n- Dictionary unpacking with ** is common when calling functions with structured data.\n- Prefer indexing when you need one position; prefer unpacking when naming several positions makes the shape clearer.\n:::\n', 'values.md': '+++\nslug = "values"\ntitle = "Values"\nsection = "Basics"\nsummary = "Python programs evaluate expressions into objects such as text, numbers, booleans, and None."\ndoc_path = "/library/stdtypes.html"\n+++\n\nA Python program works by evaluating expressions into values. Values are objects: text, integers, floats, booleans, `None`, and many richer types introduced later.\n\nNames point to values; they are not declarations that permanently fix a type. Operations usually produce new values, which you can print, store, compare, or pass to functions.\n\nThis page is a map, not the whole territory. Later pages explain the boundaries: equality vs identity, mutable vs immutable values, truthiness vs literal booleans, and `None` vs a missing key or an exception.\n\n:::program\n```python\ntext = "python"\ncount = 3\nratio = 2.5\nready = True\nmissing = None\n\nprint(type(text).__name__)\nprint(text.upper())\nprint(count + 4)\nprint(ratio * 2)\n\nprint(ready and count > 0)\nprint(missing is None)\n```\n:::\n\n:::cell\nStart with several built-in values. Python does not require declarations before binding these names, and each value is still an object with a type.\n\n```python\ntext = "python"\ncount = 3\nratio = 2.5\nready = True\nmissing = None\n\nprint(type(text).__name__)\n```\n\n```output\nstr\n```\n:::\n\n:::cell\nMethods and operators evaluate to new values. The original `text`, `count`, and `ratio` bindings remain ordinary objects you can reuse.\n\n```python\nprint(text.upper())\nprint(count + 4)\nprint(ratio * 2)\n```\n\n```output\nPYTHON\n7\n5.0\n```\n:::\n\n:::cell\nBoolean expressions combine facts, and `None` is checked by identity because it is a singleton absence marker.\n\n```python\nprint(ready and count > 0)\nprint(missing is None)\n```\n\n```output\nTrue\nTrue\n```\n:::\n\n:::note\n- Values are objects; names point to them and operations usually create new values.\n- Use `is None` for the absence marker, not `== None`.\n- This overview introduces boundaries that later pages explain in detail.\n:::\n', 'variables.md': '+++\nslug = "variables"\ntitle = "Variables"\nsection = "Basics"\nsummary = "Names are bound to values with assignment."\ndoc_path = "/reference/simple_stmts.html#assignment-statements"\n+++\n\nPython variables are names bound to objects. Assignment creates or rebinds a name; it does not require a declaration and it does not permanently attach a type to the name.\n\nRebinding changes which object a name refers to. Augmented assignment such as `+=` is the idiomatic way to update counters and accumulators.\n\nUse clear names for values that matter later. Python\'s flexibility makes naming more important, not less.\n\nUse assignment when a value needs a name for reuse or explanation. Prefer a direct expression when naming the intermediate value would add noise.\n\n:::program\n```python\nmessage = "hi"\nprint(message)\n\nmessage = "hello"\nprint(message)\n\ncount = 3\ncount += 1\nprint(count)\n```\n:::\n\n:::cell\nAssignment binds a name to a value. Once bound, the name can be used anywhere that value is needed.\n\n```python\nmessage = "hi"\nprint(message)\n```\n\n```output\nhi\n```\n:::\n\n:::cell\nAssignment can rebind the same name to a different value. The name is not permanently attached to the first object.\n\n```python\nmessage = "hello"\nprint(message)\n```\n\n```output\nhello\n```\n:::\n\n:::cell\nAugmented assignment reads the current binding, computes an updated value, and stores the result back under the same name.\n\n```python\ncount = 3\ncount += 1\nprint(count)\n```\n\n```output\n4\n```\n:::\n\n:::note\n- Python variables are names bound to objects, not boxes with fixed types.\n- Rebinding a name is normal.\n- Use augmented assignment for counters and accumulators.\n:::\n', 'virtual-environments.md': '+++\nslug = "virtual-environments"\ntitle = "Virtual Environments"\nsection = "Modules"\nsummary = "Virtual environments isolate a project\'s Python packages."\ndoc_path = "/library/venv.html"\nexpected_output = ".venv\\nTrue\\n"\n+++\n\nVirtual environments isolate a project\'s Python packages. They exist so one project can install dependencies without changing another project\'s environment.\n\nThe usual command-line workflow is `python -m venv .venv`, but Python also exposes the same feature through the `venv` module. This example creates a temporary environment so the example cleans up after itself.\n\nA virtual environment changes where Python looks for installed packages. It does not change the language, and it is separate from package layout, imports, and module names.\n\n:::program\n```python\nimport pathlib\nimport tempfile\nimport venv\n\nwith tempfile.TemporaryDirectory() as directory:\n env_path = pathlib.Path(directory) / ".venv"\n builder = venv.EnvBuilder(with_pip=False)\n builder.create(env_path)\n\n print(env_path.name)\n print((env_path / "pyvenv.cfg").exists())\n```\n:::\n\n:::unsupported\nDynamic Workers do not provide the `venv` module or a project environment workflow.\n\n```python\nbuilder = venv.EnvBuilder(with_pip=False)\nbuilder.create(".venv")\n```\n:::\n\n:::cell\n`venv.EnvBuilder` creates the same kind of isolated environment as `python -m venv`. A temporary directory keeps the example from leaving project files behind.\n\n```python\nimport pathlib\nimport tempfile\nimport venv\n\nwith tempfile.TemporaryDirectory() as directory:\n env_path = pathlib.Path(directory) / ".venv"\n builder = venv.EnvBuilder(with_pip=False)\n builder.create(env_path)\n\n print(env_path.name)\n print((env_path / "pyvenv.cfg").exists())\n```\n\n```output\n.venv\nTrue\n```\n:::\n\n:::note\n- A virtual environment gives a project its own install location.\n- Inside a venv, `sys.prefix` usually differs from `sys.base_prefix`.\n- Use `python -m venv .venv` at the command line for everyday project setup.\n:::\n', 'warnings.md': '+++\nslug = "warnings"\ntitle = "Warnings"\nsection = "Errors"\nsummary = "warnings report soft problems without immediately stopping the program."\ndoc_path = "/library/warnings.html"\n+++\n\nwarnings report soft problems without immediately stopping the program. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nimport warnings\n\n\ndef old_name():\n warnings.warn("old_name is deprecated", DeprecationWarning, stacklevel=2)\n return "result"\n\nwarnings.simplefilter("always", DeprecationWarning)\nwith warnings.catch_warnings(record=True) as caught:\n print(old_name())\n print(caught[0].category.__name__)\n print(str(caught[0].message))\n```\n:::\n\n:::cell\nWarnings are useful for deprecations and soft failures.\n\n```python\nimport warnings\n\n\ndef old_name():\n warnings.warn("old_name is deprecated", DeprecationWarning, stacklevel=2)\n return "result"\n\nwarnings.simplefilter("always", DeprecationWarning)\nwith warnings.catch_warnings(record=True) as caught:\n print(old_name())\n print(caught[0].category.__name__)\n print(str(caught[0].message))\n```\n\n```output\nresult\nDeprecationWarning\nold_name is deprecated\n```\n:::\n\n:::note\n- Warnings are useful for deprecations and soft failures.\n- Filters decide whether warnings are ignored, shown, or turned into errors.\n- Tests often capture warnings to assert migration behavior.\n:::\n', 'while-loops.md': '+++\nslug = "while-loops"\ntitle = "While Loops"\nsection = "Control Flow"\nsummary = "while repeats until changing state makes a condition false."\ndoc_path = "/reference/compound_stmts.html#while"\n+++\n\nA `while` loop repeats while a condition remains true. Unlike `for`, which consumes an existing iterable, `while` is for state-driven repetition where the next step depends on what happened so far.\n\nThe loop body must make progress toward stopping. That progress might be decrementing a counter, reading until a sentinel value, or waiting until some external state changes.\n\nReach for `for` when you already have values to consume. Reach for `while` when the loop\'s own state decides whether another iteration is needed.\n\n:::program\n```python\nremaining = 3\nwhile remaining > 0:\n print(f"launch in {remaining}")\n remaining -= 1\nprint("liftoff")\n\nresponses = iter(["retry", "retry", "ok"])\nstatus = next(responses)\nwhile status != "ok":\n print(f"status: {status}")\n status = next(responses)\nprint(f"status: {status}")\n```\n:::\n\n:::cell\nUse `while` when the condition, not an iterable, controls repetition. Here the loop owns the countdown state and updates it each time through the body.\n\n```python\nremaining = 3\nwhile remaining > 0:\n print(f"launch in {remaining}")\n remaining -= 1\nprint("liftoff")\n```\n\n```output\nlaunch in 3\nlaunch in 2\nlaunch in 1\nliftoff\n```\n:::\n\n:::cell\nA sentinel loop stops when a special value appears. The loop does not know in advance how many retries it will need; it keeps going until the state says to stop.\n\n```python\nresponses = iter(["retry", "retry", "ok"])\nstatus = next(responses)\nwhile status != "ok":\n print(f"status: {status}")\n status = next(responses)\nprint(f"status: {status}")\n```\n\n```output\nstatus: retry\nstatus: retry\nstatus: ok\n```\n:::\n\n:::note\n- Use `while` when changing state decides whether the loop continues.\n- Update loop state inside the body so the condition can become false.\n- Prefer `for` when you already have a collection, range, iterator, or generator to consume.\n:::\n', 'yield-from.md': '+++\nslug = "yield-from"\ntitle = "Yield From"\nsection = "Iteration"\nsummary = "yield from delegates part of a generator to another iterable."\ndoc_path = "/reference/expressions.html#yield-expressions"\nsee_also = [\n "generators",\n "generator-expressions",\n "itertools",\n]\n+++\n\n`yield from` lets one generator yield every value from another iterable. It is a compact way to delegate part of a stream.\n\nUse it when a generator is mostly stitching together other iterables or sub-generators. It keeps the producer pipeline visible without writing a nested `for` loop.\n\nThe consumer still sees one stream of values.\n\n:::program\n```python\ndef page():\n yield "header"\n yield from ["intro", "body"]\n yield "footer"\n\nprint(list(page()))\n\n\ndef flatten(rows):\n for row in rows:\n yield from row\n\nprint(list(flatten([[1, 2], [3]])))\n```\n:::\n\n:::cell\n`yield from` delegates to another iterable. The caller receives one stream even though part of it came from a list.\n\n```python\ndef page():\n yield "header"\n yield from ["intro", "body"]\n yield "footer"\n\nprint(list(page()))\n```\n\n```output\n[\'header\', \'intro\', \'body\', \'footer\']\n```\n:::\n\n:::cell\nDelegation is useful when flattening nested iterables. `yield from row` replaces an inner loop that would yield each item by hand.\n\n```python\ndef flatten(rows):\n for row in rows:\n yield from row\n\nprint(list(flatten([[1, 2], [3]])))\n```\n\n```output\n[1, 2, 3]\n```\n:::\n\n:::note\n- `yield from iterable` yields each value from that iterable.\n- It keeps generator pipelines compact.\n- Use a plain `yield` when producing one value directly.\n:::\n'} +EXAMPLE_SOURCE_FILES = {'manifest.toml': 'python_version = "3.13"\ndocs_base_url = "https://docs.python.org/3.13"\n\norder = [\n "hello-world",\n "values",\n "literals",\n "numbers",\n "booleans",\n "operators",\n "none",\n "variables",\n "constants",\n "truthiness",\n "equality-and-identity",\n "mutability",\n "object-lifecycle",\n "strings",\n "bytes-and-bytearray",\n "string-formatting",\n "conditionals",\n "guard-clauses",\n "assignment-expressions",\n "for-loops",\n "break-and-continue",\n "loop-else",\n "iterating-over-iterables",\n "iterators",\n "iterator-vs-iterable",\n "sentinel-iteration",\n "match-statements",\n "advanced-match-patterns",\n "while-loops",\n "lists",\n "tuples",\n "unpacking",\n "dicts",\n "sets",\n "slices",\n "comprehensions",\n "comprehension-patterns",\n "sorting",\n "collections-module",\n "copying-collections",\n "functions",\n "keyword-only-arguments",\n "positional-only-parameters",\n "args-and-kwargs",\n "multiple-return-values",\n "closures",\n "partial-functions",\n "scope-global-nonlocal",\n "recursion",\n "lambdas",\n "generators",\n "yield-from",\n "generator-expressions",\n "itertools",\n "decorators",\n "classes",\n "inheritance-and-super",\n "classmethods-and-staticmethods",\n "dataclasses",\n "properties",\n "special-methods",\n "truth-and-size",\n "container-protocols",\n "callable-objects",\n "operator-overloading",\n "attribute-access",\n "bound-and-unbound-methods",\n "descriptors",\n "metaclasses",\n "context-managers",\n "delete-statements",\n "exceptions",\n "assertions",\n "exception-chaining",\n "exception-groups",\n "warnings",\n "modules",\n "import-aliases",\n "packages",\n "virtual-environments",\n "type-hints",\n "runtime-type-checks",\n "union-and-optional-types",\n "type-aliases",\n "typed-dicts",\n "structured-data-shapes",\n "literal-and-final",\n "callable-types",\n "generics-and-typevar",\n "paramspec",\n "overloads",\n "casts-and-any",\n "newtype",\n "protocols",\n "abstract-base-classes",\n "enums",\n "regular-expressions",\n "number-parsing",\n "custom-exceptions",\n "json",\n "logging",\n "testing",\n "subprocesses",\n "threads-and-processes",\n "networking",\n "datetime",\n "csv-data",\n "async-await",\n "async-iteration-and-context",\n]\n', 'abstract-base-classes.md': '+++\nslug = "abstract-base-classes"\ntitle = "Abstract Base Classes"\nsection = "Classes"\nsummary = "ABC and abstractmethod enforce that subclasses implement required methods."\ndoc_path = "/library/abc.html"\nsee_also = [\n "protocols",\n "inheritance-and-super",\n "classes",\n]\n+++\n\n`ABC` and `@abstractmethod` describe an interface that subclasses must implement. The base class refuses to instantiate until a concrete subclass provides every abstract method, which catches "I forgot to implement this" at construction time rather than at the first method call.\n\nABCs are different from `Protocol`. An ABC is nominal: a class participates in the contract by inheriting from it. A `Protocol` is structural: any class with the right methods qualifies, no inheritance required. Reach for an ABC when you want shared implementation in the base class or you want `isinstance()` to mean "explicitly opted in"; reach for a `Protocol` when you only care about behavior at the API boundary.\n\nThe cost is a small amount of ceremony at the type level. The benefit is that a half-implemented subclass cannot be created by accident.\n\n:::program\n```python\nfrom abc import ABC, abstractmethod\nfrom typing import Protocol\n\nclass Shape(ABC):\n @abstractmethod\n def area(self) -> float:\n ...\n\n def describe(self) -> str:\n return f"shape with area {self.area()}"\n\ntry:\n Shape()\nexcept TypeError as error:\n print(error)\n\nclass Square(Shape):\n def __init__(self, side):\n self.side = side\n\n def area(self):\n return self.side ** 2\n\nprint(Square(3).area())\nprint(Square(3).describe())\n\nclass Incomplete(Shape):\n pass\n\ntry:\n Incomplete()\nexcept TypeError as error:\n print(error)\n\nclass HasArea(Protocol):\n def area(self) -> float:\n ...\n\nclass Triangle:\n def __init__(self, base, height):\n self.base = base\n self.height = height\n\n def area(self):\n return 0.5 * self.base * self.height\n\ndef total_area(shapes: list[HasArea]) -> float:\n return sum(shape.area() for shape in shapes)\n\nprint(total_area([Square(3), Triangle(4, 3)]))\nprint(isinstance(Triangle(4, 3), Shape))\nprint(isinstance(Square(3), Shape))\n```\n:::\n\n:::cell\n`ABC` plus `@abstractmethod` declares the contract. Trying to construct the base class itself fails because at least one method has no implementation. A concrete `describe()` lives alongside the abstract `area()` so subclasses inherit shared behavior for free.\n\n```python\nfrom abc import ABC, abstractmethod\n\nclass Shape(ABC):\n @abstractmethod\n def area(self) -> float:\n ...\n\n def describe(self) -> str:\n return f"shape with area {self.area()}"\n\ntry:\n Shape()\nexcept TypeError as error:\n print(error)\n```\n\n```output\nCan\'t instantiate abstract class Shape without an implementation for abstract method \'area\'\n```\n:::\n\n:::cell\nA subclass that implements every abstract method is concrete and can be instantiated. It also inherits the non-abstract methods from the base class.\n\n```python\nclass Square(Shape):\n def __init__(self, side):\n self.side = side\n\n def area(self):\n return self.side ** 2\n\nprint(Square(3).area())\nprint(Square(3).describe())\n```\n\n```output\n9\nshape with area 9\n```\n:::\n\n:::cell\nA subclass that forgets to implement an abstract method also cannot be instantiated — that is the value the ABC adds. The error fires at construction, not when something later tries to call the missing method.\n\n```python\nclass Incomplete(Shape):\n pass\n\ntry:\n Incomplete()\nexcept TypeError as error:\n print(error)\n```\n\n```output\nCan\'t instantiate abstract class Incomplete without an implementation for abstract method \'area\'\n```\n:::\n\n:::cell\nContrast with `Protocol`. A `HasArea` protocol accepts any class with an `area()` method, no inheritance required. `Triangle` does not inherit from `Shape`, so it satisfies the protocol but fails `isinstance(_, Shape)`. `Square` satisfies both because it explicitly inherited from the ABC.\n\n```python\nfrom typing import Protocol\n\nclass HasArea(Protocol):\n def area(self) -> float:\n ...\n\nclass Triangle:\n def __init__(self, base, height):\n self.base = base\n self.height = height\n\n def area(self):\n return 0.5 * self.base * self.height\n\ndef total_area(shapes: list[HasArea]) -> float:\n return sum(shape.area() for shape in shapes)\n\nprint(total_area([Square(3), Triangle(4, 3)]))\nprint(isinstance(Triangle(4, 3), Shape))\nprint(isinstance(Square(3), Shape))\n```\n\n```output\n15.0\nFalse\nTrue\n```\n:::\n\n:::note\n- `ABC` plus `@abstractmethod` blocks instantiation until every abstract method has an implementation.\n- ABCs are nominal — subclasses opt in by inheriting; `isinstance()` reflects that opt-in.\n- Protocols are structural — any class with the right shape qualifies, regardless of inheritance.\n- Prefer an ABC when shared implementation or explicit opt-in matters; prefer a Protocol when only behavior at the API boundary matters.\n:::\n', 'advanced-match-patterns.md': '+++\nslug = "advanced-match-patterns"\ntitle = "Advanced Match Patterns"\nsection = "Control Flow"\nsummary = "match patterns can destructure sequences, combine alternatives, and add guards."\ndoc_path = "/tutorial/controlflow.html#match-statements"\nsee_also = [\n "match-statements",\n "tuples",\n "classes",\n]\n+++\n\nStructural pattern matching is more than equality checks. Patterns can destructure sequences, match several alternatives, capture the rest of a sequence, and use guards.\n\nUse these forms when the shape of data is the decision. If the decision is only a single boolean condition, ordinary `if` statements are usually clearer.\n\nThe wildcard `_` catches everything not matched earlier.\n\n:::program\n```python\ndef describe(command):\n match command:\n case ["move", x, y] if x >= 0 and y >= 0:\n return f"move to {x},{y}"\n case ["quit" | "exit"]:\n return "stop"\n case ["echo", *words]:\n return " ".join(words)\n case _:\n return "unknown"\n\nprint(describe(["move", 2, 3]))\nprint(describe(["exit"]))\nprint(describe(["echo", "hello", "python"]))\nprint(describe(["move", -1, 3]))\n```\n:::\n\n:::cell\nSequence patterns match by position. A guard after `if` adds a condition that must also be true.\n\n```python\ndef describe(command):\n match command:\n case ["move", x, y] if x >= 0 and y >= 0:\n return f"move to {x},{y}"\n case ["quit" | "exit"]:\n return "stop"\n case ["echo", *words]:\n return " ".join(words)\n case _:\n return "unknown"\n\nprint(describe(["move", 2, 3]))\n```\n\n```output\nmove to 2,3\n```\n:::\n\n:::cell\nAn OR pattern accepts several alternatives in one case. A star pattern captures the rest of a sequence.\n\n```python\nprint(describe(["exit"]))\nprint(describe(["echo", "hello", "python"]))\n```\n\n```output\nstop\nhello python\n```\n:::\n\n:::cell\nThe wildcard `_` catches values that did not match earlier cases. Here the guard rejects the negative coordinate.\n\n```python\nprint(describe(["move", -1, 3]))\n```\n\n```output\nunknown\n```\n:::\n\n:::note\n- Use `case _` as a wildcard fallback.\n- Guards refine a pattern after the structure matches.\n- OR patterns and star patterns keep shape-based branches compact.\n:::\n', 'args-and-kwargs.md': '+++\nslug = "args-and-kwargs"\ntitle = "Args and Kwargs"\nsection = "Functions"\nsummary = "*args collects extra positional arguments and **kwargs collects named ones."\ndoc_path = "/tutorial/controlflow.html#arbitrary-argument-lists"\n+++\n\n`*args` and `**kwargs` let a function accept flexible positional and keyword arguments. They are the function-definition counterpart to unpacking at a call site.\n\nThese parameters are useful for wrappers, decorators, logging helpers, and APIs that forward arguments to another function.\n\nThey should not replace clear signatures. If a function has a stable interface, explicit parameters document expectations better than a bag of arguments.\n\n:::program\n```python\ndef total(*numbers):\n return sum(numbers)\n\nprint(total(2, 3, 5))\n\n\ndef describe(**metadata):\n print(metadata)\n\ndescribe(owner="Ada", public=True)\n\n\ndef report(title, *items, **metadata):\n print(title)\n print(items)\n print(metadata)\n\nreport("scores", 10, 9, owner="Ada")\n```\n:::\n\n:::cell\n`*args` collects extra positional arguments into a tuple. This fits functions that naturally accept any number of similar values.\n\n```python\ndef total(*numbers):\n return sum(numbers)\n\nprint(total(2, 3, 5))\n```\n\n```output\n10\n```\n:::\n\n:::cell\n`**kwargs` collects named arguments into a dictionary. The names become string keys.\n\n```python\ndef describe(**metadata):\n print(metadata)\n\ndescribe(owner="Ada", public=True)\n```\n\n```output\n{\'owner\': \'Ada\', \'public\': True}\n```\n:::\n\n:::cell\nA function can combine explicit parameters, `*args`, and `**kwargs`. Put the flexible parts last so the fixed shape remains visible.\n\n```python\ndef report(title, *items, **metadata):\n print(title)\n print(items)\n print(metadata)\n\nreport("scores", 10, 9, owner="Ada")\n```\n\n```output\nscores\n(10, 9)\n{\'owner\': \'Ada\'}\n```\n:::\n\n:::note\n- Use these tools when a function naturally accepts a flexible shape.\n- Prefer explicit parameters when the accepted arguments are known and fixed.\n- `*args` is a tuple; `**kwargs` is a dictionary.\n:::\n', 'assertions.md': '+++\nslug = "assertions"\ntitle = "Assertions"\nsection = "Errors"\nsummary = "assert documents internal assumptions and fails loudly when they are false."\ndoc_path = "/reference/simple_stmts.html#the-assert-statement"\nsee_also = [\n "exceptions",\n "custom-exceptions",\n "type-hints",\n]\n+++\n\n`assert` checks an internal assumption. If the condition is false, Python raises `AssertionError` with an optional message.\n\nUse assertions for programmer assumptions, not for validating user input or external data. Input validation should raise ordinary exceptions that production code expects to handle.\n\nAssertions make invariants executable while keeping the successful path compact.\n\n:::program\n```python\ndef average(scores):\n assert scores, "scores must not be empty"\n return sum(scores) / len(scores)\n\nprint(average([8, 10]))\n\ntry:\n average([])\nexcept AssertionError as error:\n print(error)\n```\n:::\n\n:::cell\nWhen the assertion is true, execution continues normally. The assertion documents the function\'s internal expectation.\n\n```python\ndef average(scores):\n assert scores, "scores must not be empty"\n return sum(scores) / len(scores)\n\nprint(average([8, 10]))\n```\n\n```output\n9.0\n```\n:::\n\n:::cell\nWhen the assertion is false, Python raises `AssertionError`. This signals a broken assumption, not a normal recovery path.\n\n```python\ntry:\n average([])\nexcept AssertionError as error:\n print(error)\n```\n\n```output\nscores must not be empty\n```\n:::\n\n:::note\n- Use `assert` for internal invariants and debugging assumptions.\n- Use explicit exceptions for user input, files, network responses, and other expected failures.\n- Assertions can be disabled with Python optimization flags, so do not rely on them for security checks.\n:::\n', 'assignment-expressions.md': '+++\nslug = "assignment-expressions"\ntitle = "Assignment Expressions"\nsection = "Control Flow"\nsummary = "The walrus operator assigns a value inside an expression."\ndoc_path = "/reference/expressions.html#assignment-expressions"\nsee_also = [\n "conditionals",\n "while-loops",\n "variables",\n]\n+++\n\nThe assignment expression operator `:=` assigns a name while evaluating an expression. It is often called the walrus operator.\n\nUse it when computing a value and testing it are naturally one step. Avoid it when a separate assignment would make the code easier to read.\n\nThe boundary is readability: the walrus operator can remove duplication, but it should not hide important state changes.\n\n:::program\n```python\nmessages = ["hello", "", "python"]\n\nfor message in messages:\n if length := len(message):\n print(message, length)\n\nqueue = ["retry", "ok"]\nwhile (status := queue.pop(0)) != "ok":\n print(status)\nprint(status)\n```\n:::\n\n:::cell\nAn assignment expression can name a computed value while a condition tests it. Here empty strings are skipped because their length is zero.\n\n```python\nmessages = ["hello", "", "python"]\n\nfor message in messages:\n if length := len(message):\n print(message, length)\n```\n\n```output\nhello 5\npython 6\n```\n:::\n\n:::cell\nThe same idea works in loops that read state until a sentinel appears. The assignment and comparison stay together.\n\n```python\nqueue = ["retry", "ok"]\nwhile (status := queue.pop(0)) != "ok":\n print(status)\nprint(status)\n```\n\n```output\nretry\nok\n```\n:::\n\n:::note\n- `name := expression` assigns and evaluates to the assigned value.\n- Use it to avoid computing the same value twice.\n- Prefer a normal assignment when the expression becomes hard to scan.\n:::\n', 'async-await.md': '+++\nslug = "async-await"\ntitle = "Async Await"\nsection = "Async"\nsummary = "async def creates coroutines, and await pauses until awaitable work completes."\ndoc_path = "/library/asyncio-task.html"\nsee_also = [\n "async-iteration-and-context",\n "functions",\n "context-managers",\n]\n+++\n\n`async def` creates a coroutine function. Calling it creates a coroutine object; the body runs when an event loop awaits or schedules it.\n\n`await` pauses the current coroutine until another awaitable completes. This lets one event loop make progress on other work while a task waits for I/O.\n\nCloudflare Workers handlers are asynchronous, so understanding `await` is practical for fetch calls, bindings, and service interactions even when a small example uses `asyncio.sleep(0)` as a stand-in.\n\nThe alternative is ordinary `def` for work that completes immediately. Use async code for I/O-shaped waiting, not as a faster replacement for CPU-bound Python.\n\n:::program\n```python\nimport asyncio\n\nasync def fetch_title(slug):\n await asyncio.sleep(0)\n return slug.replace("-", " ").title()\n\nasync def main():\n title = await fetch_title("async-await")\n print(title)\n titles = await asyncio.gather(fetch_title("json"), fetch_title("datetime"))\n print(titles)\n\nasyncio.run(main())\n\n\nclass Session:\n async def __aenter__(self):\n print("open")\n return self\n\n async def __aexit__(self, *_):\n print("close")\n return False\n\n\nasync def stream():\n for slug in ["json", "datetime"]:\n await asyncio.sleep(0)\n yield slug\n\n\nasync def driver():\n async with Session():\n async for slug in stream():\n print(slug)\n\nasyncio.run(driver())\n```\n:::\n\n:::cell\nAn `async def` function returns a coroutine object when called. The function body has not produced its final result yet.\n\n```python\nimport asyncio\n\nasync def fetch_title(slug):\n await asyncio.sleep(0)\n return slug.replace("-", " ").title()\n\ncoroutine = fetch_title("async-await")\nprint(coroutine.__class__.__name__)\ncoroutine.close()\n```\n\n```output\ncoroutine\n```\n:::\n\n:::cell\nUse `await` inside another coroutine to get the eventual result. `asyncio.run()` starts an event loop for the top-level coroutine.\n\n```python\nasync def main():\n title = await fetch_title("async-await")\n print(title)\n\nasyncio.run(main())\n```\n\n```output\nAsync Await\n```\n:::\n\n:::cell\n`asyncio.gather()` awaits several awaitables and returns their results in order. This is the shape used when independent I/O operations can progress together.\n\n```python\nasync def main():\n titles = await asyncio.gather(fetch_title("json"), fetch_title("datetime"))\n print(titles)\n\nasyncio.run(main())\n```\n\n```output\n[\'Json\', \'Datetime\']\n```\n:::\n\n:::cell\n`async with` and `async for` are the asynchronous forms of context managers and iteration. A class implements `__aenter__`/`__aexit__` to act as an async context manager; an `async def` function with `yield` becomes an async generator. The dedicated [async iteration and context](/iteration/async-iteration-and-context) page explains the protocols in depth.\n\n```python\nclass Session:\n async def __aenter__(self):\n print("open")\n return self\n\n async def __aexit__(self, *_):\n print("close")\n return False\n\n\nasync def stream():\n for slug in ["json", "datetime"]:\n await asyncio.sleep(0)\n yield slug\n\n\nasync def driver():\n async with Session():\n async for slug in stream():\n print(slug)\n\nasyncio.run(driver())\n```\n\n```output\nopen\njson\ndatetime\nclose\n```\n:::\n\n:::note\n- Calling an async function creates a coroutine object.\n- `await` yields control until an awaitable completes.\n- Workers request handlers are async, so this pattern appears around fetches and bindings.\n- Prefer ordinary functions when there is no awaitable work to coordinate.\n:::\n', 'async-iteration-and-context.md': '+++\nslug = "async-iteration-and-context"\ntitle = "Async Iteration and Context"\nsection = "Async"\nsummary = "async for and async with consume asynchronous streams and cleanup protocols."\ndoc_path = "/reference/compound_stmts.html#async-for"\nsee_also = [\n "async-await",\n "iterators",\n "context-managers",\n]\n+++\n\n`async for` consumes an asynchronous iterator: a stream whose next value may require `await`. `async with` surrounds a block with asynchronous setup and cleanup.\n\nThese forms appear around network streams, database cursors, locks, and service clients where both iteration and cleanup may wait on I/O.\n\nUse ordinary `for` and `with` when producing the next value or cleaning up does not need to await anything.\n\nThe syntax mirrors `for` and `with`, but the protocol methods are asynchronous.\n\n:::program\n```python\nimport asyncio\n\nasync def titles():\n for slug in ["values", "async-await"]:\n await asyncio.sleep(0)\n yield slug.replace("-", " ").title()\n\nclass Session:\n async def __aenter__(self):\n print("open")\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n print("close")\n\nasync def main():\n async with Session():\n async for title in titles():\n print(title)\n\nasyncio.run(main())\n```\n:::\n\n:::cell\nAn async generator can `await` before yielding each value. `async for` consumes those values with the asynchronous iteration protocol.\n\n```python\nimport asyncio\n\nasync def titles():\n for slug in ["values", "async-await"]:\n await asyncio.sleep(0)\n yield slug.replace("-", " ").title()\n\nprint(titles.__name__)\n```\n\n```output\ntitles\n```\n:::\n\n:::cell\nAn async context manager defines `__aenter__` and `__aexit__`. `async with` awaits setup and cleanup around the block.\n\n```python\nclass Session:\n async def __aenter__(self):\n print("open")\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n print("close")\n\nprint(Session.__name__)\n```\n\n```output\nSession\n```\n:::\n\n:::cell\nThe top-level coroutine combines both protocols: open the async resource, then consume the async stream inside it.\n\n```python\nasync def main():\n async with Session():\n async for title in titles():\n print(title)\n\nasyncio.run(main())\n```\n\n```output\nopen\nValues\nAsync Await\nclose\n```\n:::\n\n:::note\n- `async for` consumes asynchronous iterators.\n- `async with` awaits asynchronous setup and cleanup.\n- These forms are common around I/O-shaped resources.\n:::\n', 'attribute-access.md': '+++\nslug = "attribute-access"\ntitle = "Attribute Access"\nsection = "Data Model"\nsummary = "Attribute hooks customize lookup, missing attributes, and assignment."\ndoc_path = "/reference/datamodel.html#customizing-attribute-access"\nsee_also = [\n "properties",\n "descriptors",\n "special-methods",\n "bound-and-unbound-methods",\n]\n+++\n\nAttribute access is usually simple: `obj.name` looks up an attribute. Python exposes hooks for the uncommon cases where lookup or assignment needs to be customized.\n\n`__getattr__` runs only when normal lookup fails, which makes it a safer hook for computed fallback attributes. `__setattr__` runs for every assignment, so it should be used sparingly and carefully.\n\nPrefer ordinary attributes and `@property` first. Reach for these hooks when an object is intentionally adapting another interface, validating all assignments, or exposing dynamic fields.\n\n:::program\n```python\nclass Settings:\n def __init__(self, values):\n self._values = dict(values)\n\n def __getattr__(self, name):\n try:\n return self._values[name]\n except KeyError as error:\n raise AttributeError(name) from error\n\n def __setattr__(self, name, value):\n if name.startswith("_"):\n object.__setattr__(self, name, value)\n else:\n self._values[name] = value\n\nsettings = Settings({"theme": "dark"})\nprint(settings.theme)\nsettings.volume = 7\nprint(settings._values["volume"])\n```\n:::\n\n:::cell\nNormal initialization still needs to set real attributes. Calling `object.__setattr__` avoids recursing through your own hook.\n\n```python\nclass Settings:\n def __init__(self, values):\n self._values = dict(values)\n\nsettings = Settings({"theme": "dark"})\nprint(settings._values)\n```\n\n```output\n{\'theme\': \'dark\'}\n```\n:::\n\n:::cell\n`__getattr__` runs only for missing attributes, so it can provide fallback lookup.\n\n```python\nclass Settings:\n def __init__(self, values):\n self._values = dict(values)\n\n def __getattr__(self, name):\n try:\n return self._values[name]\n except KeyError as error:\n raise AttributeError(name) from error\n\nsettings = Settings({"theme": "dark"})\nprint(settings.theme)\n```\n\n```output\ndark\n```\n:::\n\n:::cell\n`__setattr__` intercepts assignment. This example stores public names in the backing dictionary.\n\n```python\nclass Settings:\n def __init__(self, values):\n self._values = dict(values)\n\n def __setattr__(self, name, value):\n if name.startswith("_"):\n object.__setattr__(self, name, value)\n else:\n self._values[name] = value\n\nsettings = Settings({"theme": "dark"})\nsettings.volume = 7\nprint(settings._values["volume"])\n```\n\n```output\n7\n```\n:::\n\n:::note\n- `__getattr__` is narrower than `__getattribute__` because it handles only missing attributes.\n- `__setattr__` affects every assignment on the instance.\n- Use `property` or descriptors when the behavior is attached to a known attribute name.\n:::\n', 'booleans.md': '+++\nslug = "booleans"\ntitle = "Booleans"\nsection = "Basics"\nsummary = "Booleans represent truth values and combine with logical operators."\ndoc_path = "/library/stdtypes.html#boolean-type-bool"\n+++\n\nBooleans are the values `True` and `False`. They are produced by comparisons and combined with `and`, `or`, and `not`.\n\nPython\'s logical operators short-circuit. That means the right side is evaluated only when needed, which keeps guard checks efficient and safe.\n\nBooleans are also connected to truthiness: many objects can be tested in conditions even when they are not literally `True` or `False`.\n\n:::program\n```python\nlogged_in = True\nhas_permission = False\n\nprint(logged_in and has_permission)\nprint(logged_in or has_permission)\nprint(not has_permission)\n\nname = "Ada"\nprint(name == "Ada" and len(name) > 0)\n\nprint(isinstance(True, int))\nprint(True + True)\nprint(sum([True, True, False, True]))\n\ndef is_strict_int(value):\n return isinstance(value, int) and not isinstance(value, bool)\n\nprint(is_strict_int(True))\nprint(is_strict_int(1))\n```\n:::\n\n:::cell\nUse booleans for facts that are either true or false. Python spells the constants `True` and `False`.\n\nUse `and`, `or`, and `not` to combine truth values. These operators read like English and short-circuit when possible.\n\n```python\nlogged_in = True\nhas_permission = False\n\nprint(logged_in and has_permission)\nprint(logged_in or has_permission)\nprint(not has_permission)\n```\n\n```output\nFalse\nTrue\nTrue\n```\n:::\n\n:::cell\nComparisons produce booleans too, so they compose naturally with logical operators in conditions and validation checks.\n\n```python\nname = "Ada"\nprint(name == "Ada" and len(name) > 0)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\n`bool` is a subclass of `int`, which is occasionally a footgun. `True` behaves as `1` and `False` as `0` in arithmetic, and `isinstance(True, int)` is `True`. When a function must reject booleans, exclude them explicitly with `isinstance(value, int) and not isinstance(value, bool)`.\n\n```python\nprint(isinstance(True, int))\nprint(True + True)\nprint(sum([True, True, False, True]))\n\ndef is_strict_int(value):\n return isinstance(value, int) and not isinstance(value, bool)\n\nprint(is_strict_int(True))\nprint(is_strict_int(1))\n```\n\n```output\nTrue\n2\n3\nFalse\nTrue\n```\n:::\n\n:::note\n- Boolean constants are `True` and `False`, with capital letters.\n- `and` and `or` short-circuit: Python does not evaluate the right side if the left side already determines the result.\n- Prefer truthiness for containers and explicit comparisons when the exact boolean condition matters.\n- `bool` subclasses `int`; `isinstance(True, int)` is `True`. Exclude booleans explicitly when only "real" integers should pass.\n:::\n', 'bound-and-unbound-methods.md': '+++\nslug = "bound-and-unbound-methods"\ntitle = "Bound and Unbound Methods"\nsection = "Data Model"\nsummary = "instance.method binds self automatically; Class.method is a plain function."\ndoc_path = "/reference/datamodel.html#instance-methods"\nsee_also = [\n "classes",\n "attribute-access",\n "descriptors",\n "callable-objects",\n]\n+++\n\nWhen you write `instance.method`, Python returns a bound method — a callable that already remembers which instance to pass as `self`. When you write `Class.method`, you get the underlying function back, and calling it requires passing an instance yourself.\n\nThat distinction is why methods can be stored in collections, passed as callbacks, and called later without losing track of the object they belong to. Each bound method carries its own `__self__`, so two callables produced from two different instances stay independent even when their underlying function is the same.\n\nThe mechanism is the descriptor protocol: a function attached to a class implements `__get__`, and that hook turns attribute access on an instance into a bound method. The page does not need that detail to use methods, but it explains what is happening underneath.\n\n:::program\n```python\nclass Counter:\n def __init__(self, start=0):\n self.value = start\n\n def increment(self):\n self.value += 1\n return self.value\n\nbound_counter = Counter(10)\nm = bound_counter.increment\nprint(m.__self__ is bound_counter)\nprint(m())\nprint(m())\n\nunbound_counter = Counter(0)\nunbound = Counter.increment\nprint(type(unbound).__name__)\nprint(unbound(unbound_counter))\nprint(unbound(unbound_counter))\n\nhandlers = []\nfor _ in range(2):\n handlers.append(Counter().increment)\n\nprint(handlers[0]())\nprint(handlers[0]())\nprint(handlers[1]())\n\ndescriptor_counter = Counter(0)\nfunc = Counter.__dict__["increment"]\nprint(type(func).__name__)\nrebound = func.__get__(descriptor_counter, Counter)\nprint(type(rebound).__name__)\nprint(rebound.__self__ is descriptor_counter)\n```\n:::\n\n:::cell\n`instance.method` returns a bound method. The method already remembers the instance through `__self__`, so calling it does not require passing `self` again.\n\n```python\nclass Counter:\n def __init__(self, start=0):\n self.value = start\n\n def increment(self):\n self.value += 1\n return self.value\n\nbound_counter = Counter(10)\nm = bound_counter.increment\nprint(m.__self__ is bound_counter)\nprint(m())\nprint(m())\n```\n\n```output\nTrue\n11\n12\n```\n:::\n\n:::cell\n`Class.method` returns the underlying function — there is no `self` attached. Calling it requires passing the instance as the first argument explicitly. Using a fresh counter here makes the output independent of the previous cell.\n\n```python\nunbound_counter = Counter(0)\nunbound = Counter.increment\nprint(type(unbound).__name__)\nprint(unbound(unbound_counter))\nprint(unbound(unbound_counter))\n```\n\n```output\nfunction\n1\n2\n```\n:::\n\n:::cell\nBound methods are first-class values. They can be stored in lists, passed to other functions, and called later. Each bound method carries its own `__self__`, so two methods produced from two different instances stay independent.\n\n```python\nhandlers = []\nfor _ in range(2):\n handlers.append(Counter().increment)\n\nprint(handlers[0]())\nprint(handlers[0]())\nprint(handlers[1]())\n```\n\n```output\n1\n2\n1\n```\n:::\n\n:::cell\nThe binding is the descriptor protocol at work. The function lives on the class as a plain function; instance attribute access invokes `__get__`, which returns a bound method that knows the instance.\n\n```python\ndescriptor_counter = Counter(0)\nfunc = Counter.__dict__["increment"]\nprint(type(func).__name__)\nrebound = func.__get__(descriptor_counter, Counter)\nprint(type(rebound).__name__)\nprint(rebound.__self__ is descriptor_counter)\n```\n\n```output\nfunction\nmethod\nTrue\n```\n:::\n\n:::note\n- `instance.method` produces a bound method whose `__self__` is the instance.\n- `Class.method` produces the plain function and requires you to pass the instance.\n- Each bound method is its own object; storing one captures its instance.\n- The binding is implemented by the descriptor protocol on the function object.\n:::\n', 'break-and-continue.md': '+++\nslug = "break-and-continue"\ntitle = "Break and Continue"\nsection = "Control Flow"\nsummary = "break exits a loop early, while continue skips to the next iteration."\ndoc_path = "/tutorial/controlflow.html#break-and-continue-statements"\nsee_also = [\n "for-loops",\n "while-loops",\n "loop-else",\n]\n+++\n\n`break` and `continue` control the nearest enclosing loop. They exist for loops whose body discovers an early stop rule or an item-level skip rule.\n\nUse `continue` when the current item should not run the rest of the body. Use `break` when no later item should be processed.\n\nThe alternative is ordinary `if`/`else` nesting. Prefer `break` and `continue` when they keep the normal path flatter and easier to read.\n\n:::program\n```python\nnames = ["Ada", "", "Grace"]\nfor name in names:\n if not name:\n continue\n print(name)\n\ncommands = ["load", "save", "stop", "delete"]\nfor command in commands:\n if command == "stop":\n break\n print(command)\n```\n:::\n\n:::cell\n`continue` skips the rest of the current iteration. The empty name is ignored, and the loop moves on to the next value.\n\n```python\nnames = ["Ada", "", "Grace"]\nfor name in names:\n if not name:\n continue\n print(name)\n```\n\n```output\nAda\nGrace\n```\n:::\n\n:::cell\n`break` exits the loop immediately. The value after `stop` is never processed because the loop has already ended.\n\n```python\ncommands = ["load", "save", "stop", "delete"]\nfor command in commands:\n if command == "stop":\n break\n print(command)\n```\n\n```output\nload\nsave\n```\n:::\n\n:::note\n- `continue` skips to the next loop iteration.\n- `break` exits the nearest enclosing loop immediately.\n- Prefer plain `if`/`else` when the loop does not need early skip or early stop behavior.\n:::\n', 'bytes-and-bytearray.md': '+++\nslug = "bytes-and-bytearray"\ntitle = "Bytes and Bytearray"\nsection = "Basics"\nsummary = "bytes and bytearray store binary data, not Unicode text."\ndoc_path = "/library/stdtypes.html#binary-sequence-types-bytes-bytearray-memoryview"\nsee_also = [\n "strings",\n "literals",\n "networking",\n]\n+++\n\n`str` stores Unicode text. `bytes` stores raw byte values. The boundary matters whenever text leaves Python for a file, network protocol, subprocess, or binary format.\n\nEncoding turns text into bytes with a named encoding such as UTF-8. Decoding turns bytes back into text. The lengths can differ because one Unicode character may require several bytes.\n\nUse immutable `bytes` for stable binary data and `bytearray` when the bytes must be changed in place.\n\n:::program\n```python\ntext = "café"\ndata = text.encode("utf-8")\n\nprint(data)\nprint(len(text), len(data))\nprint(data.decode("utf-8"))\nprint(data[0])\n\npacket = bytearray(b"py")\npacket[0] = ord("P")\nprint(packet)\n```\n:::\n\n:::cell\nEncode text when an external boundary needs bytes. UTF-8 uses one byte for ASCII characters and more than one byte for many other characters.\n\n```python\ntext = "café"\ndata = text.encode("utf-8")\nprint(data)\nprint(len(text), len(data))\n```\n\n```output\nb\'caf\\xc3\\xa9\'\n4 5\n```\n:::\n\n:::cell\nDecode bytes when the program needs text again. The decoder must match the encoding used at the boundary.\n\n```python\nprint(data.decode("utf-8"))\n```\n\n```output\ncafé\n```\n:::\n\n:::cell\nIndexing a `bytes` object returns an integer byte value, not a one-character `bytes` object.\n\n```python\nprint(data[0])\n```\n\n```output\n99\n```\n:::\n\n:::cell\n`bytes` is immutable. Use `bytearray` when binary data must be changed in place.\n\n```python\npacket = bytearray(b"py")\npacket[0] = ord("P")\nprint(packet)\n```\n\n```output\nbytearray(b\'Py\')\n```\n:::\n\n:::note\n- Encode text when an external boundary needs bytes.\n- Decode bytes when you want text again.\n- Indexing `bytes` returns integers from 0 to 255.\n- Use `bytearray` when binary data must be changed in place.\n:::\n', 'callable-objects.md': '+++\nslug = "callable-objects"\ntitle = "Callable Objects"\nsection = "Data Model"\nsummary = "__call__ lets an instance behave like a function while keeping state."\ndoc_path = "/reference/datamodel.html#object.__call__"\nsee_also = [\n "functions",\n "closures",\n "callable-types",\n "bound-and-unbound-methods",\n]\n+++\n\nFunctions are not the only callable objects in Python. Any instance can be called with parentheses when its class defines `__call__`.\n\nCallable objects are useful when behavior needs remembered configuration or evolving state. A closure can do this too; a class is often clearer when the state has multiple fields or needs named methods.\n\nThe tradeoff is ceremony. Use a function for simple behavior, a closure for small captured state, and a callable object when naming the state improves the interface.\n\n:::program\n```python\nclass Multiplier:\n def __init__(self, factor):\n self.factor = factor\n self.calls = 0\n\n def __call__(self, value):\n self.calls += 1\n return value * self.factor\n\ndouble = Multiplier(2)\nprint(double(5))\nprint(double(7))\nprint(double.calls)\n```\n:::\n\n:::cell\nA callable object starts as ordinary state stored on an instance.\n\n```python\nclass Multiplier:\n def __init__(self, factor):\n self.factor = factor\n self.calls = 0\n\ndouble = Multiplier(2)\nprint(double.factor)\n```\n\n```output\n2\n```\n:::\n\n:::cell\n`__call__` makes the instance usable with function-call syntax.\n\n```python\nclass Multiplier:\n def __init__(self, factor):\n self.factor = factor\n self.calls = 0\n\n def __call__(self, value):\n self.calls += 1\n return value * self.factor\n\ndouble = Multiplier(2)\nprint(double(5))\nprint(double(7))\n```\n\n```output\n10\n14\n```\n:::\n\n:::cell\nBecause the callable is still an object, it can remember state across calls.\n\n```python\nprint(double.calls)\n```\n\n```output\n2\n```\n:::\n\n:::note\n- `callable(obj)` checks whether an object can be called.\n- Callable objects are good for named, stateful behavior.\n- Prefer plain functions when no instance state is needed.\n:::\n', 'callable-types.md': '+++\nslug = "callable-types"\ntitle = "Callable Types"\nsection = "Types"\nsummary = "Callable annotations describe functions passed as values."\ndoc_path = "/library/typing.html#annotating-callable-objects"\nsee_also = [\n "functions",\n "callable-objects",\n "protocols",\n]\n+++\n\nCallable annotations describe values that can be called like functions. They are useful when a function accepts a callback, strategy, predicate, or transformation.\n\n`Callable[[int], int]` says how the callback will be called: one integer argument, integer result. The annotation helps tools and readers, while runtime still only needs an object that is actually callable.\n\nUse `Callable` for simple call shapes. Use a protocol when the callback needs named attributes, overloaded signatures, or a more descriptive interface.\n\n:::program\n```python\nfrom collections.abc import Callable\n\n\ndef apply_twice(value: int, func: Callable[[int], int]) -> int:\n return func(func(value))\n\n\ndef add_one(number: int) -> int:\n return number + 1\n\nclass Doubler:\n def __call__(self, number: int) -> int:\n return number * 2\n\nprint(apply_twice(3, add_one))\nprint(apply_twice(3, Doubler()))\nprint(callable(add_one), callable(Doubler()))\n```\n:::\n\n:::cell\nUse `Callable[[Arg], Return]` for function-shaped values. The callback is passed in and called by the receiving function.\n\n```python\nfrom collections.abc import Callable\n\n\ndef apply_twice(value: int, func: Callable[[int], int]) -> int:\n return func(func(value))\n\n\ndef add_one(number: int) -> int:\n return number + 1\n\nprint(apply_twice(3, add_one))\n```\n\n```output\n5\n```\n:::\n\n:::cell\nCallable annotations are structural: an object with `__call__` can also satisfy the shape.\n\n```python\nclass Doubler:\n def __call__(self, number: int) -> int:\n return number * 2\n\nprint(apply_twice(3, Doubler()))\n```\n\n```output\n12\n```\n:::\n\n:::cell\nRuntime callability is a separate question from static annotation. `callable()` checks whether Python can call the object.\n\n```python\nprint(callable(add_one), callable(Doubler()))\n```\n\n```output\nTrue True\n```\n:::\n\n:::note\n- Use `Callable[[Arg], Return]` for simple function-shaped values.\n- The annotation documents how the callback will be called.\n- For complex call signatures, protocols can be clearer.\n:::\n', 'casts-and-any.md': '+++\nslug = "casts-and-any"\ntitle = "Casts and Any"\nsection = "Types"\nsummary = "Any and cast are escape hatches for places static analysis cannot prove."\ndoc_path = "/library/typing.html#typing.cast"\nsee_also = [\n "type-hints",\n "runtime-type-checks",\n "typed-dicts",\n]\n+++\n\n`Any` and `cast()` are escape hatches. They are useful at messy boundaries where a type checker cannot prove what a value is, but they also remove protection when overused.\n\n`Any` tells static tools to stop checking most operations on a value. `cast(T, value)` tells the type checker to treat a value as `T`, but it returns the same runtime object unchanged.\n\nPrefer narrowing with runtime checks when possible. Use `cast()` when another invariant already proves the type and the checker cannot see that proof.\n\n:::program\n```python\nfrom typing import Any, cast\n\nraw: Any = {"score": "98"}\nscore_text = cast(dict[str, str], raw)["score"]\nscore = int(score_text)\n\nprint(score + 2)\nprint(cast(list[int], raw) is raw)\nprint(type(raw).__name__)\n```\n:::\n\n:::cell\n`Any` disables most static checking for a value. The runtime object is still whatever value was actually assigned.\n\n```python\nfrom typing import Any, cast\n\nraw: Any = {"score": "98"}\nscore_text = cast(dict[str, str], raw)["score"]\nscore = int(score_text)\nprint(score + 2)\n```\n\n```output\n100\n```\n:::\n\n:::cell\n`cast()` does not convert or validate the value. It returns the same object at runtime.\n\n```python\nprint(cast(list[int], raw) is raw)\nprint(type(raw).__name__)\n```\n\n```output\nTrue\ndict\n```\n:::\n\n:::cell\nA real runtime check narrows by inspecting the value. This is safer when the input is untrusted.\n\n```python\nvalue: object = {"score": "98"}\nif isinstance(value, dict):\n print(value["score"])\n```\n\n```output\n98\n```\n:::\n\n:::note\n- `Any` disables most static checking for a value.\n- `cast()` tells the type checker to trust you without changing the runtime object.\n- Prefer narrowing with checks when possible.\n:::\n', 'classes.md': '+++\nslug = "classes"\ntitle = "Classes"\nsection = "Classes"\nsummary = "Classes bundle data and behavior into new object types."\ndoc_path = "/tutorial/classes.html"\nsee_also = [\n "inheritance-and-super",\n "classmethods-and-staticmethods",\n "bound-and-unbound-methods",\n "dataclasses",\n]\n+++\n\nClasses define new object types by bundling data with behavior. They are useful when several values and operations belong together and should travel as one object.\n\nThe alternative is often a dictionary plus separate functions. That is fine for loose data, but a class gives the data a stable API and keeps behavior next to the state it changes.\n\n`__init__` initializes each instance, and methods receive the instance as `self`. Separate instances keep separate state because each object has its own attributes.\n\n:::program\n```python\nclass Counter:\n step = 1\n\n def __init__(self, start=0):\n self.value = start\n\n def increment(self, amount=1):\n self.value += amount\n return self.value\n\nfirst = Counter()\nsecond = Counter(10)\n\nprint(first.value)\nprint(second.value)\nprint(first.increment())\nprint(second.increment(5))\nprint(first.step)\nCounter.step = 5\nprint(second.step)\n\nclass Cart:\n items = []\n\n def add(self, item):\n self.items.append(item)\n\nshared_a = Cart()\nshared_b = Cart()\nshared_a.add("apple")\nprint(shared_b.items)\n\nclass FixedCart:\n def __init__(self):\n self.items = []\n\n def add(self, item):\n self.items.append(item)\n\nown_a = FixedCart()\nown_b = FixedCart()\nown_a.add("apple")\nprint(own_b.items)\n```\n:::\n\n:::cell\nDefine a class when data and behavior should travel together. The initializer gives each object its starting state.\n\n```python\nclass Counter:\n def __init__(self, start=0):\n self.value = start\n\nfirst = Counter()\nsecond = Counter(10)\nprint(first.value)\nprint(second.value)\n```\n\n```output\n0\n10\n```\n:::\n\n:::cell\nMethods are functions attached to the class. `self` is the particular object receiving the method call, so separate instances keep separate state.\n\n```python\nclass Counter:\n def __init__(self, start=0):\n self.value = start\n\n def increment(self, amount=1):\n self.value += amount\n return self.value\n\nfirst = Counter()\nsecond = Counter(10)\nprint(first.increment())\nprint(second.increment(5))\n```\n\n```output\n1\n15\n```\n:::\n\n:::cell\nA name defined directly on the class body is a class attribute, shared by every instance. Reading falls back to the class when the instance has no attribute of that name; assigning to the class itself changes the value for every instance at once.\n\n```python\nclass Counter:\n step = 1\n\n def __init__(self, start=0):\n self.value = start\n\nfirst = Counter()\nsecond = Counter()\nprint(first.step)\nCounter.step = 5\nprint(second.step)\n```\n\n```output\n1\n5\n```\n:::\n\n:::cell\nA mutable class attribute is shared mutable state — the classic footgun. Define per-instance containers in `__init__` so each object owns its own copy.\n\n```python\nclass Cart:\n items = []\n\n def add(self, item):\n self.items.append(item)\n\nshared_a = Cart()\nshared_b = Cart()\nshared_a.add("apple")\nprint(shared_b.items)\n\nclass FixedCart:\n def __init__(self):\n self.items = []\n\n def add(self, item):\n self.items.append(item)\n\nown_a = FixedCart()\nown_b = FixedCart()\nown_a.add("apple")\nprint(own_b.items)\n```\n\n```output\n[\'apple\']\n[]\n```\n:::\n\n:::note\n- `self` is the instance the method is operating on.\n- `__init__` initializes each new object.\n- Class attributes are shared across instances; instance attributes belong to one object.\n- Put mutable defaults in `__init__`, not on the class body.\n- Use classes when behavior belongs with state; use dictionaries for looser structured data.\n:::\n', 'classmethods-and-staticmethods.md': '+++\nslug = "classmethods-and-staticmethods"\ntitle = "Classmethods and Staticmethods"\nsection = "Classes"\nsummary = "Three method shapes: instance, class, and static — each receives a different first argument."\ndoc_path = "/library/functions.html#classmethod"\nsee_also = [\n "classes",\n "decorators",\n "inheritance-and-super",\n]\n+++\n\nA regular method receives the instance as `self`. `@classmethod` makes a method receive the class as `cls` instead, which is the standard shape for alternate constructors. `@staticmethod` removes the implicit first argument entirely, leaving a plain function attached to the class for namespacing.\n\nThe pressure that justifies the decorators is name organization. `Date.from_string("2026-05-09")` reads better than a free-floating `parse_date` function, and `Date.is_leap_year(2024)` keeps the helper next to the class it belongs to even when the helper does not need any class state.\n\nPick instance methods when the work depends on instance state, classmethods when an alternate constructor or class-level operation is the right shape, and staticmethods when the function only happens to live near a class.\n\n:::program\n```python\nclass Date:\n def __init__(self, year, month, day):\n self.year = year\n self.month = month\n self.day = day\n\n def display(self):\n return f"{self.year}-{self.month:02d}-{self.day:02d}"\n\n @classmethod\n def from_string(cls, text):\n year, month, day = (int(part) for part in text.split("-"))\n return cls(year, month, day)\n\n @staticmethod\n def is_leap_year(year):\n return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)\n\ntoday = Date(2026, 5, 9)\nprint(today.display())\n\nlater = Date.from_string("2026-12-31")\nprint(later.display())\n\nprint(Date.is_leap_year(2024))\nprint(Date.is_leap_year(2025))\n\nclass Demo:\n def instance_method(self):\n return type(self).__name__\n\n @classmethod\n def class_method(cls):\n return cls.__name__\n\n @staticmethod\n def static_method():\n return "no receiver"\n\nprint(Demo().instance_method())\nprint(Demo.class_method())\nprint(Demo.static_method())\n```\n:::\n\n:::cell\nAn instance method receives the instance as `self` and reads its state. This is the default and the right shape when the work depends on a particular object\'s data.\n\n```python\nclass Date:\n def __init__(self, year, month, day):\n self.year = year\n self.month = month\n self.day = day\n\n def display(self):\n return f"{self.year}-{self.month:02d}-{self.day:02d}"\n\ntoday = Date(2026, 5, 9)\nprint(today.display())\n```\n\n```output\n2026-05-09\n```\n:::\n\n:::cell\n`@classmethod` makes the method receive the class itself as `cls`. The canonical use is an alternate constructor that parses some other input format and calls `cls(...)`. Because `cls` is the actual class, subclasses calling the same method get an instance of their own type.\n\n```python\nclass Date:\n def __init__(self, year, month, day):\n self.year = year\n self.month = month\n self.day = day\n\n @classmethod\n def from_string(cls, text):\n year, month, day = (int(part) for part in text.split("-"))\n return cls(year, month, day)\n\nlater = Date.from_string("2026-12-31")\nprint(later.year, later.month, later.day)\n```\n\n```output\n2026 12 31\n```\n:::\n\n:::cell\n`@staticmethod` strips the implicit first argument. The function lives on the class for namespacing — like `Date.is_leap_year(2024)` — but does not touch any instance or class state.\n\n```python\nclass Date:\n @staticmethod\n def is_leap_year(year):\n return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)\n\nprint(Date.is_leap_year(2024))\nprint(Date.is_leap_year(2025))\n```\n\n```output\nTrue\nFalse\n```\n:::\n\n:::cell\nSide by side: instance methods receive the instance, classmethods receive the class, staticmethods receive nothing. Classmethods and staticmethods can be called on either the class or an instance.\n\n```python\nclass Demo:\n def instance_method(self):\n return type(self).__name__\n\n @classmethod\n def class_method(cls):\n return cls.__name__\n\n @staticmethod\n def static_method():\n return "no receiver"\n\nprint(Demo().instance_method())\nprint(Demo.class_method())\nprint(Demo.static_method())\n```\n\n```output\nDemo\nDemo\nno receiver\n```\n:::\n\n:::note\n- Instance methods need an instance; classmethods and staticmethods can be called on the class.\n- Use `@classmethod` for alternate constructors and class-level operations that respect subclassing.\n- Use `@staticmethod` only when a function is truly independent of instance and class state but still belongs in the class\'s namespace.\n- A free function is often the right answer when neither decorator applies.\n:::\n', 'closures.md': '+++\nslug = "closures"\ntitle = "Closures"\nsection = "Functions"\nsummary = "Inner functions can remember values from an enclosing scope."\ndoc_path = "/reference/executionmodel.html#binding-of-names"\n+++\n\nA closure is a function that remembers names from the scope where it was created. This lets you configure behavior once and call it later.\n\nEach call to the outer function creates a separate remembered environment. That is why `double` and `triple` can share the same code but keep different factors.\n\nClosures are a foundation for decorators, callbacks, and small function factories.\n\n:::program\n```python\ndef make_multiplier(factor):\n def multiply(value):\n return value * factor\n return multiply\n\ndouble = make_multiplier(2)\nprint(double(5))\n\ntriple = make_multiplier(3)\nprint(triple(5))\n\nlate = []\nfor i in range(3):\n late.append(lambda: i)\nprint([f() for f in late])\n\nbound = []\nfor i in range(3):\n bound.append(lambda i=i: i)\nprint([f() for f in bound])\n```\n:::\n\n:::cell\nDefine a function inside another function when the inner behavior needs to remember setup from the outer call. The returned function keeps access to `factor`.\n\n```python\ndef make_multiplier(factor):\n def multiply(value):\n return value * factor\n return multiply\n\ndouble = make_multiplier(2)\nprint(double(5))\n```\n\n```output\n10\n```\n:::\n\n:::cell\nCalling the outer function again creates a separate closure. `triple` uses the same inner code, but remembers a different `factor`.\n\n```python\ntriple = make_multiplier(3)\nprint(triple(5))\n```\n\n```output\n15\n```\n:::\n\n:::cell\nClosures bind names, not values. Lambdas defined in a loop all reference the same loop variable, so calling them later sees its final value. Capture the value at definition time by binding it as a default argument — `lambda i=i: i` — so each closure remembers its own `i`.\n\n```python\nlate = []\nfor i in range(3):\n late.append(lambda: i)\nprint([f() for f in late])\n\nbound = []\nfor i in range(3):\n bound.append(lambda i=i: i)\nprint([f() for f in bound])\n```\n\n```output\n[2, 2, 2]\n[0, 1, 2]\n```\n:::\n\n:::note\n- A closure keeps access to names from the scope where the inner function was created.\n- Each call to the outer function can create a separate remembered environment.\n- Closures are useful for callbacks, small factories, and decorators.\n- Closures bind names, not values; capture loop variables with `lambda x=x: ...` to freeze them at definition time.\n:::\n', 'collections-module.md': '+++\nslug = "collections-module"\ntitle = "Collections Module"\nsection = "Collections"\nsummary = "collections provides specialized containers for common data shapes."\ndoc_path = "/library/collections.html"\n+++\n\ncollections provides specialized containers for common data shapes. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nfrom collections import Counter, defaultdict, deque, namedtuple\n\ncounts = Counter("banana")\nprint(counts.most_common(2))\n\ngroups = defaultdict(list)\nfor name, team in [("Ada", "red"), ("Grace", "blue"), ("Lin", "red")]:\n groups[team].append(name)\nprint(dict(groups))\n\nqueue = deque(["first"])\nqueue.append("second")\nprint(queue.popleft())\n\nPoint = namedtuple("Point", "x y")\nprint(Point(2, 3).x)\n```\n:::\n\n:::cell\nUse `Counter` when counting is the data shape.\n\n```python\nfrom collections import Counter, defaultdict, deque, namedtuple\n\ncounts = Counter("banana")\nprint(counts.most_common(2))\n\ngroups = defaultdict(list)\nfor name, team in [("Ada", "red"), ("Grace", "blue"), ("Lin", "red")]:\n groups[team].append(name)\nprint(dict(groups))\n\nqueue = deque(["first"])\nqueue.append("second")\nprint(queue.popleft())\n\nPoint = namedtuple("Point", "x y")\nprint(Point(2, 3).x)\n```\n\n```output\n[(\'a\', 3), (\'n\', 2)]\n{\'red\': [\'Ada\', \'Lin\'], \'blue\': [\'Grace\']}\nfirst\n2\n```\n:::\n\n:::note\n- Use `Counter` when counting is the data shape.\n- Use `defaultdict` when grouping values by key.\n- Use `deque` for efficient queue operations and `namedtuple` for lightweight named records.\n:::\n', 'comprehension-patterns.md': '+++\nslug = "comprehension-patterns"\ntitle = "Comprehension Patterns"\nsection = "Collections"\nsummary = "Comprehensions can use multiple for clauses and filters when the shape stays clear."\ndoc_path = "/tutorial/datastructures.html#list-comprehensions"\nsee_also = [\n "comprehensions",\n "generator-expressions",\n "for-loops",\n]\n+++\n\nComprehensions can contain more than one `for` clause and more than one `if` filter. The clauses are read in the same order as nested loops.\n\nUse these forms only while the shape remains easy to scan. If a comprehension starts needing several names, comments, or branches, an explicit loop is usually better.\n\nNested comprehensions build concrete collections immediately, just like simpler list, dict, and set comprehensions.\n\n:::program\n```python\ncolors = ["red", "blue"]\nsizes = ["S", "M"]\nvariants = [(color, size) for color in colors for size in sizes]\nprint(variants)\n\nnumbers = range(10)\nfiltered = [n for n in numbers if n % 2 == 0 if n > 2]\nprint(filtered)\n```\n:::\n\n:::cell\nMultiple `for` clauses behave like nested loops. The leftmost `for` is the outer loop, and the next `for` runs inside it.\n\n```python\ncolors = ["red", "blue"]\nsizes = ["S", "M"]\nvariants = [(color, size) for color in colors for size in sizes]\nprint(variants)\n```\n\n```output\n[(\'red\', \'S\'), (\'red\', \'M\'), (\'blue\', \'S\'), (\'blue\', \'M\')]\n```\n:::\n\n:::cell\nMultiple `if` clauses filter values. They are useful for simple conditions, but an explicit loop is clearer when the rules need names or explanation.\n\n```python\nnumbers = range(10)\nfiltered = [n for n in numbers if n % 2 == 0 if n > 2]\nprint(filtered)\n```\n\n```output\n[4, 6, 8]\n```\n:::\n\n:::note\n- Read comprehension clauses from left to right.\n- Multiple `for` clauses act like nested loops.\n- Prefer an explicit loop when the comprehension stops being obvious.\n:::\n', 'comprehensions.md': '+++\nslug = "comprehensions"\ntitle = "Comprehensions"\nsection = "Collections"\nsummary = "Comprehensions build collections by mapping and filtering iterables."\ndoc_path = "/tutorial/datastructures.html#list-comprehensions"\n+++\n\nComprehensions are expression forms for building concrete collections from iterables. Read them from left to right: produce this value, for each item, optionally only when a condition is true.\n\nThey are best for direct transformations where the expression is still easy to scan. When the work needs several statements or names, an explicit loop is usually clearer.\n\nList, dictionary, and set comprehensions are eager: they build collections immediately. Generator expressions use similar syntax to stream values later and are covered in the Iteration section.\n\n:::program\n```python\nnames = ["ada", "guido", "grace"]\ntitled = [name.title() for name in names]\nprint(titled)\n\nscores = {"Ada": 10, "Guido": 8, "Grace": 10}\nhigh_scores = {name: score for name, score in scores.items() if score >= 10}\nprint(high_scores)\n\nunique_scores = {score for score in scores.values()}\nprint(unique_scores)\n```\n:::\n\n:::cell\nA list comprehension maps each input item to one output item. This one calls `title()` for every name and collects the results in a new list.\n\n```python\nnames = ["ada", "guido", "grace"]\ntitled = [name.title() for name in names]\nprint(titled)\n```\n\n```output\n[\'Ada\', \'Guido\', \'Grace\']\n```\n:::\n\n:::cell\nAdd an `if` clause when only some items should appear. A dictionary comprehension can transform key/value pairs while preserving the dictionary shape.\n\n```python\nscores = {"Ada": 10, "Guido": 8, "Grace": 10}\nhigh_scores = {name: score for name, score in scores.items() if score >= 10}\nprint(high_scores)\n```\n\n```output\n{\'Ada\': 10, \'Grace\': 10}\n```\n:::\n\n:::cell\nA set comprehension keeps only unique results. Here two people have the same score, so the resulting set has two values.\n\n```python\nunique_scores = {score for score in scores.values()}\nprint(unique_scores)\n```\n\n```output\n{8, 10}\n```\n:::\n\n:::note\n- The left side says what to produce; the `for` clause says where values come from.\n- Use an `if` clause for simple filters.\n- List, dict, and set comprehensions build concrete collections immediately.\n- Switch to a loop when the transformation needs multiple steps or explanations.\n:::\n', 'conditionals.md': '+++\nslug = "conditionals"\ntitle = "Conditionals"\nsection = "Control Flow"\nsummary = "if, elif, and else choose which block runs."\ndoc_path = "/tutorial/controlflow.html#if-statements"\n+++\n\n`if`, `elif`, and `else` let a program choose one path based on a condition. Python uses indentation to show which statements belong to each branch.\n\nConditions use Python truthiness: booleans work directly, and many objects such as empty lists or empty strings are considered false. Order branches from most specific to most general.\n\nUse `elif` to keep one decision flat instead of nested. Use Python\'s ternary expression only when you are choosing between two values.\n\n:::program\n```python\ntemperature = 72\n\nif temperature < 60:\n print("cold")\nelif temperature < 80:\n print("comfortable")\nelse:\n print("hot")\n\nitems = ["coat", "hat"]\nif items:\n print(f"packing {len(items)} items")\n\nstatus = "ok" if temperature < 90 else "danger"\nprint(status)\n```\n:::\n\n:::cell\nStart with the value that the branches will test. A conditional is only useful when the branch condition is visible and meaningful.\n\nUse `if`, `elif`, and `else` for one ordered choice. Python tests the branches from top to bottom and runs only the first matching block.\n\n```python\ntemperature = 72\n\nif temperature < 60:\n print("cold")\nelif temperature < 80:\n print("comfortable")\nelse:\n print("hot")\n```\n\n```output\ncomfortable\n```\n:::\n\n:::cell\nTruthiness is part of conditional flow. Empty collections are false, so `if items:` reads as “if there is anything to work with.”\n\n```python\nitems = ["coat", "hat"]\nif items:\n print(f"packing {len(items)} items")\n```\n\n```output\npacking 2 items\n```\n:::\n\n:::cell\nUse the ternary expression when you are choosing a value. If either side needs multiple statements, use a normal `if` block instead.\n\n```python\nstatus = "ok" if temperature < 90 else "danger"\nprint(status)\n```\n\n```output\nok\n```\n:::\n\n:::note\n- Python has no mandatory parentheses around conditions; the colon and indentation define the block.\n- Comparison operators such as `<` and `==` can be chained, as in `0 < value < 10`.\n- Keep branch bodies short; move larger work into functions so the decision remains easy to scan.\n:::\n', 'constants.md': '+++\nslug = "constants"\ntitle = "Constants"\nsection = "Basics"\nsummary = "Python uses naming conventions for values that should not change."\ndoc_path = "/tutorial/classes.html#python-scopes-and-namespaces"\n+++\n\nPython has no `const` keyword for ordinary variables. Instead, modules use all-caps names to mark values that should be treated as constants by convention.\n\nThe interpreter will not stop rebinding, but the convention is important API communication. Readers understand that `MAX_RETRIES` is configuration, not loop state.\n\nNamed constants remove magic values from code and give repeated literals one place to change.\n\n:::program\n```python\nMAX_RETRIES = 3\nAPI_VERSION = "2026-05"\n\nfor attempt in range(1, MAX_RETRIES + 1):\n print(f"attempt {attempt} of {MAX_RETRIES}")\n\nprint(API_VERSION)\n```\n:::\n\n:::cell\nPython does not have a `const` declaration like Go or Rust. Instead, modules use all-caps names for values callers should treat as fixed.\n\nThe interpreter will still let you rebind the name, but the convention is strong enough that readers understand the design intent.\n\n```python\nMAX_RETRIES = 3\nAPI_VERSION = "2026-05"\n\nfor attempt in range(1, MAX_RETRIES + 1):\n print(f"attempt {attempt} of {MAX_RETRIES}")\n```\n\n```output\nattempt 1 of 3\nattempt 2 of 3\nattempt 3 of 3\n```\n:::\n\n:::cell\nConstants are useful for configuration values that should be named once and reused instead of repeated as magic literals.\n\n```python\nprint(API_VERSION)\n```\n\n```output\n2026-05\n```\n:::\n\n:::note\n- Python has no `const` keyword for ordinary names.\n- All-caps names such as `MAX_RETRIES` communicate that a value is intended to stay fixed.\n:::\n', 'container-protocols.md': '+++\nslug = "container-protocols"\ntitle = "Container Protocols"\nsection = "Data Model"\nsummary = "Container methods connect objects to indexing, membership, and item assignment."\ndoc_path = "/reference/datamodel.html#emulating-container-types"\nsee_also = [\n "lists",\n "dicts",\n "special-methods",\n]\n+++\n\nContainer protocols let a class behave like the collection it represents. Instead of inventing method names such as `has()` or `lookup()`, the object can support `in`, indexing, and assignment.\n\nThe key methods are small and familiar: `__contains__` powers `in`, `__getitem__` powers `obj[key]`, and `__setitem__` powers `obj[key] = value`. Add only the operations the object can honestly support.\n\nThis keeps the public interface aligned with Python\'s built-in containers. Callers can use the same syntax for custom records, caches, tables, and sequence-like objects.\n\n:::program\n```python\nclass Scores:\n def __init__(self):\n self._scores = {}\n\n def __contains__(self, name):\n return name in self._scores\n\n def __getitem__(self, name):\n return self._scores[name]\n\n def __setitem__(self, name, score):\n self._scores[name] = score\n\nscores = Scores()\nscores["Ada"] = 98\nprint("Ada" in scores)\nprint(scores["Ada"])\n```\n:::\n\n:::cell\n`__setitem__` gives assignment syntax to a custom container.\n\n```python\nclass Scores:\n def __init__(self):\n self._scores = {}\n\n def __setitem__(self, name, score):\n self._scores[name] = score\n\nscores = Scores()\nscores["Ada"] = 98\nprint(scores._scores)\n```\n\n```output\n{\'Ada\': 98}\n```\n:::\n\n:::cell\n`__contains__` answers membership tests written with `in`.\n\n```python\nclass Scores:\n def __init__(self):\n self._scores = {"Ada": 98}\n\n def __contains__(self, name):\n return name in self._scores\n\nscores = Scores()\nprint("Ada" in scores)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\n`__getitem__` connects bracket lookup to your internal storage.\n\n```python\nclass Scores:\n def __init__(self):\n self._scores = {"Ada": 98}\n\n def __getitem__(self, name):\n return self._scores[name]\n\nscores = Scores()\nprint(scores["Ada"])\n```\n\n```output\n98\n```\n:::\n\n:::note\n- Implement the narrowest container protocol your object needs.\n- Use `KeyError` and `IndexError` consistently with built-in containers.\n- If a plain `dict` or `list` is enough, prefer it over a custom container.\n:::\n', 'context-managers.md': '+++\nslug = "context-managers"\ntitle = "Context Managers"\nsection = "Data Model"\nsummary = "with ensures setup and cleanup happen together."\ndoc_path = "/reference/datamodel.html#context-managers"\nsee_also = [\n "exceptions",\n "special-methods",\n "descriptors",\n]\n+++\n\nContext managers define setup and cleanup around a block of code. The `with` statement guarantees that cleanup runs when the block exits, even when an exception is raised.\n\nThe protocol is powered by `__enter__` and `__exit__`. The `contextlib.contextmanager` decorator is a concise way to write the same idea as a generator when a full class would be noisy.\n\nProduction code often uses `with` for files, locks, transactions, temporary state, and resources that need reliable release.\n\n:::program\n```python\nfrom contextlib import contextmanager\n\nclass Tag:\n def __init__(self, name):\n self.name = name\n\n def __enter__(self):\n print(f"<{self.name}>")\n return self\n\n def __exit__(self, exc_type, exc, tb):\n print(f"")\n return False\n\n@contextmanager\ndef tag(name):\n print(f"<{name}>")\n try:\n yield\n finally:\n print(f"")\n\nwith Tag("section"):\n print("content")\n\ntry:\n with tag("error"):\n raise ValueError("boom")\nexcept ValueError:\n print("handled")\n```\n:::\n\n:::cell\nA class-based context manager implements `__enter__` and `__exit__`. The value returned by `__enter__` is bound by `as` when the `with` statement uses it.\n\n```python\nclass Tag:\n def __init__(self, name):\n self.name = name\n\n def __enter__(self):\n print(f"<{self.name}>")\n return self\n\n def __exit__(self, exc_type, exc, tb):\n print(f"")\n return False\n\nwith Tag("section"):\n print("content")\n```\n\n```output\n
    \ncontent\n
    \n```\n:::\n\n:::cell\n`contextlib.contextmanager` writes the same setup/cleanup shape as a generator. Code before `yield` is setup, and code after `yield` is cleanup.\n\n```python\nfrom contextlib import contextmanager\n\n@contextmanager\ndef tag(name):\n print(f"<{name}>")\n try:\n yield\n finally:\n print(f"")\n\nwith tag("note"):\n print("body")\n```\n\n```output\n\nbody\n\n```\n:::\n\n:::cell\nCleanup still runs when the block raises. Returning `False` from `__exit__`, or letting a generator context manager re-raise, allows the exception to keep propagating.\n\n```python\ntry:\n with tag("error"):\n raise ValueError("boom")\nexcept ValueError:\n print("handled")\n```\n\n```output\n\n\nhandled\n```\n:::\n\n:::note\n- Files, locks, and temporary state commonly use context managers.\n- `__enter__` and `__exit__` power the protocol.\n- Use `finally` when cleanup must happen after errors too.\n- Returning true from `__exit__` suppresses an exception; do that only intentionally.\n:::\n', 'copying-collections.md': '+++\nslug = "copying-collections"\ntitle = "Copying Collections"\nsection = "Collections"\nsummary = "Copies can duplicate a container while still sharing nested objects."\ndoc_path = "/library/copy.html"\n+++\n\nCopies can duplicate a container while still sharing nested objects. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nimport copy\n\nrows = [["Ada"], ["Grace"]]\nshallow = rows.copy()\ndeep = copy.deepcopy(rows)\n\nrows[0].append("Lovelace")\n\nprint(shallow)\nprint(deep)\nprint(rows[0] is shallow[0])\nprint(rows[0] is deep[0])\n```\n:::\n\n:::cell\nA shallow copy makes a new outer container.\n\n```python\nimport copy\n\nrows = [["Ada"], ["Grace"]]\nshallow = rows.copy()\ndeep = copy.deepcopy(rows)\n\nrows[0].append("Lovelace")\n\nprint(shallow)\nprint(deep)\nprint(rows[0] is shallow[0])\nprint(rows[0] is deep[0])\n```\n\n```output\n[[\'Ada\', \'Lovelace\'], [\'Grace\']]\n[[\'Ada\'], [\'Grace\']]\nTrue\nFalse\n```\n:::\n\n:::note\n- A shallow copy makes a new outer container.\n- Nested objects are still shared by a shallow copy.\n- Use `copy.deepcopy()` only when nested independence is required.\n:::\n', 'csv-data.md': '+++\nslug = "csv-data"\ntitle = "CSV Data"\nsection = "Standard Library"\nsummary = "csv reads and writes row-shaped text data."\ndoc_path = "/library/csv.html"\n+++\n\ncsv reads and writes row-shaped text data. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nimport csv\nimport io\n\ntext = "name,score\\nAda,98\\nGrace,95\\n"\nreader = csv.DictReader(io.StringIO(text))\nrows = list(reader)\n\nprint(rows[0]["name"])\nprint(sum(int(row["score"]) for row in rows))\n\noutput = io.StringIO()\nwriter = csv.DictWriter(output, fieldnames=["name", "passed"])\nwriter.writeheader()\nwriter.writerow({"name": "Ada", "passed": True})\nprint(output.getvalue().splitlines()[1])\n```\n:::\n\n:::cell\nUse `DictReader` when column names should become dictionary keys.\n\n```python\nimport csv\nimport io\n\ntext = "name,score\\nAda,98\\nGrace,95\\n"\nreader = csv.DictReader(io.StringIO(text))\nrows = list(reader)\n\nprint(rows[0]["name"])\nprint(sum(int(row["score"]) for row in rows))\n\noutput = io.StringIO()\nwriter = csv.DictWriter(output, fieldnames=["name", "passed"])\nwriter.writeheader()\nwriter.writerow({"name": "Ada", "passed": True})\nprint(output.getvalue().splitlines()[1])\n```\n\n```output\nAda\n193\nAda,True\n```\n:::\n\n:::note\n- Use `DictReader` when column names should become dictionary keys.\n- CSV fields arrive as text, so convert numbers explicitly.\n- `DictWriter` writes dictionaries back to row-shaped text.\n:::\n', 'custom-exceptions.md': '+++\nslug = "custom-exceptions"\ntitle = "Custom Exceptions"\nsection = "Errors"\nsummary = "Custom exception classes name failures that belong to your domain."\ndoc_path = "/tutorial/errors.html#user-defined-exceptions"\n+++\n\nCustom exceptions give names to failures in your problem domain. A named exception is easier to catch and explain than a generic error with only a string message.\n\nRaise the custom exception at the point where the invalid state is discovered. Include a message for the specific occurrence.\n\nCatch custom exceptions at the boundary where recovery makes sense, such as returning an error response or asking for corrected input.\n\n:::program\n```python\nclass EmptyCartError(Exception):\n pass\n\nprint(EmptyCartError.__name__)\n\n\ndef checkout(items):\n if not items:\n raise EmptyCartError("cart is empty")\n return "paid"\n\nprint(checkout(["book"]))\n\ntry:\n checkout([])\nexcept EmptyCartError as error:\n print(error)\n```\n:::\n\n:::cell\nCreate a custom exception when a failure has a name in your problem domain. The class can be empty at first.\n\n```python\nclass EmptyCartError(Exception):\n pass\n\nprint(EmptyCartError.__name__)\n```\n\n```output\nEmptyCartError\n```\n:::\n\n:::cell\nRaise the custom exception where the invalid state is detected. Normal inputs still follow the ordinary success path.\n\n```python\ndef checkout(items):\n if not items:\n raise EmptyCartError("cart is empty")\n return "paid"\n\nprint(checkout(["book"]))\n```\n\n```output\npaid\n```\n:::\n\n:::cell\nCallers can catch the precise error type without accidentally catching unrelated failures.\n\n```python\ntry:\n checkout([])\nexcept EmptyCartError as error:\n print(error)\n```\n\n```output\ncart is empty\n```\n:::\n\n:::note\n- Subclass `Exception` for errors callers are expected to catch.\n- A custom exception name can be clearer than reusing a generic `ValueError` everywhere.\n- Catch custom exceptions at a boundary that can recover or report clearly.\n:::\n', 'dataclasses.md': '+++\nslug = "dataclasses"\ntitle = "Dataclasses"\nsection = "Classes"\nsummary = "dataclass generates common class methods for data containers."\ndoc_path = "/library/dataclasses.html"\nsee_also = [\n "structured-data-shapes",\n "classes",\n "type-hints",\n]\n+++\n\n`dataclass` is a standard-library decorator for classes that mainly store data. It generates methods such as `__init__` and `__repr__` from type-annotated fields.\n\nDataclasses reduce boilerplate while keeping classes explicit. They are a good fit for simple records, configuration objects, and values passed between layers.\n\nType annotations define fields. Defaults work like normal class attributes and appear in the generated initializer.\n\n:::program\n```python\nfrom dataclasses import dataclass\n\n@dataclass\nclass User:\n name: str\n active: bool = True\n\nuser = User("Ada")\nprint(user)\nprint(user.name)\n\ninactive = User("Guido", active=False)\nprint(inactive)\nprint(inactive.active)\n```\n:::\n\n:::cell\nA dataclass uses annotations to define fields. Python generates an initializer, so the class can be constructed without writing `__init__` by hand.\n\n```python\nfrom dataclasses import dataclass\n\n@dataclass\nclass User:\n name: str\n active: bool = True\n\nuser = User("Ada")\nprint(user)\n```\n\n```output\nUser(name=\'Ada\', active=True)\n```\n:::\n\n:::cell\nThe generated instance still exposes ordinary attributes. A dataclass is a regular class with useful methods filled in.\n\n```python\nprint(user.name)\n```\n\n```output\nAda\n```\n:::\n\n:::cell\nDefaults can be overridden by keyword. The generated representation includes the field names, which is useful during debugging.\n\n```python\ninactive = User("Guido", active=False)\nprint(inactive)\nprint(inactive.active)\n```\n\n```output\nUser(name=\'Guido\', active=False)\nFalse\n```\n:::\n\n:::note\n- Type annotations define dataclass fields.\n- Dataclasses generate methods but remain normal Python classes.\n- Use `field()` for advanced defaults such as per-instance lists or dictionaries.\n:::\n', 'datetime.md': '+++\nslug = "datetime"\ntitle = "Dates and Times"\nsection = "Standard Library"\nsummary = "datetime represents dates, times, durations, formatting, and parsing."\ndoc_path = "/library/datetime.html"\n+++\n\nThe `datetime` module covers several related ideas: `date` for calendar days, `time` for clock times, `datetime` for both together, and `timedelta` for durations.\n\nTimezone-aware datetimes avoid ambiguity in real systems. `timezone.utc` is a clear default for examples because output stays stable and portable.\n\nUse ISO formatting for interchange, `strftime()` for display, and parsing helpers such as `fromisoformat()` to turn text back into datetime objects.\n\n:::program\n```python\nfrom datetime import date, datetime, time, timedelta, timezone\n\nrelease_day = date(2026, 5, 4)\nmeeting_time = time(12, 30)\ncreated_at = datetime.combine(release_day, meeting_time, tzinfo=timezone.utc)\n\nprint(release_day.isoformat())\nprint(meeting_time.isoformat())\nprint(created_at.isoformat())\n\nexpires_at = created_at + timedelta(days=7, hours=2)\nprint(expires_at.isoformat())\n\nprint(created_at.strftime("%Y-%m-%d %H:%M %Z"))\niso_text = "2026-05-04T12:30:00+00:00"\nparsed = datetime.fromisoformat(iso_text)\nprint(parsed == created_at)\n```\n:::\n\n:::cell\nThe `datetime` module separates calendar dates, clock times, combined datetimes, and durations. Import the types you need explicitly.\n\nUse `date` for a calendar day and `time` for a time of day. Combine them into a timezone-aware `datetime` when you mean an instant.\n\n`isoformat()` produces stable machine-readable text. It is a good default for examples, APIs, and logs.\n\n```python\nfrom datetime import date, datetime, time, timedelta, timezone\n\nrelease_day = date(2026, 5, 4)\nmeeting_time = time(12, 30)\ncreated_at = datetime.combine(release_day, meeting_time, tzinfo=timezone.utc)\n\nprint(release_day.isoformat())\nprint(meeting_time.isoformat())\nprint(created_at.isoformat())\n```\n\n```output\n2026-05-04\n12:30:00\n2026-05-04T12:30:00+00:00\n```\n:::\n\n:::cell\nUse `timedelta` for durations. Adding one to a `datetime` produces another `datetime` without manually changing calendar fields.\n\n```python\nexpires_at = created_at + timedelta(days=7, hours=2)\nprint(expires_at.isoformat())\n```\n\n```output\n2026-05-11T14:30:00+00:00\n```\n:::\n\n:::cell\nUse `strftime()` for human-facing formatting and `fromisoformat()` when reading ISO 8601 text back into a `datetime`.\n\n```python\nprint(created_at.strftime("%Y-%m-%d %H:%M %Z"))\niso_text = "2026-05-04T12:30:00+00:00"\nparsed = datetime.fromisoformat(iso_text)\nprint(parsed == created_at)\n```\n\n```output\n2026-05-04 12:30 UTC\nTrue\n```\n:::\n\n:::note\n- Use timezone-aware datetimes for instants that cross system or user boundaries.\n- Use `date` for calendar days, `time` for clock times, `datetime` for both, and `timedelta` for durations.\n- Prefer ISO 8601 strings for interchange; use `strftime` for human-facing display.\n:::\n', 'decorators.md': '+++\nslug = "decorators"\ntitle = "Decorators"\nsection = "Functions"\nsummary = "Decorators wrap or register functions using @ syntax."\ndoc_path = "/glossary.html#term-decorator"\nsee_also = [\n "closures",\n "functions",\n "callable-types",\n "classmethods-and-staticmethods",\n]\n+++\n\nA decorator is a callable that receives a function and returns a replacement. The `@` syntax applies that transformation at function definition time.\n\nDecorators are common in frameworks because they can register handlers or add behavior while keeping the decorated function focused on the core action.\n\n`@decorator` is shorthand for rebinding a function to the decorator\'s return value. Production wrappers usually use `functools.wraps` so debugging, help text, and framework introspection still see the original function metadata.\n\n:::program\n```python\nfrom functools import wraps\n\n\ndef loud(func):\n @wraps(func)\n def wrapper(name):\n return func(name).upper()\n return wrapper\n\n\ndef greet(name):\n return f"hello {name}"\n\nmanual_greet = loud(greet)\nprint(manual_greet("python"))\n\n@loud\ndef welcome(name):\n """Return a welcome message."""\n return f"welcome {name}"\n\nprint(welcome("workers"))\nprint(welcome.__name__)\n```\n:::\n\n:::cell\nA decorator is just a function that takes a function and returns another callable. Applying it manually shows the wrapping step.\n\n```python\nfrom functools import wraps\n\n\ndef loud(func):\n @wraps(func)\n def wrapper(name):\n return func(name).upper()\n return wrapper\n\n\ndef greet(name):\n return f"hello {name}"\n\nmanual_greet = loud(greet)\nprint(manual_greet("python"))\n```\n\n```output\nHELLO PYTHON\n```\n:::\n\n:::cell\nThe `@loud` syntax performs the same rebinding at definition time. After decoration, `welcome` refers to the wrapper returned by `loud`.\n\n```python\n@loud\ndef welcome(name):\n """Return a welcome message."""\n return f"welcome {name}"\n\nprint(welcome("workers"))\n```\n\n```output\nWELCOME WORKERS\n```\n:::\n\n:::cell\n`functools.wraps` copies useful metadata from the original function onto the wrapper.\n\n```python\nprint(welcome.__name__)\nprint(welcome.__doc__)\n```\n\n```output\nwelcome\nReturn a welcome message.\n```\n:::\n\n:::note\n- `@decorator` is shorthand for assigning `func = decorator(func)`.\n- Decorators can wrap, replace, or register functions.\n- Use `functools.wraps` in production wrappers that should preserve metadata.\n:::\n', 'delete-statements.md': '+++\nslug = "delete-statements"\ntitle = "Delete Statements"\nsection = "Data Model"\nsummary = "del removes bindings, items, and attributes rather than producing a value."\ndoc_path = "/reference/simple_stmts.html#the-del-statement"\nsee_also = [\n "variables",\n "dicts",\n "mutability",\n]\n+++\n\n`del` removes a binding or an item. It is a statement, not a function, and it does not return the removed value.\n\nUse `del name` when a name should no longer be bound. Use `del mapping[key]` or `del sequence[index]` when mutating a container by removing one part.\n\nThis is different from assigning `None`: `None` is still a value, while `del` removes the binding or slot.\n\n:::program\n```python\nprofile = {"name": "Ada", "temporary": True}\ndel profile["temporary"]\nprint(profile)\n\nitems = ["a", "b", "c"]\ndel items[1]\nprint(items)\n\nvalue = "cached"\ndel value\nprint("value" in locals())\n```\n:::\n\n:::cell\nDeleting a dictionary key mutates the dictionary. The key is gone; it has not been set to `None`.\n\n```python\nprofile = {"name": "Ada", "temporary": True}\ndel profile["temporary"]\nprint(profile)\n```\n\n```output\n{\'name\': \'Ada\'}\n```\n:::\n\n:::cell\nDeleting a list item removes that position and shifts later items left.\n\n```python\nitems = ["a", "b", "c"]\ndel items[1]\nprint(items)\n```\n\n```output\n[\'a\', \'c\']\n```\n:::\n\n:::cell\nDeleting a name removes the binding from the current namespace. It is different from rebinding the name to `None`.\n\n```python\nvalue = "cached"\ndel value\nprint("value" in locals())\n```\n\n```output\nFalse\n```\n:::\n\n:::note\n- `del` removes bindings or container entries.\n- Assign `None` when absence should remain an explicit value.\n- Use container methods such as `pop()` when you need the removed value back.\n:::\n', 'descriptors.md': '+++\nslug = "descriptors"\ntitle = "Descriptors"\nsection = "Data Model"\nsummary = "Descriptors customize attribute access through __get__, __set__, or __delete__."\ndoc_path = "/howto/descriptor.html"\nsee_also = [\n "attribute-access",\n "properties",\n "bound-and-unbound-methods",\n]\n+++\n\nDescriptors customize attribute access through __get__, __set__, or __delete__. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nclass Positive:\n def __set_name__(self, owner, name):\n self.private_name = "_" + name\n\n def __get__(self, obj, owner):\n return getattr(obj, self.private_name)\n\n def __set__(self, obj, value):\n if value <= 0:\n raise ValueError("must be positive")\n setattr(obj, self.private_name, value)\n\nclass Product:\n price = Positive()\n\n def __init__(self, price):\n self.price = price\n\nitem = Product(10)\nprint(item.price)\ntry:\n item.price = -1\nexcept ValueError as error:\n print(error)\n```\n:::\n\n:::cell\nA descriptor object lives on the class.\n\n```python\nclass Positive:\n def __set_name__(self, owner, name):\n self.private_name = "_" + name\n\n def __get__(self, obj, owner):\n return getattr(obj, self.private_name)\n\n def __set__(self, obj, value):\n if value <= 0:\n raise ValueError("must be positive")\n setattr(obj, self.private_name, value)\n\nclass Product:\n price = Positive()\n\n def __init__(self, price):\n self.price = price\n\nitem = Product(10)\nprint(item.price)\ntry:\n item.price = -1\nexcept ValueError as error:\n print(error)\n```\n\n```output\n10\nmust be positive\n```\n:::\n\n:::note\n- A descriptor object lives on the class.\n- Attribute access on instances calls descriptor methods.\n- Properties, methods, and many ORMs build on the descriptor protocol.\n:::\n', 'dicts.md': '+++\nslug = "dicts"\ntitle = "Dictionaries"\nsection = "Collections"\nsummary = "Dictionaries map keys to values for records, lookup, and structured data."\ndoc_path = "/tutorial/datastructures.html#dictionaries"\n+++\n\nDictionaries are Python\'s built-in mapping type. They exist for data where names or keys are more meaningful than numeric positions: records, lookup tables, counters, and JSON-like payloads.\n\nUse direct indexing when a key is required. Use `get()` when absence is expected and the code has a reasonable fallback.\n\nUnlike lists, dictionaries answer “what value belongs to this key?” rather than “what value is at this position?” Iterating with `items()` keeps each key next to its value.\n\n:::program\n```python\nprofile = {"name": "Ada", "language": "Python"}\nprofile["year"] = 1843\nprint(profile["name"])\nprint(profile.get("timezone", "UTC"))\n\nscores = {"Ada": 10, "Grace": 9}\nprint(scores["Grace"])\nprint(scores.get("Guido", 0))\n\nfor name, score in scores.items():\n print(f"{name}: {score}")\n\ninventory = {"apple": 0, "pear": 3, "plum": 0}\nfor name in list(inventory.keys()):\n if inventory[name] == 0:\n del inventory[name]\nprint(inventory)\n```\n:::\n\n:::cell\nUse a dictionary as a small record when fields have names. Direct indexing communicates that the key is required, while `get()` communicates that a missing key has a fallback.\n\n```python\nprofile = {"name": "Ada", "language": "Python"}\nprofile["year"] = 1843\nprint(profile["name"])\nprint(profile.get("timezone", "UTC"))\n```\n\n```output\nAda\nUTC\n```\n:::\n\n:::cell\nUse a dictionary as a lookup table when keys identify values. This is different from a list, where numeric position is the lookup key.\n\n```python\nscores = {"Ada": 10, "Grace": 9}\nprint(scores["Grace"])\nprint(scores.get("Guido", 0))\n```\n\n```output\n9\n0\n```\n:::\n\n:::cell\nUse `items()` when the loop needs both keys and values. It avoids looping over keys and then indexing back into the dictionary.\n\n```python\nfor name, score in scores.items():\n print(f"{name}: {score}")\n```\n\n```output\nAda: 10\nGrace: 9\n```\n:::\n\n:::cell\nMutating a dictionary while iterating it raises `RuntimeError`. Snapshot the keys with `list(d.keys())` (or build a list of changes and apply them after the loop) so the iteration sees a stable view.\n\n```python\ninventory = {"apple": 0, "pear": 3, "plum": 0}\nfor name in list(inventory.keys()):\n if inventory[name] == 0:\n del inventory[name]\nprint(inventory)\n```\n\n```output\n{\'pear\': 3}\n```\n:::\n\n:::note\n- Dictionaries preserve insertion order in modern Python.\n- Use `get()` when a missing key has a reasonable default.\n- Use direct indexing when a missing key should be treated as an error.\n- Snapshot keys with `list(d.keys())` before deleting items in a loop; mutating during iteration raises `RuntimeError`.\n:::\n', 'enums.md': '+++\nslug = "enums"\ntitle = "Enums"\nsection = "Types"\nsummary = "Enum defines symbolic names for a fixed set of values."\ndoc_path = "/library/enum.html"\n+++\n\n`Enum` defines a fixed set of named values. This makes states and modes easier to read than raw strings scattered through a program.\n\nEach enum member has a name and a value. Comparing enum members is explicit and helps avoid typos that plain strings would allow.\n\nUse enums when a value must be one of a small known set: statuses, modes, directions, roles, and similar choices.\n\n:::program\n```python\nfrom enum import Enum\n\nclass Status(Enum):\n PENDING = "pending"\n DONE = "done"\n\ncurrent = Status.PENDING\nprint(current.name)\nprint(current.value)\nprint(current is Status.PENDING)\nprint(current == "pending")\n```\n:::\n\n:::cell\nAn enum member has a symbolic name and an underlying value. The symbolic name is what readers usually care about in code.\n\n```python\nfrom enum import Enum\n\nclass Status(Enum):\n PENDING = "pending"\n DONE = "done"\n\ncurrent = Status.PENDING\nprint(current.name)\nprint(current.value)\n```\n\n```output\nPENDING\npending\n```\n:::\n\n:::cell\nCompare enum members with enum members, not with raw strings. This keeps the set of valid states explicit.\n\n```python\nprint(current is Status.PENDING)\nprint(current == "pending")\n```\n\n```output\nTrue\nFalse\n```\n:::\n\n:::note\n- Enums make states and choices explicit.\n- Members have names and values.\n- Comparing enum members avoids string typo bugs.\n- Prefer raw strings for open-ended text; prefer enums for a closed set of named choices.\n:::\n', 'equality-and-identity.md': '+++\nslug = "equality-and-identity"\ntitle = "Equality and Identity"\nsection = "Data Model"\nsummary = "== compares values, while is compares object identity."\ndoc_path = "/reference/expressions.html#is-not"\n+++\n\nPython separates equality from identity. Equality asks whether two objects should be considered the same value, while identity asks whether two names point to the same object.\n\nThis distinction matters for mutable containers because two equal lists can still be independent objects. Mutating one should not imply mutating the other unless they share identity.\n\nThe `is` operator is best reserved for identity checks against singletons such as `None`. For ordinary values, `==` is the comparison readers expect.\n\n:::program\n```python\nleft = [1, 2, 3]\nright = [1, 2, 3]\nprint(left == right)\nprint(left is right)\n\nsame = left\nsame.append(4)\nprint(left)\nprint(same is left)\n\nvalue = None\nprint(value is None)\n\nsmall_a = 100\nsmall_b = 100\nprint(small_a is small_b)\n\nbig_a = int("1000")\nbig_b = int("1000")\nprint(big_a is big_b)\nprint(big_a == big_b)\n```\n:::\n\n:::cell\nEqual containers can be different objects. `==` compares list contents, while `is` checks whether both names refer to the same list object.\n\n```python\nleft = [1, 2, 3]\nright = [1, 2, 3]\nprint(left == right)\nprint(left is right)\n```\n\n```output\nTrue\nFalse\n```\n:::\n\n:::cell\nIdentity matters when objects are mutable. `same` is another name for `left`, so mutating through one name changes the object seen through the other.\n\n```python\nsame = left\nsame.append(4)\nprint(left)\nprint(same is left)\n```\n\n```output\n[1, 2, 3, 4]\nTrue\n```\n:::\n\n:::cell\nUse `is` for singleton identity checks such as `None`. This asks whether the value is the one special `None` object.\n\n```python\nvalue = None\nprint(value is None)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\n`is` for integers is unreliable because CPython caches small integers (roughly `-5` to `256`) but not larger ones. Two equal large integers can be different objects. Use `==` for value comparisons; reserve `is` for singletons.\n\n```python\nsmall_a = 100\nsmall_b = 100\nprint(small_a is small_b)\n\nbig_a = int("1000")\nbig_b = int("1000")\nprint(big_a is big_b)\nprint(big_a == big_b)\n```\n\n```output\nTrue\nFalse\nTrue\n```\n:::\n\n:::note\n- Use `==` for ordinary value comparisons.\n- Use `is` primarily for identity checks against singletons such as `None`.\n- Equal mutable containers can still be independent objects.\n- Never use `is` to compare numbers; CPython\'s small-integer cache makes the result an implementation detail.\n:::\n', 'exception-chaining.md': '+++\nslug = "exception-chaining"\ntitle = "Exception Chaining"\nsection = "Errors"\nsummary = "raise from preserves the original cause when translating exceptions."\ndoc_path = "/tutorial/errors.html#exception-chaining"\nsee_also = [\n "exceptions",\n "custom-exceptions",\n "assertions",\n]\n+++\n\nException chaining connects a higher-level error to the lower-level exception that caused it. The syntax is `raise NewError(...) from error`.\n\nUse chaining when translating implementation details into a domain-specific error while preserving the original cause for debugging.\n\nThis is different from hiding the original exception. The caller can catch the domain error, and tooling can still inspect `__cause__`.\n\n:::program\n```python\nclass ConfigError(Exception):\n pass\n\n\ndef read_port(text):\n try:\n return int(text)\n except ValueError as error:\n raise ConfigError("port must be a number") from error\n\nprint(ConfigError.__name__)\n\ntry:\n read_port("abc")\nexcept ConfigError as error:\n print(error)\n print(type(error.__cause__).__name__)\n```\n:::\n\n:::cell\nCatch the low-level exception where it happens, then raise a domain-specific exception from it.\n\n```python\nclass ConfigError(Exception):\n pass\n\n\ndef read_port(text):\n try:\n return int(text)\n except ValueError as error:\n raise ConfigError("port must be a number") from error\n\nprint(ConfigError.__name__)\n```\n\n```output\nConfigError\n```\n:::\n\n:::cell\nThe caller handles the domain error. The original `ValueError` remains available as `__cause__`.\n\n```python\ntry:\n read_port("abc")\nexcept ConfigError as error:\n print(error)\n print(type(error.__cause__).__name__)\n```\n\n```output\nport must be a number\nValueError\n```\n:::\n\n:::note\n- Use `raise ... from error` when translating exceptions across a boundary.\n- The new exception\'s `__cause__` points to the original exception.\n- Chaining keeps user-facing errors clear without losing debugging context.\n:::\n', 'exception-groups.md': '+++\nslug = "exception-groups"\ntitle = "Exception Groups"\nsection = "Errors"\nsummary = "except* handles matching exceptions inside an ExceptionGroup."\ndoc_path = "/tutorial/errors.html#raising-and-handling-multiple-unrelated-exceptions"\nsee_also = [\n "exceptions",\n "exception-chaining",\n "async-await",\n]\n+++\n\n`ExceptionGroup` represents several unrelated exceptions raised together. `except*` exists for code that may receive multiple failures at once, especially concurrent work.\n\nUse ordinary `except` for one exception. Use `except*` only when the value being handled is an exception group and each matching subgroup needs its own handling.\n\nEach `except*` clause receives a smaller exception group containing the matching exceptions.\n\n:::program\n```python\nerrors = ExceptionGroup(\n "batch failed",\n [ValueError("bad port"), TypeError("bad mode")],\n)\nprint(len(errors.exceptions))\n\ntry:\n raise errors\nexcept* ValueError as group:\n print(type(group).__name__)\n print(group.exceptions[0])\nexcept* TypeError as group:\n print(group.exceptions[0])\n```\n:::\n\n:::cell\nAn exception group bundles several exception objects. This is different from an ordinary exception because more than one failure is present.\n\n```python\nerrors = ExceptionGroup(\n "batch failed",\n [ValueError("bad port"), TypeError("bad mode")],\n)\nprint(len(errors.exceptions))\n```\n\n```output\n2\n```\n:::\n\n:::cell\n`except*` handles matching members of the group. The `ValueError` handler sees the value error, and the `TypeError` handler sees the type error.\n\n```python\ntry:\n raise errors\nexcept* ValueError as group:\n print(type(group).__name__)\n print(group.exceptions[0])\nexcept* TypeError as group:\n print(group.exceptions[0])\n```\n\n```output\nExceptionGroup\nbad port\nbad mode\n```\n:::\n\n:::note\n- `except*` is for `ExceptionGroup`, not ordinary single exceptions.\n- Each `except*` clause handles matching members of the group.\n- Exception groups often appear around concurrent work.\n:::\n', 'exceptions.md': '+++\nslug = "exceptions"\ntitle = "Exceptions"\nsection = "Errors"\nsummary = "Use try, except, else, and finally to separate success, recovery, and cleanup."\ndoc_path = "/tutorial/errors.html"\n+++\n\nExceptions represent errors or unusual conditions that interrupt normal control flow. `try` marks the operation that may fail, and `except` handles a specific failure where recovery makes sense.\n\nKeep the successful path separate from the recovery path. `else` runs only when no exception was raised, while `finally` runs either way for cleanup or bookkeeping.\n\nUse exceptions when an operation cannot produce a valid result. Prefer ordinary conditionals for expected branches that are not errors.\n\nCatch specific exceptions whenever possible. A broad catch can hide programming mistakes, while a targeted `ValueError` handler documents exactly what failure is expected.\n\n:::program\n```python\ndef parse_int(text):\n return int(text)\n\nfor text in ["42", "python"]:\n try:\n number = parse_int(text)\n except ValueError:\n print(f"{text}: invalid")\n else:\n print(f"{text}: {number}")\n finally:\n print(f"checked {text}")\n\n\ndef safe_parse_broken(text):\n try:\n return int(text)\n except Exception:\n return None\n\ndef safe_parse_fixed(text):\n try:\n return int(text)\n except ValueError:\n return None\n\nprint(safe_parse_broken("42"))\nprint(safe_parse_fixed("42"))\n```\n:::\n\n:::cell\nWhen no exception is raised, the `else` block runs. Keeping success in `else` makes the `try` block contain only the operation that might fail.\n\n```python\ndef parse_int(text):\n return int(text)\n\ntext = "42"\ntry:\n number = parse_int(text)\nexcept ValueError:\n print(f"{text}: invalid")\nelse:\n print(f"{text}: {number}")\nfinally:\n print(f"checked {text}")\n```\n\n```output\n42: 42\nchecked 42\n```\n:::\n\n:::cell\nWhen parsing fails, `int()` raises `ValueError`. Catching that specific exception makes the expected recovery path explicit.\n\n```python\ntext = "python"\ntry:\n number = parse_int(text)\nexcept ValueError:\n print(f"{text}: invalid")\nelse:\n print(f"{text}: {number}")\nfinally:\n print(f"checked {text}")\n```\n\n```output\npython: invalid\nchecked python\n```\n:::\n\n:::cell\nBare `except:` and broad `except Exception:` swallow far more than the failure you meant to handle, including `KeyboardInterrupt` (bare) and most programming bugs (broad). Catch the specific class — `ValueError` here — so unexpected failures still surface.\n\n```python\ndef safe_parse_broken(text):\n try:\n return int(text)\n except Exception:\n return None\n\ndef safe_parse_fixed(text):\n try:\n return int(text)\n except ValueError:\n return None\n\nprint(safe_parse_broken("42"))\nprint(safe_parse_fixed("42"))\n```\n\n```output\n42\n42\n```\n:::\n\n:::note\n- Catch the most specific exception you can.\n- `else` is for success code that should run only if the `try` block did not fail.\n- `finally` runs whether the operation succeeded or failed.\n- Avoid bare `except:` and broad `except Exception:` — they hide bugs and absorb signals like `KeyboardInterrupt`.\n:::\n', 'for-loops.md': '+++\nslug = "for-loops"\ntitle = "For Loops"\nsection = "Control Flow"\nsummary = "for iterates over any iterable object."\ndoc_path = "/tutorial/controlflow.html#for-statements"\n+++\n\nPython for loops iterate over values from an iterable. This is different from languages where for primarily means incrementing a numeric counter.\n\nrange() is itself an iterable that produces numbers lazily. Use it when you need a sequence of integers, but prefer direct iteration when you already have a collection.\n\nBlocks are defined by indentation. range(3) yields 0, 1, and 2.\n\n:::program\n```python\nfor name in ["Ada", "Grace", "Guido"]:\n print(name)\n\nfor number in range(3):\n print(number)\n```\n:::\n\n:::cell\nPython for loops iterate over values from an iterable. This is different from languages where for primarily means incrementing a numeric counter.\n\n```python\nfor name in ["Ada", "Grace", "Guido"]:\n print(name)\n```\n\n```output\nAda\nGrace\nGuido\n```\n:::\n\n:::cell\nrange() is itself an iterable that produces numbers lazily. Use it when you need a sequence of integers, but prefer direct iteration when you already have a collection.\n\n```python\nfor number in range(3):\n print(number)\n```\n\n```output\n0\n1\n2\n```\n:::\n\n:::note\n- Blocks are defined by indentation.\n- range(3) yields 0, 1, and 2.\n:::\n', 'functions.md': '+++\nslug = "functions"\ntitle = "Functions"\nsection = "Functions"\nsummary = "Use def to name reusable behavior and return results."\ndoc_path = "/tutorial/controlflow.html#defining-functions"\n+++\n\nFunctions package behavior behind a name. `def` creates a function object that can accept arguments, compute values, and return a result.\n\nDefault arguments make common calls short, and keyword arguments make call sites easier to read. A function that reaches the end without `return` produces `None`.\n\nUse functions when a calculation has a useful name, when code repeats, or when a piece of behavior should be tested independently.\n\n:::program\n```python\ndef greet(name):\n return f"Hello, {name}."\n\nprint(greet("Python"))\n\n\ndef format_total(amount, currency="USD"):\n return f"{amount} {currency}"\n\nprint(format_total(10))\nprint(format_total(10, currency="EUR"))\n\n\ndef log(message):\n print(f"log: {message}")\n\nresult = log("saved")\nprint(result)\n\n\ndef append_broken(item, items=[]):\n items.append(item)\n return items\n\nprint(append_broken("a"))\nprint(append_broken("b"))\n\n\ndef append_fixed(item, items=None):\n if items is None:\n items = []\n items.append(item)\n return items\n\nprint(append_fixed("a"))\nprint(append_fixed("b"))\n```\n:::\n\n:::cell\n`return` sends a value back to the caller. The caller can print it, store it, or pass it to another function.\n\n```python\ndef greet(name):\n return f"Hello, {name}."\n\nprint(greet("Python"))\n```\n\n```output\nHello, Python.\n```\n:::\n\n:::cell\nDefault arguments provide common values. Keyword arguments make it clear which option is being overridden.\n\n```python\ndef format_total(amount, currency="USD"):\n return f"{amount} {currency}"\n\nprint(format_total(10))\nprint(format_total(10, currency="EUR"))\n```\n\n```output\n10 USD\n10 EUR\n```\n:::\n\n:::cell\nA function without an explicit `return` returns `None`. That makes side-effect-only functions easy to distinguish from value-producing ones.\n\n```python\ndef log(message):\n print(f"log: {message}")\n\nresult = log("saved")\nprint(result)\n```\n\n```output\nlog: saved\nNone\n```\n:::\n\n:::cell\nMutable default arguments are evaluated once when the function is defined, not on each call. The same list is shared across calls, so successive calls see each other\'s mutations. Use `None` as the sentinel and create a fresh container inside the body.\n\n```python\ndef append_broken(item, items=[]):\n items.append(item)\n return items\n\nprint(append_broken("a"))\nprint(append_broken("b"))\n\n\ndef append_fixed(item, items=None):\n if items is None:\n items = []\n items.append(item)\n return items\n\nprint(append_fixed("a"))\nprint(append_fixed("b"))\n```\n\n```output\n[\'a\']\n[\'a\', \'b\']\n[\'a\']\n[\'b\']\n```\n:::\n\n:::note\n- Use `return` for values the caller should receive.\n- Defaults keep common calls concise.\n- Keyword arguments make options readable at the call site.\n- Never use a mutable value as a default argument; use `None` and build the container inside the function body.\n:::\n', 'generator-expressions.md': '+++\nslug = "generator-expressions"\ntitle = "Generator Expressions"\nsection = "Iteration"\nsummary = "Generator expressions use comprehension-like syntax to stream values lazily."\ndoc_path = "/tutorial/classes.html#generator-expressions"\n+++\n\nGenerator expressions look like list comprehensions with parentheses, but they produce an iterator instead of building a concrete collection immediately.\n\nUse them when a consumer such as `sum()`, `any()`, or a `for` loop can use values one at a time. This keeps the transformation close to the consumer and avoids storing intermediate lists.\n\nLike other iterators, a generator expression is consumed as values are requested. Create a new generator expression when you need another pass.\n\n:::program\n```python\nnumbers = [1, 2, 3, 4]\nlist_squares = [number * number for number in numbers]\nprint(list_squares)\n\nstream_squares = (number * number for number in numbers)\nprint(next(stream_squares))\nprint(next(stream_squares))\nprint(list(stream_squares))\n\nprint(sum(number * number for number in numbers))\n```\n:::\n\n:::cell\nA list comprehension is eager: it builds a list immediately. That is useful when you need to store or reuse the results.\n\n```python\nnumbers = [1, 2, 3, 4]\nlist_squares = [number * number for number in numbers]\nprint(list_squares)\n```\n\n```output\n[1, 4, 9, 16]\n```\n:::\n\n:::cell\nA generator expression is lazy: it creates an iterator that produces values as they are consumed. After two `next()` calls, only the remaining squares are left.\n\n```python\nstream_squares = (number * number for number in numbers)\nprint(next(stream_squares))\nprint(next(stream_squares))\nprint(list(stream_squares))\n```\n\n```output\n1\n4\n[9, 16]\n```\n:::\n\n:::cell\nGenerator expressions are common inside reducing functions. When a generator expression is the only argument, the extra parentheses can be omitted.\n\n```python\nprint(sum(number * number for number in numbers))\n```\n\n```output\n30\n```\n:::\n\n:::note\n- List, dict, and set comprehensions build concrete collections.\n- Generator expressions produce one-pass iterators.\n- Use generator expressions when the consumer can process values one at a time.\n:::\n', 'generators.md': '+++\nslug = "generators"\ntitle = "Generators"\nsection = "Iteration"\nsummary = "yield creates an iterator that produces values on demand."\ndoc_path = "/tutorial/classes.html#generators"\nsee_also = [\n "iterators",\n "iterator-vs-iterable",\n "generator-expressions",\n]\n+++\n\nA generator function is a convenient way to write your own iterator. `yield` produces one value, pauses the function, and resumes when the next value is requested.\n\nGenerators are useful for pipelines, large inputs, and infinite sequences because they avoid building an entire collection in memory.\n\nUse `next()` to request one value manually, or loop over the generator to consume values until it is exhausted.\n\n:::program\n```python\ndef countdown(n):\n while n > 0:\n yield n\n n -= 1\n\nnumbers = countdown(3)\nprint(next(numbers))\nprint(next(numbers))\n\nfor value in countdown(3):\n print(value)\n\ndef countdown_eager(n):\n result = []\n while n > 0:\n result.append(n)\n n -= 1\n return result\n\nvalues = countdown_eager(3)\nprint(values)\nprint(values)\n\nstream = countdown(3)\nprint(list(stream))\nprint(list(stream))\n\nclass Countdown:\n def __init__(self, n):\n self.n = n\n\n def __iter__(self):\n return self\n\n def __next__(self):\n if self.n <= 0:\n raise StopIteration\n value = self.n\n self.n -= 1\n return value\n\nprint(list(Countdown(3)))\n```\n:::\n\n:::cell\nCalling a generator function returns an iterator. `next()` asks for one value and resumes the function until the next `yield`.\n\n```python\ndef countdown(n):\n while n > 0:\n yield n\n n -= 1\n\nnumbers = countdown(3)\nprint(next(numbers))\nprint(next(numbers))\n```\n\n```output\n3\n2\n```\n:::\n\n:::cell\nA `for` loop repeatedly calls `next()` for you. The loop stops when the generator is exhausted.\n\n```python\nfor value in countdown(3):\n print(value)\n```\n\n```output\n3\n2\n1\n```\n:::\n\n:::cell\n`return` builds the entire result before handing it back; `yield` produces values on demand. The list keeps its values for repeated use, while the generator is exhausted after one pass.\n\n```python\ndef countdown_eager(n):\n result = []\n while n > 0:\n result.append(n)\n n -= 1\n return result\n\nvalues = countdown_eager(3)\nprint(values)\nprint(values)\n\nstream = countdown(3)\nprint(list(stream))\nprint(list(stream))\n```\n\n```output\n[3, 2, 1]\n[3, 2, 1]\n[3, 2, 1]\n[]\n```\n:::\n\n:::cell\nEvery generator is an iterator. The same countdown written by hand needs `__iter__` and `__next__` and an explicit `StopIteration`. The generator function expresses the same protocol with one `yield`.\n\n```python\nclass Countdown:\n def __init__(self, n):\n self.n = n\n\n def __iter__(self):\n return self\n\n def __next__(self):\n if self.n <= 0:\n raise StopIteration\n value = self.n\n self.n -= 1\n return value\n\nprint(list(Countdown(3)))\n```\n\n```output\n[3, 2, 1]\n```\n:::\n\n:::note\n- Generator functions are a concise way to create custom iterators; every generator is an iterator.\n- `yield` defers work and streams values; `return` produces the whole result up front.\n- A generator is consumed as you iterate over it.\n- Prefer a list when you need to reuse stored results; prefer a generator when values can be streamed once.\n:::\n', 'generics-and-typevar.md': '+++\nslug = "generics-and-typevar"\ntitle = "Generics and TypeVar"\nsection = "Types"\nsummary = "Generics preserve type information across reusable functions and classes."\ndoc_path = "/library/typing.html#generics"\nsee_also = [\n "type-hints",\n "collections-module",\n "casts-and-any",\n]\n+++\n\nGenerics connect types across an API. A plain function that returns `object` loses information; a generic function can say that the returned value has the same type as the input element.\n\nA `TypeVar` stands for a type chosen by the caller. In `list[T] -> T`, the same `T` says that a list of strings produces a string and a list of integers produces an integer.\n\nUse generics when a function or class is reusable but still preserves a relationship between input and output types.\n\n:::program\n```python\nfrom typing import TypeVar\n\nT = TypeVar("T")\n\n\ndef first(items: list[T]) -> T:\n return items[0]\n\n\ndef pair(left: T, right: T) -> tuple[T, T]:\n return (left, right)\n\nprint(first([1, 2, 3]))\nprint(first(["Ada", "Grace"]))\nprint(pair("x", "y"))\nprint(T.__name__)\n```\n:::\n\n:::cell\nA `TypeVar` stands for a type chosen by the caller. The return type follows the list element type.\n\n```python\nfrom typing import TypeVar\n\nT = TypeVar("T")\n\n\ndef first(items: list[T]) -> T:\n return items[0]\n\nprint(first([1, 2, 3]))\nprint(first(["Ada", "Grace"]))\n```\n\n```output\n1\nAda\n```\n:::\n\n:::cell\nReusing the same `TypeVar` expresses a relationship between parameters and results.\n\n```python\ndef pair(left: T, right: T) -> tuple[T, T]:\n return (left, right)\n\nprint(pair("x", "y"))\n```\n\n```output\n(\'x\', \'y\')\n```\n:::\n\n:::cell\n`TypeVar` is visible at runtime, but the relationship is mainly for type checkers.\n\n```python\nprint(T.__name__)\nprint(first.__annotations__)\n```\n\n```output\nT\n{\'items\': list[~T], \'return\': ~T}\n```\n:::\n\n:::note\n- A `TypeVar` stands for a type chosen by the caller.\n- Generic functions avoid losing information to `object` or `Any`.\n- Use generics when input and output types are connected.\n:::\n', 'guard-clauses.md': '+++\nslug = "guard-clauses"\ntitle = "Guard Clauses"\nsection = "Control Flow"\nsummary = "Guard clauses handle exceptional cases early so the main path stays flat."\ndoc_path = "/tutorial/controlflow.html#if-statements"\n+++\n\nGuard clauses handle exceptional cases early so the main path stays flat. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\ndef price_after_discount(price, percent):\n if price < 0:\n return "invalid price"\n if not 0 <= percent <= 100:\n return "invalid discount"\n\n discount = price * percent / 100\n return round(price - discount, 2)\n\nprint(price_after_discount(100, 15))\nprint(price_after_discount(-5, 10))\nprint(price_after_discount(100, 120))\n```\n:::\n\n:::cell\nReturn early when inputs cannot be handled.\n\n```python\ndef price_after_discount(price, percent):\n if price < 0:\n return "invalid price"\n if not 0 <= percent <= 100:\n return "invalid discount"\n\n discount = price * percent / 100\n return round(price - discount, 2)\n\nprint(price_after_discount(100, 15))\nprint(price_after_discount(-5, 10))\nprint(price_after_discount(100, 120))\n```\n\n```output\n85.0\ninvalid price\ninvalid discount\n```\n:::\n\n:::note\n- Return early when inputs cannot be handled.\n- After the guards, the remaining code can read as the normal path.\n- Guard clauses are a style choice, not new syntax.\n:::\n', 'hello-world.md': '+++\nslug = "hello-world"\ntitle = "Hello World"\nsection = "Basics"\nsummary = "The first Python program prints a line of text."\ndoc_path = "/tutorial/introduction.html"\n+++\n\nEvery Python program starts by executing statements from top to bottom. Calling `print()` is the smallest useful program because it shows how Python evaluates an expression and sends text to standard output.\n\nStrings are ordinary values, so the message passed to `print()` can be changed, stored in a variable, or produced by a function. This example keeps the first program intentionally small.\n\n`print()` writes text followed by a newline. Strings can be delimited with single or double quotes.\n\n:::program\n```python\nprint("hello world")\n```\n:::\n\n:::cell\nEvery Python program starts by executing statements from top to bottom. Calling `print()` is the smallest useful program because it shows how Python evaluates an expression and sends text to standard output.\n\n```python\nprint("hello world")\n```\n\n```output\nhello world\n```\n:::\n\n:::note\n- `print()` writes text followed by a newline.\n- Strings can be delimited with single or double quotes.\n:::\n', 'import-aliases.md': '+++\nslug = "import-aliases"\ntitle = "Import Aliases"\nsection = "Modules"\nsummary = "as gives imported modules or names a local alias."\ndoc_path = "/reference/simple_stmts.html#the-import-statement"\nsee_also = [\n "modules",\n "functions",\n]\n+++\n\n`as` gives an imported module or imported name a local alias. Use it when a conventional short name improves readability or when two imports would otherwise collide.\n\nThe alternative is a plain import, which is usually better when the module name is already clear. Avoid aliases that make readers guess where a name came from.\n\nAvoid star imports in examples and production modules because they hide dependencies and blur the boundary between modules.\n\n:::program\n```python\nimport statistics as stats\nfrom math import sqrt as square_root\n\nscores = [8, 10, 9]\nprint(stats.mean(scores))\nprint(stats.__name__)\n\nprint(square_root(81))\nprint(square_root.__name__)\n```\n:::\n\n:::cell\nA module alias keeps the namespace but changes the local name. Here `stats` is shorter, but readers can still see that `mean` belongs to the statistics module.\n\n```python\nimport statistics as stats\n\nscores = [8, 10, 9]\nprint(stats.mean(scores))\nprint(stats.__name__)\n```\n\n```output\n9\nstatistics\n```\n:::\n\n:::cell\nA name imported with `from` can also be aliased. Use this when the local name explains the role better than the original name.\n\n```python\nfrom math import sqrt as square_root\n\nprint(square_root(81))\nprint(square_root.__name__)\n```\n\n```output\n9.0\nsqrt\n```\n:::\n\n:::note\n- `import module as alias` keeps module-style access under a shorter or clearer name.\n- `from module import name as alias` imports one name under a local alias.\n- Prefer plain imports unless an alias improves clarity or follows a strong convention.\n- Avoid `from module import *` because it makes dependencies harder to see.\n:::\n', 'inheritance-and-super.md': '+++\nslug = "inheritance-and-super"\ntitle = "Inheritance and Super"\nsection = "Classes"\nsummary = "Inheritance reuses behavior, and super delegates to a parent implementation."\ndoc_path = "/tutorial/classes.html#inheritance"\nsee_also = [\n "classes",\n "abstract-base-classes",\n "classmethods-and-staticmethods",\n "special-methods",\n]\n+++\n\nInheritance lets one class specialize another class. The child class gets parent behavior and can add or override methods.\n\nUse `super()` when the child method should extend the parent implementation instead of replacing it entirely.\n\nPrefer composition when objects merely collaborate. Inheritance is best when the child really is a specialized version of the parent.\n\n:::program\n```python\nclass Animal:\n def __init__(self, name):\n self.name = name\n\n def speak(self):\n return f"{self.name} makes a sound"\n\nclass Dog(Animal):\n def speak(self):\n base = super().speak()\n return f"{base}; {self.name} barks"\n\npet = Dog("Nina")\nprint(pet.name)\nprint(pet.speak())\nprint(isinstance(pet, Animal))\n```\n:::\n\n:::cell\nA child class names its parent in parentheses. `Dog` instances get the `Animal.__init__` method because `Dog` does not define its own initializer.\n\n```python\nclass Animal:\n def __init__(self, name):\n self.name = name\n\n def speak(self):\n return f"{self.name} makes a sound"\n\nclass Dog(Animal):\n def speak(self):\n base = super().speak()\n return f"{base}; {self.name} barks"\n\npet = Dog("Nina")\nprint(pet.name)\n```\n\n```output\nNina\n```\n:::\n\n:::cell\n`super()` delegates to the parent implementation. The child method can reuse the parent result and then add specialized behavior.\n\n```python\nprint(pet.speak())\nprint(isinstance(pet, Animal))\n```\n\n```output\nNina makes a sound; Nina barks\nTrue\n```\n:::\n\n:::note\n- Inheritance models an “is a specialized kind of” relationship.\n- `super()` calls the next implementation in the method resolution order.\n- Prefer composition when an object only needs to use another object.\n:::\n', 'iterating-over-iterables.md': '+++\nslug = "iterating-over-iterables"\ntitle = "Iterating over Iterables"\nsection = "Iteration"\nsummary = "for loops consume values from any iterable object."\ndoc_path = "/tutorial/controlflow.html#for-statements"\nsee_also = [\n "iterators",\n "iterator-vs-iterable",\n "for-loops",\n]\n+++\n\nPython\'s `for` statement consumes values from any iterable object: lists, strings, dictionaries, ranges, generators, files, and many standard-library helpers.\n\nThis makes iteration a value-stream protocol rather than a special case for arrays. The producer decides how values are made, and the loop consumes them one at a time.\n\nUse `enumerate()` when you need positions and values together, and `dict.items()` when you need keys and values. These helpers express intent better than manual indexing.\n\n:::program\n```python\nnames = ["Ada", "Grace", "Guido"]\n\nfor name in names:\n print(name)\n\nfor index, name in enumerate(names):\n print(index, name)\n\nscores = {"Ada": 10, "Grace": 9}\nfor name, score in scores.items():\n print(name, score)\n```\n:::\n\n:::cell\nStart with an ordinary list. A list stores values, and a `for` loop asks it for one value at a time.\n\nWhen you only need the values, iterate over the collection directly. There is no index variable because the loop body does not need one.\n\n```python\nnames = ["Ada", "Grace", "Guido"]\n\nfor name in names:\n print(name)\n```\n\n```output\nAda\nGrace\nGuido\n```\n:::\n\n:::cell\nWhen you need both a position and a value, use `enumerate()`. It produces index/value pairs without manual indexing.\n\n```python\nfor index, name in enumerate(names):\n print(index, name)\n```\n\n```output\n0 Ada\n1 Grace\n2 Guido\n```\n:::\n\n:::cell\nDictionaries are iterable too, but `dict.items()` is the clearest way to say that the loop needs keys and values together.\n\n```python\nscores = {"Ada": 10, "Grace": 9}\nfor name, score in scores.items():\n print(name, score)\n```\n\n```output\nAda 10\nGrace 9\n```\n:::\n\n:::note\n- A `for` loop consumes values from an iterable.\n- Different producers can feed the same loop protocol.\n- Prefer `enumerate()` over `range(len(...))` when you need an index.\n:::\n', 'iterator-vs-iterable.md': '+++\nslug = "iterator-vs-iterable"\ntitle = "Iterator vs Iterable"\nsection = "Iteration"\nsummary = "Iterables produce fresh iterators; iterators are one-pass."\ndoc_path = "/glossary.html#term-iterable"\nsee_also = [\n "iterators",\n "iterating-over-iterables",\n "generators",\n]\n+++\n\nAn iterable can produce values when asked. An iterator is the object that remembers where the production currently is. The distinction matters because iterables can be traversed many times, while many iterators can be traversed only once.\n\n`iter(iterable)` returns a fresh iterator each call. `iter(iterator)` returns the iterator itself. That self-iteration property is how `for` loops can accept either kind, and it is also why a function that loops over its argument twice silently breaks when called with a generator instead of a list.\n\nThe takeaway for API design: receive iterables when the caller may want a second pass, and materialize once at the boundary if you must.\n\n:::program\n```python\nnames = ["Ada", "Grace"]\n\nprint(list(names))\nprint(list(names))\n\nstream = iter(names)\nprint(list(stream))\nprint(list(stream))\n\nfirst = iter(names)\nsecond = iter(names)\nprint(first is second)\nprint(iter(first) is first)\n\ndef total_and_count(numbers):\n total = sum(numbers)\n count = sum(1 for _ in numbers)\n return total, count\n\ndef values():\n yield from [10, 9, 8]\n\nprint(total_and_count([10, 9, 8]))\nprint(total_and_count(values()))\n\ndef total_and_count_safe(numbers):\n items = list(numbers)\n return sum(items), len(items)\n\nprint(total_and_count_safe(values()))\n```\n:::\n\n:::cell\nA list is iterable. Each `for` loop or `list()` call asks the list for a fresh iterator under the hood, so the same data can be traversed many times.\n\n```python\nnames = ["Ada", "Grace"]\nprint(list(names))\nprint(list(names))\n```\n\n```output\n[\'Ada\', \'Grace\']\n[\'Ada\', \'Grace\']\n```\n:::\n\n:::cell\nAn iterator is one-pass. Calling `iter()` returns a position-tracking object; once it has been exhausted, it stays exhausted.\n\n```python\nstream = iter(names)\nprint(list(stream))\nprint(list(stream))\n```\n\n```output\n[\'Ada\', \'Grace\']\n[]\n```\n:::\n\n:::cell\nCalling `iter()` on an iterable returns a brand-new iterator each time. Calling `iter()` on an iterator returns the same object — that is the rule that lets a `for` loop accept either kind.\n\n```python\nfirst = iter(names)\nsecond = iter(names)\nprint(first is second)\nprint(iter(first) is first)\n```\n\n```output\nFalse\nTrue\n```\n:::\n\n:::cell\nThe distinction shows up at API boundaries. A function that loops over its argument twice works for an iterable but silently produces wrong answers for an iterator, because the second pass finds the iterator already exhausted. Materialize once at the boundary when both passes matter.\n\n```python\ndef total_and_count(numbers):\n total = sum(numbers)\n count = sum(1 for _ in numbers)\n return total, count\n\ndef values():\n yield from [10, 9, 8]\n\nprint(total_and_count([10, 9, 8]))\nprint(total_and_count(values()))\n\ndef total_and_count_safe(numbers):\n items = list(numbers)\n return sum(items), len(items)\n\nprint(total_and_count_safe(values()))\n```\n\n```output\n(27, 3)\n(27, 0)\n(27, 3)\n```\n:::\n\n:::note\n- An iterable produces an iterator each time `iter()` is called on it; an iterator produces values until it is exhausted.\n- `iter(iterable)` returns a fresh iterator; `iter(iterator)` returns the same iterator.\n- Functions that traverse their input more than once must accept an iterable or materialize the input at the boundary.\n:::\n', 'iterators.md': '+++\nslug = "iterators"\ntitle = "Iterators"\nsection = "Iteration"\nsummary = "iter and next expose the protocol behind for loops."\ndoc_path = "/library/stdtypes.html#iterator-types"\nsee_also = [\n "iterating-over-iterables",\n "iterator-vs-iterable",\n "generators",\n]\n+++\n\nAn iterable is an object that can produce values for a loop. An iterator is the object that remembers where that production currently is.\n\n`iter()` asks an iterable for an iterator, and `next()` consumes one value from that iterator. A `for` loop performs those steps for you until the iterator is exhausted.\n\nThis is the core value-stream protocol in Python: one object produces values, another piece of code consumes them, and many streams are one-pass.\n\n:::program\n```python\nnames = ["Ada", "Grace", "Guido"]\niterator = iter(names)\nprint(next(iterator))\nprint(next(iterator))\n\nfor name in iterator:\n print(name)\n\nagain = iter(names)\nprint(next(again))\n```\n:::\n\n:::cell\n`iter()` asks an iterable for an iterator. `next()` consumes one value and advances the iterator\'s position.\n\n```python\nnames = ["Ada", "Grace", "Guido"]\niterator = iter(names)\nprint(next(iterator))\nprint(next(iterator))\n```\n\n```output\nAda\nGrace\n```\n:::\n\n:::cell\nA `for` loop consumes the same iterator protocol. Because two values were already consumed, the loop sees only the remaining value.\n\n```python\nfor name in iterator:\n print(name)\n```\n\n```output\nGuido\n```\n:::\n\n:::cell\nThe list itself is reusable. Asking it for a fresh iterator starts a new pass over the same stored values.\n\n```python\nagain = iter(names)\nprint(next(again))\n```\n\n```output\nAda\n```\n:::\n\n:::note\n- Iterables produce iterators; iterators produce values.\n- `next()` consumes one value from an iterator.\n- Many iterators are one-pass even when the original collection is reusable.\n:::\n', 'itertools.md': '+++\nslug = "itertools"\ntitle = "Itertools"\nsection = "Iteration"\nsummary = "itertools composes lazy iterator streams."\ndoc_path = "/library/itertools.html"\n+++\n\nThe `itertools` module contains tools for composing iterator streams: combining, slicing, grouping, and repeating values without changing the consumer protocol.\n\nMany `itertools` functions are lazy. They describe work to do later instead of building a list immediately, so helpers such as `islice()` are useful when taking a finite window.\n\nIterator pipelines let each step stay small: one object produces values, another transforms them, and a final consumer such as `list()` or a loop pulls values through the pipeline.\n\n:::program\n```python\nimport itertools\n\ncounter = itertools.count(10)\nprint(list(itertools.islice(counter, 3)))\n\npages = itertools.chain(["intro", "setup"], ["deploy"])\nprint(list(pages))\n\nscores = [7, 10, 8, 10]\nhigh_scores = itertools.compress(scores, [score >= 9 for score in scores])\nprint(list(high_scores))\n```\n:::\n\n:::cell\n`count()` can produce values forever, so `islice()` takes a finite window. Nothing is materialized until `list()` consumes the iterator.\n\n```python\nimport itertools\n\ncounter = itertools.count(10)\nprint(list(itertools.islice(counter, 3)))\n```\n\n```output\n[10, 11, 12]\n```\n:::\n\n:::cell\n`chain()` presents several iterables as one stream. This avoids building an intermediate list just to loop over combined inputs.\n\n```python\npages = itertools.chain(["intro", "setup"], ["deploy"])\nprint(list(pages))\n```\n\n```output\n[\'intro\', \'setup\', \'deploy\']\n```\n:::\n\n:::cell\nIterator helpers compose with ordinary Python expressions. `compress()` keeps items whose corresponding selector is true.\n\n```python\nscores = [7, 10, 8, 10]\nhigh_scores = itertools.compress(scores, [score >= 9 for score in scores])\nprint(list(high_scores))\n```\n\n```output\n[10, 10]\n```\n:::\n\n:::note\n- `itertools` composes producer and transformer streams.\n- Iterator pipelines avoid building intermediate lists.\n- Use `islice()` to take a finite piece from an infinite iterator.\n- Convert to a list only when you need concrete results.\n:::\n', 'json.md': '+++\nslug = "json"\ntitle = "JSON"\nsection = "Standard Library"\nsummary = "json encodes Python values as JSON text and decodes them back."\ndoc_path = "/library/json.html"\nsee_also = [\n "dicts",\n "typed-dicts",\n "strings",\n]\n+++\n\nThe `json` module converts between Python values and JSON text. Dictionaries, lists, strings, numbers, booleans, and `None` map naturally to JSON structures.\n\nUse `dumps()` when you need a string and `loads()` when you need Python objects back. Options such as `sort_keys=True` and `indent=2` control stable, readable output.\n\nJSON is a data format, not a way to preserve arbitrary Python objects. Encode simple data structures at service boundaries, and expect decode errors when the incoming text is not valid JSON.\n\n:::program\n```python\nimport json\n\npayload = {"language": "Python", "versions": [3, 13], "stable": True, "missing": None}\ntext = json.dumps(payload, sort_keys=True)\nprint(text)\n\npretty = json.dumps({"language": "Python", "stable": True}, indent=2, sort_keys=True)\nprint(pretty.splitlines()[0])\nprint(pretty.splitlines()[1])\n\ndecoded = json.loads(text)\nprint(decoded["language"])\nprint(decoded["missing"] is None)\n\ntry:\n json.loads("{bad json}")\nexcept json.JSONDecodeError as error:\n print(error.__class__.__name__)\n```\n:::\n\n:::cell\n`dumps()` encodes Python data as JSON text. `sort_keys=True` keeps dictionary keys in a stable order for reproducible output.\n\n```python\nimport json\n\npayload = {"language": "Python", "versions": [3, 13], "stable": True, "missing": None}\ntext = json.dumps(payload, sort_keys=True)\nprint(text)\n```\n\n```output\n{"language": "Python", "missing": null, "stable": true, "versions": [3, 13]}\n```\n:::\n\n:::cell\nFormatting options change the JSON text, not the Python value. `indent=2` is useful for human-readable output.\n\n```python\npretty = json.dumps({"language": "Python", "stable": True}, indent=2, sort_keys=True)\nprint(pretty.splitlines()[0])\nprint(pretty.splitlines()[1])\n```\n\n```output\n{\n "language": "Python",\n```\n:::\n\n:::cell\n`loads()` decodes JSON text back into Python values. JSON `null` becomes Python `None`.\n\n```python\ndecoded = json.loads(text)\nprint(decoded["language"])\nprint(decoded["missing"] is None)\n```\n\n```output\nPython\nTrue\n```\n:::\n\n:::cell\nInvalid JSON raises `JSONDecodeError`, so input boundaries should handle decode failures explicitly.\n\n```python\ntry:\n json.loads("{bad json}")\nexcept json.JSONDecodeError as error:\n print(error.__class__.__name__)\n```\n\n```output\nJSONDecodeError\n```\n:::\n\n:::note\n- `dumps()` returns a string; `loads()` accepts a string.\n- JSON `true`, `false`, and `null` become Python `True`, `False`, and `None`.\n- Use `sort_keys=True` when stable text output matters.\n- JSON only represents data shapes, not arbitrary Python objects or behavior.\n:::\n', 'keyword-only-arguments.md': '+++\nslug = "keyword-only-arguments"\ntitle = "Keyword-only Arguments"\nsection = "Functions"\nsummary = "Use * to require selected function arguments to be named."\ndoc_path = "/tutorial/controlflow.html#special-parameters"\n+++\n\nA bare `*` in a function signature marks the following parameters as keyword-only. Callers must name those arguments explicitly.\n\nKeyword-only arguments are useful for options such as timeouts, flags, and modes where positional calls would be ambiguous or easy to misread.\n\nThey let the required data stay positional while optional controls remain self-documenting at the call site.\n\n:::program\n```python\ndef connect(host, *, timeout=5, secure=True):\n scheme = "https" if secure else "http"\n print(f"{scheme}://{host} timeout={timeout}")\n\nconnect("example.com")\nconnect("example.com", timeout=10)\nconnect("localhost", secure=False)\n```\n:::\n\n:::cell\nParameters after `*` must be named. The default options still apply when the caller omits them.\n\n```python\ndef connect(host, *, timeout=5, secure=True):\n scheme = "https" if secure else "http"\n print(f"{scheme}://{host} timeout={timeout}")\n\nconnect("example.com")\n```\n\n```output\nhttps://example.com timeout=5\n```\n:::\n\n:::cell\nNaming the option makes the call site explicit. A reader does not have to remember which positional slot controls the timeout.\n\n```python\nconnect("example.com", timeout=10)\n```\n\n```output\nhttps://example.com timeout=10\n```\n:::\n\n:::cell\nFlags are especially good keyword-only arguments because a bare positional `False` is hard to interpret.\n\n```python\nconnect("localhost", secure=False)\n```\n\n```output\nhttp://localhost timeout=5\n```\n:::\n\n:::note\n- Put `*` before options that callers should name.\n- Keyword-only flags avoid mysterious positional `True` and `False` arguments.\n- Defaults work normally for keyword-only parameters.\n:::\n', 'lambdas.md': '+++\nslug = "lambdas"\ntitle = "Lambdas"\nsection = "Functions"\nsummary = "lambda creates small anonymous function expressions."\ndoc_path = "/tutorial/controlflow.html#lambda-expressions"\n+++\n\n`lambda` creates a small anonymous function expression. It is most useful when Python asks for a function and the behavior is short enough to read inline.\n\nA lambda can only contain one expression. Use `def` when the behavior deserves a name, needs statements, or would be easier to test separately.\n\nLambdas often appear as key functions, callbacks, and tiny adapters. Keep them simple enough that the call site remains clearer than a named helper.\n\n:::program\n```python\nadd_tax = lambda price: round(price * 1.08, 2)\nprint(add_tax(10))\n\nitems = [("notebook", 5), ("pen", 2), ("bag", 20)]\nby_price = sorted(items, key=lambda item: item[1])\nprint(by_price)\n\ndef price(item):\n return item[1]\n\nprint(sorted(items, key=price))\n```\n:::\n\n:::cell\nA lambda is a function expression. Assigning one to a name works, although `def` is usually clearer for reusable behavior.\n\n```python\nadd_tax = lambda price: round(price * 1.08, 2)\nprint(add_tax(10))\n```\n\n```output\n10.8\n```\n:::\n\n:::cell\nLambdas are most idiomatic when passed directly to another function. `sorted()` calls this key function once for each item.\n\n```python\nitems = [("notebook", 5), ("pen", 2), ("bag", 20)]\nby_price = sorted(items, key=lambda item: item[1])\nprint(by_price)\n```\n\n```output\n[(\'pen\', 2), (\'notebook\', 5), (\'bag\', 20)]\n```\n:::\n\n:::cell\nA named function is better when the behavior should be reused or explained. It produces the same sort key, but gives the operation a name.\n\n```python\ndef price(item):\n return item[1]\n\nprint(sorted(items, key=price))\n```\n\n```output\n[(\'pen\', 2), (\'notebook\', 5), (\'bag\', 20)]\n```\n:::\n\n:::note\n- Lambdas are expressions, not statements.\n- Prefer `def` for multi-step or reused behavior.\n- Lambdas are common as `key=` functions because the behavior is local to one call.\n:::\n', 'lists.md': '+++\nslug = "lists"\ntitle = "Lists"\nsection = "Collections"\nsummary = "Lists are ordered, mutable collections."\ndoc_path = "/tutorial/datastructures.html#more-on-lists"\n+++\n\nLists are Python\'s general-purpose mutable sequence type. Use them when order matters and the collection may grow, shrink, or be rearranged.\n\nIndexing reads individual positions. `0` is the first item, and negative indexes count backward from the end.\n\nMutation and copying matter: `append()` changes the list, while `sorted()` returns a new ordered list and leaves the original alone.\n\n:::program\n```python\nnumbers = [3, 1, 4]\nnumbers.append(1)\n\nprint(numbers)\nprint(numbers[0])\nprint(numbers[-1])\nprint(sorted(numbers))\nprint(numbers)\n```\n:::\n\n:::cell\nCreate a list with square brackets. Because lists are mutable, `append()` changes this same list object.\n\n```python\nnumbers = [3, 1, 4]\nnumbers.append(1)\n\nprint(numbers)\n```\n\n```output\n[3, 1, 4, 1]\n```\n:::\n\n:::cell\nUse indexes to read positions. Negative indexes are convenient for reading from the end.\n\n```python\nprint(numbers[0])\nprint(numbers[-1])\n```\n\n```output\n3\n1\n```\n:::\n\n:::cell\nUse `sorted()` when you want an ordered copy and still need the original order afterward.\n\n```python\nprint(sorted(numbers))\nprint(numbers)\n```\n\n```output\n[1, 1, 3, 4]\n[3, 1, 4, 1]\n```\n:::\n\n:::note\n- Lists are mutable sequences: methods such as `append()` change the list in place.\n- Negative indexes count from the end.\n- `sorted()` returns a new list; `list.sort()` sorts the existing list in place.\n:::\n', 'literal-and-final.md': '+++\nslug = "literal-and-final"\ntitle = "Literal and Final"\nsection = "Types"\nsummary = "Literal restricts values, while Final marks names that should not be rebound."\ndoc_path = "/library/typing.html#typing.Literal"\n+++\n\n`Literal` restricts a value to one of a small set of exact options, and `Final` tells the type checker that a name should not be rebound. Both are static promises that type checkers enforce; Python\'s runtime assignment rules still permit any value through if a program ignores the annotation.\n\nUse them when an annotation makes a constant or a small option set explicit at the API boundary. Prefer simpler neighboring tools when the extra machinery would hide the intent.\n\n`Literal` pairs naturally with type aliases and overloads when a function should accept only a known set of strings or numbers. `Final` is most useful for module-level constants and class attributes that the rest of the codebase should treat as immutable.\n\n:::program\n```python\nfrom typing import Final, Literal\n\nMode = Literal["read", "write"]\nDEFAULT_MODE: Final[Mode] = "read"\n\n\ndef open_label(mode: Mode) -> str:\n return f"opening for {mode}"\n\nprint(open_label(DEFAULT_MODE))\nprint(open_label("write"))\nprint(DEFAULT_MODE)\n```\n:::\n\n:::cell\n`Literal` describes a small set of exact allowed values.\n\n```python\nfrom typing import Final, Literal\n\nMode = Literal["read", "write"]\nDEFAULT_MODE: Final[Mode] = "read"\n\n\ndef open_label(mode: Mode) -> str:\n return f"opening for {mode}"\n\nprint(open_label(DEFAULT_MODE))\nprint(open_label("write"))\nprint(DEFAULT_MODE)\n```\n\n```output\nopening for read\nopening for write\nread\n```\n:::\n\n:::note\n- `Literal` describes a small set of exact allowed values.\n- `Final` tells type checkers that a name should not be rebound.\n- Both are static promises; ordinary runtime assignment rules still apply.\n:::\n', 'literals.md': '+++\nslug = "literals"\ntitle = "Literals"\nsection = "Basics"\nsummary = "Literals write values directly in Python source code."\ndoc_path = "/reference/lexical_analysis.html#literals"\nsee_also = [\n "values",\n "strings",\n "numbers",\n "string-formatting",\n]\n+++\n\nLiterals are source-code forms for values: numbers, text, bytes, containers, booleans, `None`, and a few specialized markers. They are how a program writes small values directly.\n\nThe literal form is only the beginning. Later examples explain each value family in depth: strings are Unicode text, bytes are binary data, lists and dicts are containers, and `None` represents intentional absence.\n\nUse literals when the value is small and local. Give repeated or meaningful values a name so the program explains why that value matters.\n\n:::program\n```python\nwhole = 42\nfraction = 3.5\ncomplex_number = 2 + 3j\nprint(whole, fraction, complex_number.imag)\n\nflags = 0xFF\nmask = 0b1010\nmillion = 1_000_000\nprint(flags, mask, million)\n\ntext = "python"\nraw_pattern = r"\\d+"\ndata = b"py"\nscore = 98\nformatted = f"score={score}"\nprint(text)\nprint(raw_pattern)\nprint(data)\nprint(formatted)\n\npoint = (2, 3)\nnames = ["Ada", "Grace"]\nscores = {"Ada": 98}\nunique = {"py", "go"}\nprint(point)\nprint(names[0])\nprint(scores["Ada"])\nprint(sorted(unique))\n\nprint(True, False, None)\nprint(...)\n\nprint(type({}).__name__)\nprint(type(set()).__name__)\nprint(type({1, 2}).__name__)\n```\n:::\n\n:::cell\nNumeric literals write numbers directly. Complex literals use `j` for the imaginary part.\n\n```python\nwhole = 42\nfraction = 3.5\ncomplex_number = 2 + 3j\nprint(whole, fraction, complex_number.imag)\n```\n\n```output\n42 3.5 3.0\n```\n:::\n\n:::cell\nInteger literals also accept hexadecimal (`0x`), binary (`0b`), and octal (`0o`) prefixes. Underscores group digits visually without changing the value.\n\n```python\nflags = 0xFF\nmask = 0b1010\nmillion = 1_000_000\nprint(flags, mask, million)\n```\n\n```output\n255 10 1000000\n```\n:::\n\n:::cell\nString literals write Unicode text. Raw strings keep backslashes literal, bytes literals write binary data rather than text, and f-strings (`f"..."`) embed expressions inline.\n\n```python\ntext = "python"\nraw_pattern = r"\\d+"\ndata = b"py"\nscore = 98\nformatted = f"score={score}"\nprint(text)\nprint(raw_pattern)\nprint(data)\nprint(formatted)\n```\n\n```output\npython\n\\d+\nb\'py\'\nscore=98\n```\n:::\n\n:::cell\nContainer literals create tuples, lists, dictionaries, and sets. Each container answers a different question about order, position, lookup, or uniqueness.\n\n```python\npoint = (2, 3)\nnames = ["Ada", "Grace"]\nscores = {"Ada": 98}\nunique = {"py", "go"}\nprint(point)\nprint(names[0])\nprint(scores["Ada"])\nprint(sorted(unique))\n```\n\n```output\n(2, 3)\nAda\n98\n[\'go\', \'py\']\n```\n:::\n\n:::cell\n`True`, `False`, `None`, and `...` are singleton literal-like constants used for truth values, absence, and placeholders.\n\n```python\nprint(True, False, None)\nprint(...)\n```\n\n```output\nTrue False None\nEllipsis\n```\n:::\n\n:::cell\nCurly-brace literals are dictionaries by default. The empty form `{}` is an empty dictionary, not an empty set; use `set()` for that. A non-empty `{1, 2}` is a set because keyless items can only be a set.\n\n```python\nprint(type({}).__name__)\nprint(type(set()).__name__)\nprint(type({1, 2}).__name__)\n```\n\n```output\ndict\nset\nset\n```\n:::\n\n:::note\n- Literals are good for small local values; constants are better for repeated values with meaning.\n- `{}` is an empty dictionary. Use `set()` for an empty set.\n- Bytes literals are binary data; string literals are Unicode text.\n- `...` evaluates to the `Ellipsis` object.\n:::\n', 'logging.md': '+++\nslug = "logging"\ntitle = "Logging"\nsection = "Standard Library"\nsummary = "logging records operational events without using print as infrastructure."\ndoc_path = "/library/logging.html"\n+++\n\n`logging` records operational events without using `print` as infrastructure. Loggers name where each event came from, handlers route records to outputs, and levels (`DEBUG`, `INFO`, `WARNING`, `ERROR`) let operators choose how much detail they want to see.\n\nUse it when a program needs structured records with thresholds — production services, command-line tools, scheduled jobs. Prefer plain `print` when a small script just needs to show a line of human output.\n\nThe example wires a single stream handler to stdout to keep the output deterministic. Real applications usually configure logging once at startup and then call `logging.getLogger(__name__)` from each module.\n\n:::program\n```python\nimport logging\nimport sys\n\nlogger = logging.getLogger("example")\nlogger.setLevel(logging.INFO)\nhandler = logging.StreamHandler(sys.stdout)\nformatter = logging.Formatter("%(levelname)s:%(message)s")\nhandler.setFormatter(formatter)\nlogger.handlers[:] = [handler]\n\nlogger.debug("hidden")\nlogger.info("service started")\nlogger.warning("disk almost full")\n```\n:::\n\n:::cell\nLoggers name where an event came from.\n\n```python\nimport logging\nimport sys\n\nlogger = logging.getLogger("example")\nlogger.setLevel(logging.INFO)\nhandler = logging.StreamHandler(sys.stdout)\nformatter = logging.Formatter("%(levelname)s:%(message)s")\nhandler.setFormatter(formatter)\nlogger.handlers[:] = [handler]\n\nlogger.debug("hidden")\nlogger.info("service started")\nlogger.warning("disk almost full")\n```\n\n```output\nINFO:service started\nWARNING:disk almost full\n```\n:::\n\n:::note\n- Loggers name where an event came from.\n- Handlers decide where records go.\n- Levels let operators choose how much detail to see.\n:::\n', 'loop-else.md': '+++\nslug = "loop-else"\ntitle = "Loop Else"\nsection = "Control Flow"\nsummary = "A loop else block runs only when the loop did not end with break."\ndoc_path = "/tutorial/controlflow.html#else-clauses-on-loops"\nsee_also = [\n "break-and-continue",\n "for-loops",\n "while-loops",\n]\n+++\n\nPython loops can have an `else` clause. The name is surprising at first: loop `else` means “no `break` happened,” not “the loop condition was false.”\n\nThis is useful for searches. Put the successful early exit in `break`, then put the not-found path in `else`.\n\nUse loop `else` sparingly. It is clearest when the loop is visibly searching for something.\n\n:::program\n```python\nnames = ["Ada", "Grace", "Guido"]\n\nfor name in names:\n if name == "Grace":\n print("found")\n break\nelse:\n print("missing")\n\nfor name in names:\n if name == "Linus":\n print("found")\n break\nelse:\n print("missing")\n```\n:::\n\n:::cell\nIf the loop reaches `break`, the `else` block is skipped. This branch means the search succeeded early.\n\n```python\nnames = ["Ada", "Grace", "Guido"]\n\nfor name in names:\n if name == "Grace":\n print("found")\n break\nelse:\n print("missing")\n```\n\n```output\nfound\n```\n:::\n\n:::cell\nIf the loop finishes without `break`, the `else` block runs. This branch means the search examined every value and found nothing.\n\n```python\nfor name in names:\n if name == "Linus":\n print("found")\n break\nelse:\n print("missing")\n```\n\n```output\nmissing\n```\n:::\n\n:::note\n- Loop `else` runs when the loop was not ended by `break`.\n- It is best for search loops with a clear found/not-found split.\n- It works with both `for` and `while` loops.\n:::\n', 'match-statements.md': '+++\nslug = "match-statements"\ntitle = "Match Statements"\nsection = "Control Flow"\nsummary = "match selects cases using structural pattern matching."\ndoc_path = "/tutorial/controlflow.html#match-statements"\n+++\n\nStructural pattern matching lets a program choose a branch based on the shape of data. It is especially useful when commands, messages, or parsed data have a few known forms.\n\nA `case` pattern can both check constants and bind names. The move case checks the action and extracts `x` and `y` in one readable step.\n\nOrder matters because Python tries cases from top to bottom. Specific shapes should appear before broad fallback cases such as `_`.\n\n:::program\n```python\ncommand = {"action": "move", "x": 3, "y": 4}\n\nmatch command:\n case {"action": "move", "x": x, "y": y}:\n print(f"move to {x},{y}")\n case {"action": "quit"}:\n print("quit")\n case {"action": action}:\n print(f"unknown action: {action}")\n case _:\n print("invalid command")\n```\n:::\n\n:::cell\nUse `match` when the shape of a value is the decision. This command is a dictionary with an action and coordinates; the first case checks that shape and binds `x` and `y`.\n\n```python\ncommand = {"action": "move", "x": 3, "y": 4}\n\nmatch command:\n case {"action": "move", "x": x, "y": y}:\n print(f"move to {x},{y}")\n```\n\n```output\nmove to 3,4\n```\n:::\n\n:::cell\nOther cases describe other valid shapes. This complete fragment changes the command so the `quit` case is the first matching pattern.\n\n```python\ncommand = {"action": "quit"}\n\nmatch command:\n case {"action": "move", "x": x, "y": y}:\n print(f"move to {x},{y}")\n case {"action": "quit"}:\n print("quit")\n```\n\n```output\nquit\n```\n:::\n\n:::cell\nBroader patterns and the `_` catch-all belong after specific cases. This fragment extracts an unknown action before the final fallback would run.\n\n```python\ncommand = {"action": "jump"}\n\nmatch command:\n case {"action": "move", "x": x, "y": y}:\n print(f"move to {x},{y}")\n case {"action": "quit"}:\n print("quit")\n case {"action": action}:\n print(f"unknown action: {action}")\n case _:\n print("invalid command")\n```\n\n```output\nunknown action: jump\n```\n:::\n\n:::note\n- `match` compares structure, not just equality.\n- Patterns can bind names such as `x` and `y` while matching.\n- Put the catch-all `_` case last, because cases are tried from top to bottom.\n:::\n', 'metaclasses.md': '+++\nslug = "metaclasses"\ntitle = "Metaclasses"\nsection = "Classes"\nsummary = "A metaclass customizes how classes themselves are created."\ndoc_path = "/reference/datamodel.html#metaclasses"\nsee_also = [\n "classes",\n "inheritance-and-super",\n "special-methods",\n]\n+++\n\nA metaclass is the class of a class. Most Python code never needs one, but the syntax appears in frameworks that register, validate, or modify classes as they are created.\n\nThe `metaclass=` keyword in a class statement chooses the object that builds the class. This is advanced machinery; decorators and ordinary functions are usually simpler.\n\nUse metaclasses only when class creation itself is the problem being solved.\n\n:::program\n```python\nclass Tagged(type):\n def __new__(mcls, name, bases, namespace):\n namespace["tag"] = name.lower()\n return super().__new__(mcls, name, bases, namespace)\n\nclass Event(metaclass=Tagged):\n pass\n\nprint(Event.tag)\nprint(type(Event).__name__)\n```\n:::\n\n:::cell\nA metaclass customizes class creation. `__new__` receives the class name, bases, and namespace before the class object exists.\n\n```python\nclass Tagged(type):\n def __new__(mcls, name, bases, namespace):\n namespace["tag"] = name.lower()\n return super().__new__(mcls, name, bases, namespace)\n\nprint(Tagged.__name__)\n```\n\n```output\nTagged\n```\n:::\n\n:::cell\nThe `metaclass=` keyword applies that class-building rule. Here the metaclass adds a `tag` attribute to the new class.\n\n```python\nclass Event(metaclass=Tagged):\n pass\n\nprint(Event.tag)\nprint(type(Event).__name__)\n```\n\n```output\nevent\nTagged\n```\n:::\n\n:::note\n- Metaclasses customize class creation, not instance behavior directly.\n- Most code should prefer class decorators, functions, or ordinary inheritance.\n- You are most likely to meet metaclasses inside frameworks and ORMs.\n:::\n', 'modules.md': '+++\nslug = "modules"\ntitle = "Modules"\nsection = "Modules"\nsummary = "Modules organize code into namespaces and expose reusable definitions."\ndoc_path = "/tutorial/modules.html"\nsee_also = [\n "import-aliases",\n "packages",\n]\n+++\n\nModules organize Python code into files and namespaces. `import` executes a module once, stores it in Python\'s import cache, and gives your program access to its definitions.\n\nThis page focuses on import forms and module namespaces. Package layout, aliases, and dynamic imports have their own neighboring examples.\n\nUse module namespaces such as `math.sqrt` when the source of a name should stay visible. Use focused imports such as `from statistics import mean` when the imported name is clear at the call site.\n\n:::program\n```python\nimport math\nimport sys\nfrom statistics import mean\n\nradius = 3\narea = math.pi * radius ** 2\nprint(round(area, 2))\n\nscores = [8, 10, 9]\nprint(mean(scores))\n\nprint(math.__name__)\nprint("math" in sys.modules)\n```\n:::\n\n:::cell\nImporting a module gives access to its namespace. The `math.` prefix makes it clear where `pi` came from.\n\n```python\nimport math\n\nradius = 3\narea = math.pi * radius ** 2\nprint(round(area, 2))\n```\n\n```output\n28.27\n```\n:::\n\n:::cell\nA focused `from ... import ...` brings one definition into the current namespace. This keeps a common operation concise without importing every name.\n\n```python\nfrom statistics import mean\n\nscores = [8, 10, 9]\nprint(mean(scores))\n```\n\n```output\n9\n```\n:::\n\n:::cell\nModules are objects too. Their attributes include metadata such as `__name__`, which records the module\'s import name.\n\n```python\nprint(math.__name__)\n```\n\n```output\nmath\n```\n:::\n\n:::cell\nImported modules are cached in `sys.modules`. Later imports reuse the module object instead of executing the file again.\n\n```python\nimport sys\nprint("math" in sys.modules)\n```\n\n```output\nTrue\n```\n:::\n\n:::note\n- Prefer plain `import module` when the namespace improves readability.\n- Use focused imports for a small number of clear names.\n- Place imports near the top of the file.\n- Imports execute module top-level code once, then reuse the cached module object.\n:::\n', 'multiple-return-values.md': '+++\nslug = "multiple-return-values"\ntitle = "Multiple Return Values"\nsection = "Functions"\nsummary = "Python returns multiple values by returning a tuple and unpacking it."\ndoc_path = "/tutorial/datastructures.html#tuples-and-sequences"\n+++\n\nPython multiple return values are tuple return values with friendly syntax. `return a, b` creates one tuple containing two positions.\n\nMost callers unpack that tuple immediately. Good target names make the meaning of each returned position explicit.\n\nUse this for small, fixed groups of results. For larger records, a dataclass or named tuple usually communicates better.\n\n:::program\n```python\ndef divide_with_remainder(total, size):\n quotient = total // size\n remainder = total % size\n return quotient, remainder\n\nresult = divide_with_remainder(17, 5)\nprint(result)\n\nboxes, leftover = result\nprint(boxes)\nprint(leftover)\n```\n:::\n\n:::cell\nReturning values separated by commas returns one tuple. The tuple is visible if the caller stores the result directly.\n\n```python\ndef divide_with_remainder(total, size):\n quotient = total // size\n remainder = total % size\n return quotient, remainder\n\nresult = divide_with_remainder(17, 5)\nprint(result)\n```\n\n```output\n(3, 2)\n```\n:::\n\n:::cell\nCallers usually unpack the tuple immediately or soon after. The names at the call site document what each position means.\n\n```python\nboxes, leftover = result\nprint(boxes)\nprint(leftover)\n```\n\n```output\n3\n2\n```\n:::\n\n:::note\n- A comma creates a tuple; `return a, b` returns one tuple containing two values.\n- Unpacking at the call site gives each returned position a meaningful name.\n- Use a class-like record when the result has many fields.\n:::\n', 'mutability.md': '+++\nslug = "mutability"\ntitle = "Mutability"\nsection = "Data Model"\nsummary = "Some objects change in place, while others return new values."\ndoc_path = "/reference/datamodel.html#objects-values-and-types"\n+++\n\nObjects in Python can be mutable or immutable. Mutable objects such as lists and dictionaries can change in place, while immutable objects such as strings and tuples produce new values instead.\n\nNames can share one mutable object, so a change through one name is visible through another. This is powerful, but it is also the source of many beginner surprises.\n\nThe boundary matters across Python: `append()` mutates a list, string methods return new strings, and `sorted()` returns a new list while `list.sort()` mutates an existing one.\n\n:::program\n```python\nfirst = ["python"]\nsecond = first\nsecond.append("workers")\nprint(first)\nprint(second)\n\ntext = "python"\nupper_text = text.upper()\nprint(text)\nprint(upper_text)\n\nnumbers = [3, 1, 2]\nordered = sorted(numbers)\nprint(ordered)\nprint(numbers)\n```\n:::\n\n:::cell\nMutable objects can change in place. `first` and `second` point to the same list, so appending through one name changes the object seen through both names.\n\n```python\nfirst = ["python"]\nsecond = first\nsecond.append("workers")\nprint(first)\nprint(second)\n```\n\n```output\n[\'python\', \'workers\']\n[\'python\', \'workers\']\n```\n:::\n\n:::cell\nImmutable objects do not change in place. String methods such as `upper()` return a new string, leaving the original string unchanged.\n\n```python\ntext = "python"\nupper_text = text.upper()\nprint(text)\nprint(upper_text)\n```\n\n```output\npython\nPYTHON\n```\n:::\n\n:::cell\nSome APIs make the boundary explicit. `sorted()` returns a new list, while methods such as `append()` and `list.sort()` mutate an existing list.\n\n```python\nnumbers = [3, 1, 2]\nordered = sorted(numbers)\nprint(ordered)\nprint(numbers)\n```\n\n```output\n[1, 2, 3]\n[3, 1, 2]\n```\n:::\n\n:::note\n- Lists and dictionaries are mutable; strings and tuples are immutable.\n- Aliasing is useful, but copy mutable containers when independent changes are needed.\n- Pay attention to whether an operation mutates in place or returns a new value.\n:::\n', 'networking.md': '+++\nslug = "networking"\ntitle = "Networking"\nsection = "Standard Library"\nsummary = "Networking code exchanges bytes across explicit protocol boundaries."\ndoc_path = "/library/socket.html"\nexpected_output = "b\'ping\'\\nping\\n"\n+++\n\nNetworking code sends and receives bytes. Higher-level HTTP clients hide many details, but the core boundary is still explicit: text must be encoded before sending and decoded after receiving.\n\nThis example uses `socket.socketpair()` so it stays local and deterministic in ordinary Python. Real network clients often use `socket.create_connection()` or a higher-level HTTP library, but the same byte boundary applies.\n\nThe useful mental model is endpoint plus bytes plus cleanup. A socket connects two endpoints, transfers byte strings, and must be closed when the conversation is finished.\n\n:::program\n```python\nimport socket\n\nleft, right = socket.socketpair()\ntry:\n message = "ping"\n left.sendall(message.encode("utf-8"))\n data = right.recv(16)\n print(data)\n print(data.decode("utf-8"))\nfinally:\n left.close()\n right.close()\n```\n:::\n\n:::unsupported\n`socketpair()` returns two connected endpoints. `sendall` writes encoded bytes into one end, and `recv` reads up to 16 bytes off the other. The byte boundary is the whole point: `"ping".encode("utf-8")` produces `b\'ping\'`, which is what the socket actually moves. (This fragment runs in standard Python only — Dynamic Workers don\'t expose arbitrary sockets and this app disables Worker outbound access.)\n\n```python\nleft, right = socket.socketpair()\nleft.sendall("ping".encode("utf-8"))\ndata = right.recv(16)\n```\n:::\n\n:::cell\nThe complete version adds two things: a `try`/`finally` so both endpoints close even if `recv` or the surrounding work raises, and a second `print` that `decode`s the received bytes back into a Python `str` for display. The first `print` shows the raw bytes `b\'ping\'`; the second shows the decoded text `ping`.\n\n```python\nimport socket\n\nleft, right = socket.socketpair()\ntry:\n message = "ping"\n left.sendall(message.encode("utf-8"))\n data = right.recv(16)\n print(data)\n print(data.decode("utf-8"))\nfinally:\n left.close()\n right.close()\n```\n\n```output\nb\'ping\'\nping\n```\n:::\n\n:::note\n- Network protocols move bytes, not Python `str` objects.\n- Close real sockets when finished, usually with a context manager or `finally` block.\n- Use high-level HTTP libraries for application HTTP unless socket-level control is the lesson.\n:::\n', 'newtype.md': '+++\nslug = "newtype"\ntitle = "NewType"\nsection = "Types"\nsummary = "NewType creates distinct static identities for runtime-compatible values."\ndoc_path = "/library/typing.html#typing.NewType"\nsee_also = [\n "type-aliases",\n "type-hints",\n "runtime-type-checks",\n]\n+++\n\n`NewType` creates a distinct static identity for a value that is represented by an existing runtime type. It is useful for IDs, units, and other values that should not be mixed accidentally.\n\nThe key boundary is static versus runtime behavior. A type checker can distinguish `UserId` from `OrderId`, but at runtime both values are plain integers.\n\nUse a type alias when you only want a clearer name for a shape. Use `NewType` when mixing two compatible shapes should be treated as a mistake by static analysis.\n\n:::program\n```python\nfrom typing import NewType\n\nUserId = NewType("UserId", int)\nOrderId = NewType("OrderId", int)\n\n\ndef load_user(user_id: UserId) -> str:\n return f"user {user_id}"\n\nuid = UserId(42)\noid = OrderId(42)\nprint(load_user(uid))\nprint(uid == oid)\nprint(type(uid).__name__)\nprint(UserId.__name__)\n```\n:::\n\n:::cell\n`NewType` helps type checkers distinguish values that share a runtime representation.\n\n```python\nfrom typing import NewType\n\nUserId = NewType("UserId", int)\nOrderId = NewType("OrderId", int)\n\n\ndef load_user(user_id: UserId) -> str:\n return f"user {user_id}"\n\nuid = UserId(42)\nprint(load_user(uid))\n```\n\n```output\nuser 42\n```\n:::\n\n:::cell\nAt runtime, a `NewType` value is the underlying value. It compares like that value and has the same runtime type.\n\n```python\noid = OrderId(42)\nprint(uid == oid)\nprint(type(uid).__name__)\n```\n\n```output\nTrue\nint\n```\n:::\n\n:::cell\nThe `NewType` constructor keeps a name for static tools and introspection.\n\n```python\nprint(UserId.__name__)\nprint(OrderId.__name__)\n```\n\n```output\nUserId\nOrderId\n```\n:::\n\n:::note\n- `NewType` helps type checkers distinguish values that share a runtime representation.\n- At runtime, the value is still the underlying type.\n- Use aliases for readability; use `NewType` for static separation.\n:::\n', 'none.md': '+++\nslug = "none"\ntitle = "None"\nsection = "Basics"\nsummary = "None represents expected absence, distinct from missing keys and errors."\ndoc_path = "/library/constants.html#None"\n+++\n\n`None` represents the absence of a value. It is the usual sentinel when a function has no result to return but the absence itself is meaningful.\n\nBecause `None` is a singleton, idiomatic Python checks it with `is None` or `is not None`. This avoids confusing identity with value equality.\n\nAbsence has several nearby shapes in Python. A function can return `None`, a dictionary lookup can supply a default for a missing key, and an invalid operation can raise an exception.\n\n:::program\n```python\nresult = None\nprint(result is None)\n\ndef find_score(name):\n if name == "Ada":\n return 10\n return None\n\nscore = find_score("Grace")\nprint(score is None)\n\nprofile = {"name": "Ada"}\nprint(profile.get("timezone", "UTC"))\n\ntry:\n int("python")\nexcept ValueError:\n print("invalid number")\n```\n:::\n\n:::cell\n`None` is Python\'s value for “nothing here.” Check it with `is None` because it is a singleton identity value.\n\n```python\nresult = None\nprint(result is None)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\nFunctions often return `None` when absence is expected and callers can continue. The function name and surrounding code should make that possibility clear.\n\n```python\ndef find_score(name):\n if name == "Ada":\n return 10\n return None\n\nscore = find_score("Grace")\nprint(score is None)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\nA missing dictionary key is another absence boundary. Use `get()` when the mapping can supply a default, and use exceptions for invalid operations that cannot produce a value.\n\n```python\nprofile = {"name": "Ada"}\nprint(profile.get("timezone", "UTC"))\n\ntry:\n int("python")\nexcept ValueError:\n print("invalid number")\n```\n\n```output\nUTC\ninvalid number\n```\n:::\n\n:::note\n- Use `is None` rather than `== None`; `None` is a singleton identity value.\n- Use `None` for expected absence that callers can test.\n- Use dictionary defaults for missing mapping keys and exceptions for invalid operations.\n:::\n', 'number-parsing.md': '+++\nslug = "number-parsing"\ntitle = "Number Parsing"\nsection = "Standard Library"\nsummary = "int() and float() parse text into numbers and raise ValueError on bad input."\ndoc_path = "/library/functions.html#int"\n+++\n\nParsing turns text into numeric values. `int()` parses whole-number text and `float()` parses decimal or scientific-notation text.\n\nInvalid numeric text raises `ValueError`. Catching that specific exception makes it clear that bad input is expected and recoverable.\n\nAfter parsing, the result is a number and can participate in arithmetic; before parsing, it is just text.\n\n:::program\n```python\nprint(int("42"))\nprint(float("3.5"))\n\ntry:\n int("python")\nexcept ValueError as error:\n print(type(error).__name__)\n```\n:::\n\n:::cell\nUse `int()` for whole numbers and `float()` for decimal text. Parsed values are real numbers, not strings.\n\n```python\nprint(int("42"))\nprint(float("3.5"))\n```\n\n```output\n42\n3.5\n```\n:::\n\n:::cell\nBad numeric text raises `ValueError`. Catch that specific exception when invalid input is part of the normal flow.\n\n```python\ntry:\n int("python")\nexcept ValueError as error:\n print(type(error).__name__)\n```\n\n```output\nValueError\n```\n:::\n\n:::note\n- `int()` and `float()` are constructors that also parse strings.\n- Catch `ValueError` when bad user input is expected and recoverable.\n:::\n', 'numbers.md': '+++\nslug = "numbers"\ntitle = "Numbers"\nsection = "Basics"\nsummary = "Python numbers include integers, floats, and complex values."\ndoc_path = "/library/stdtypes.html#numeric-types-int-float-complex"\nsee_also = [\n "literals",\n "operators",\n]\n+++\n\nPython\'s numeric model starts with `int`, `float`, and `complex`. Integers are arbitrary precision, floats are approximate double-precision values, and complex numbers carry real and imaginary parts.\n\nOperators encode different numeric questions. `/` means true division and returns a float, `//` means floor division, `%` gives the remainder, and `**` computes powers.\n\nUse rounding for display, not as a substitute for understanding floating-point approximation. Financial code usually needs `decimal.Decimal`, which is a separate precision topic.\n\n:::program\n```python\nimport math\n\ncount = 10\nratio = 0.25\nz = 2 + 3j\n\nprint(count + 5)\nprint(count / 4)\nprint(count // 4)\nprint(count % 4)\nprint(2 ** 5)\nprint(z.real, z.imag)\nprint(0.1 + 0.2)\nprint(0.1 + 0.2 == 0.3)\nprint(math.isclose(0.1 + 0.2, 0.3))\nprint(round(3.14159, 2))\n```\n:::\n\n:::cell\nPython has `int` for whole numbers and `float` for approximate real-valued arithmetic. True division with `/` returns a `float`, even when both inputs are integers.\n\n```python\ncount = 10\nratio = 0.25\n\nprint(count + 5)\nprint(count / 4)\nprint(ratio * 2)\n```\n\n```output\n15\n2.5\n0.5\n```\n:::\n\n:::cell\nFloor division and modulo are useful when you need quotient and remainder behavior. Powers use `**`, not `^`.\n\n```python\nprint(count // 4)\nprint(count % 4)\nprint(2 ** 5)\n```\n\n```output\n2\n2\n32\n```\n:::\n\n:::cell\nComplex numbers are built in. The literal suffix `j` marks the imaginary part.\n\n```python\nz = 2 + 3j\nprint(z.real, z.imag)\n```\n\n```output\n2.0 3.0\n```\n:::\n\n:::cell\nFloating-point values are approximate, so `==` between expected and computed floats is rarely the right test. Compare with `math.isclose` (or work in `decimal.Decimal`) when the question is "are these the same number to within tolerance".\n\n```python\nimport math\n\nprint(0.1 + 0.2)\nprint(0.1 + 0.2 == 0.3)\nprint(math.isclose(0.1 + 0.2, 0.3))\nprint(round(3.14159, 2))\n```\n\n```output\n0.30000000000000004\nFalse\nTrue\n3.14\n```\n:::\n\n:::note\n- Python\'s `int` has arbitrary precision; it grows as large as memory allows.\n- Python\'s `float` is approximate double-precision floating point.\n- Use `/` for true division and `//` for floor division.\n- Use `math.isclose` instead of `==` for floating-point comparison; reach for `decimal.Decimal` when exact decimal precision is the domain requirement.\n:::\n', 'object-lifecycle.md': '+++\nslug = "object-lifecycle"\ntitle = "Object Lifecycle"\nsection = "Basics"\nsummary = "References keep objects alive until Python can reclaim them."\ndoc_path = "/reference/datamodel.html#objects-values-and-types"\n+++\n\nReferences keep objects alive until Python can reclaim them. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nimport gc\n\nnames = []\nalias = names\nalias.append("Ada")\n\nprint(names is alias)\nprint(names)\n\nobject_id = id(names)\ndel alias\nprint(id(names) == object_id)\n\ndel names\nprint("object can be reclaimed")\ngc.collect()\n```\n:::\n\n:::cell\nUse `is` and `id()` to observe identity while two names refer to the same object.\n\n```python\nimport gc\n\nnames = []\nalias = names\nalias.append("Ada")\n\nprint(names is alias)\nprint(names)\n\nobject_id = id(names)\ndel alias\nprint(id(names) == object_id)\n\ndel names\nprint("object can be reclaimed")\ngc.collect()\n```\n\n```output\nTrue\n[\'Ada\']\nTrue\nobject can be reclaimed\n```\n:::\n\n:::note\n- Use `is` and `id()` to observe identity while two names refer to the same object.\n- Deleting a name removes one reference; it does not directly destroy the object if another reference still exists.\n- Python reclaims unreachable objects automatically, so programs usually manage ownership by controlling references.\n:::\n', 'operator-overloading.md': '+++\nslug = "operator-overloading"\ntitle = "Operator Overloading"\nsection = "Data Model"\nsummary = "Operator methods let objects define arithmetic and comparison syntax."\ndoc_path = "/reference/datamodel.html#emulating-numeric-types"\nsee_also = [\n "operators",\n "special-methods",\n "equality-and-identity",\n]\n+++\n\nOperator overloading lets a class define what expressions such as `a + b` mean for its objects. This is useful when the operation is part of the domain vocabulary.\n\nThe method should preserve the meaning readers expect from the operator. Vectors can add component by component; money can add amounts in the same currency; surprising overloads make code harder to trust.\n\nPython also has reflected methods such as `__radd__` for cases where the left operand does not know how to handle the right operand. That keeps mixed operations possible without making every type know every other type.\n\n:::program\n```python\nclass Vector:\n def __init__(self, x, y):\n self.x = x\n self.y = y\n\n def __add__(self, other):\n return Vector(self.x + other.x, self.y + other.y)\n\n def __eq__(self, other):\n return (self.x, self.y) == (other.x, other.y)\n\n def __repr__(self):\n return f"Vector({self.x}, {self.y})"\n\nprint(Vector(2, 3) + Vector(4, 5))\nprint(Vector(1, 1) == Vector(1, 1))\n```\n:::\n\n:::cell\n`__add__` defines how the `+` operator combines two objects.\n\n```python\nclass Vector:\n def __init__(self, x, y):\n self.x = x\n self.y = y\n\n def __add__(self, other):\n return Vector(self.x + other.x, self.y + other.y)\n\n def __repr__(self):\n return f"Vector({self.x}, {self.y})"\n\nprint(Vector(2, 3) + Vector(4, 5))\n```\n\n```output\nVector(6, 8)\n```\n:::\n\n:::cell\n`__eq__` defines value equality for `==`. Without it, user-defined objects compare by identity.\n\n```python\nclass Vector:\n def __init__(self, x, y):\n self.x = x\n self.y = y\n\n def __eq__(self, other):\n return (self.x, self.y) == (other.x, other.y)\n\nprint(Vector(1, 1) == Vector(1, 1))\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\nA useful `__repr__` makes operator results inspectable while debugging.\n\n```python\nclass Vector:\n def __init__(self, x, y):\n self.x = x\n self.y = y\n\n def __add__(self, other):\n return Vector(self.x + other.x, self.y + other.y)\n\n def __repr__(self):\n return f"Vector({self.x}, {self.y})"\n\nprint(repr(Vector(2, 3) + Vector(4, 5)))\n```\n\n```output\nVector(6, 8)\n```\n:::\n\n:::note\n- Overload operators only when the operation is unsurprising.\n- Return `NotImplemented` when an operand type is unsupported.\n- Implement equality deliberately when value comparison matters.\n:::\n', 'operators.md': '+++\nslug = "operators"\ntitle = "Operators"\nsection = "Basics"\nsummary = "Operators combine, compare, and test values in expressions."\ndoc_path = "/reference/expressions.html#operator-precedence"\nsee_also = [\n "numbers",\n "equality-and-identity",\n "assignment-expressions",\n "operator-overloading",\n]\n+++\n\nOperators are the punctuation and keywords that combine values into expressions. Some operators compute new values, some compare values, and some ask relationship questions such as membership or identity.\n\nThis page is the surface map. Focused examples explain the deeper behavior of numbers, booleans, conditions, sets, assignment expressions, and operator overloading.\n\nRead operators by the question they ask: arithmetic computes, comparison answers true or false, boolean operators combine truth values, membership searches a container, and specialized operators should only appear when the data shape calls for them.\n\n:::program\n```python\ncount = 10\nprint(count + 5)\nprint(count // 4)\nprint(count % 4)\nprint(2 ** 5)\n\nscore = 91\nprint(80 <= score < 100)\nprint(score == 100 or score >= 90)\nprint("py" in "python")\n\nflags = 0b0011\nprint(flags & 0b0101)\nprint(flags | 0b0100)\nprint(flags ^ 0b0101)\nprint(flags << 1)\n\nclass Scale:\n def __init__(self, value):\n self.value = value\n\n def __matmul__(self, other):\n return self.value * other.value\n\nprint(Scale(2) @ Scale(3))\n\nitems = ["a", "b"]\nif (size := len(items)) > 0:\n print(size)\n```\n:::\n\n:::cell\nArithmetic operators compute new values. Use `//` for floor division, `%` for remainder, and `**` for powers.\n\n```python\ncount = 10\nprint(count + 5)\nprint(count // 4)\nprint(count % 4)\nprint(2 ** 5)\n```\n\n```output\n15\n2\n2\n32\n```\n:::\n\n:::cell\nComparison operators produce booleans. Python comparisons can chain, which keeps range checks readable.\n\n```python\nscore = 91\nprint(80 <= score < 100)\nprint(score == 100 or score >= 90)\nprint("py" in "python")\n```\n\n```output\nTrue\nTrue\nTrue\n```\n:::\n\n:::cell\nBitwise operators work on integer bit patterns. They are useful for masks and flags, not ordinary boolean logic. `&` is bitwise AND, `|` is bitwise OR, `^` is exclusive OR, and `<<` shifts left.\n\n```python\nflags = 0b0011\nprint(flags & 0b0101)\nprint(flags | 0b0100)\nprint(flags ^ 0b0101)\nprint(flags << 1)\n```\n\n```output\n1\n7\n6\n6\n```\n:::\n\n:::cell\nThe `@` operator is reserved for matrix-like multiplication and custom types that define `__matmul__`.\n\n```python\nclass Scale:\n def __init__(self, value):\n self.value = value\n\n def __matmul__(self, other):\n return self.value * other.value\n\nprint(Scale(2) @ Scale(3))\n```\n\n```output\n6\n```\n:::\n\n:::cell\nThe walrus operator `:=` assigns inside an expression. Use it when naming a value avoids repeating work in a condition.\n\n```python\nitems = ["a", "b"]\nif (size := len(items)) > 0:\n print(size)\n```\n\n```output\n2\n```\n:::\n\n:::cell\n`and` and `or` short-circuit: the right side runs only when the left side cannot already determine the result. That makes them safe for guard expressions like `obj and obj.value` where the right side would fail on `None`.\n\n```python\ndef loud():\n print("ran")\n return True\n\nprint(False and loud())\nprint(True or loud())\nprint(True and loud())\n```\n\n```output\nFalse\nTrue\nran\nTrue\n```\n:::\n\n:::note\n- Use the clearest operator for the question: arithmetic, comparison, boolean logic, membership, identity, or bitwise manipulation.\n- `and` and `or` short-circuit, so the right side may not run.\n- Operators have precedence; use parentheses when grouping would otherwise be hard to read.\n- Custom operator behavior should make an object feel more natural, not more clever.\n:::\n', 'overloads.md': '+++\nslug = "overloads"\ntitle = "Overloads"\nsection = "Types"\nsummary = "overload describes APIs whose return type depends on argument types."\ndoc_path = "/library/typing.html#typing.overload"\n+++\n\noverload describes APIs whose return type depends on argument types. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nfrom typing import overload\n\n@overload\ndef double(value: int) -> int: ...\n\n@overload\ndef double(value: str) -> str: ...\n\ndef double(value):\n return value * 2\n\nprint(double(4))\nprint(double("ha"))\nprint(double.__name__)\n```\n:::\n\n:::cell\n`@overload` signatures are for static type checkers.\n\n```python\nfrom typing import overload\n\n@overload\ndef double(value: int) -> int: ...\n\n@overload\ndef double(value: str) -> str: ...\n\ndef double(value):\n return value * 2\n\nprint(double(4))\nprint(double("ha"))\nprint(double.__name__)\n```\n\n```output\n8\nhaha\ndouble\n```\n:::\n\n:::note\n- `@overload` signatures are for static type checkers.\n- The real implementation comes after the overload declarations.\n- Use overloads when a single runtime function has multiple precise static shapes.\n:::\n', 'packages.md': '+++\nslug = "packages"\ntitle = "Packages"\nsection = "Modules"\nsummary = "Packages organize modules into importable directories."\ndoc_path = "/tutorial/modules.html#packages"\nsee_also = [\n "modules",\n "import-aliases",\n "virtual-environments",\n]\n+++\n\nPackages are modules that can contain other modules. They let a project group related code behind dotted import paths such as `json.decoder` or `email.message`.\n\nAt runtime, importing a submodule gives Python a path through that package structure. In a project on disk, that structure is usually a directory with Python files and often an `__init__.py` file.\n\nUse packages when one module has grown into a small namespace of related modules. Keep module names boring and explicit so readers can tell where imported definitions come from.\n\n:::program\n```python\nimport importlib\nimport json\nimport json.decoder\n\nmodule = importlib.import_module("json.decoder")\n\nprint(json.__name__)\nprint(json.decoder.__name__)\nprint(module.JSONDecoder.__name__)\nprint(module is json.decoder)\n\n\nimport os\nimport sys\nimport tempfile\n\nwith tempfile.TemporaryDirectory() as tmp:\n pkg = os.path.join(tmp, "shapes")\n os.makedirs(pkg)\n with open(os.path.join(pkg, "__init__.py"), "w") as init:\n init.write("from .square import area\\n__all__ = [\'area\']\\n")\n with open(os.path.join(pkg, "square.py"), "w") as square:\n square.write("def area(side):\\n return side * side\\n")\n sys.path.insert(0, tmp)\n try:\n import shapes\n print(shapes.area(3))\n print(shapes.__all__)\n finally:\n sys.path.remove(tmp)\n sys.modules.pop("shapes", None)\n sys.modules.pop("shapes.square", None)\n```\n:::\n\n:::cell\nA package is itself a module. The `json` package exposes a namespace that can contain submodules.\n\n```python\nimport json\n\nprint(json.__name__)\n```\n\n```output\njson\n```\n:::\n\n:::cell\nDotted imports name a path through a package. Importing `json.decoder` makes that submodule available under the package namespace.\n\n```python\nimport json.decoder\n\nprint(json.decoder.__name__)\nprint(json.decoder.JSONDecoder.__name__)\n```\n\n```output\njson.decoder\nJSONDecoder\n```\n:::\n\n:::cell\n`importlib.import_module()` imports by string. It is useful for plugin systems and dynamic imports, but ordinary `import` is clearer when the dependency is known.\n\n```python\nimport importlib\n\nmodule = importlib.import_module("json.decoder")\nprint(module is json.decoder)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\nInside a package\'s `__init__.py`, `from .submodule import name` re-exports a submodule\'s name at the package root, and `__all__` lists the names that `from package import *` should make visible. This cell builds a temporary `shapes` package on disk to make both forms concrete.\n\n```python\nimport os\nimport sys\nimport tempfile\n\nwith tempfile.TemporaryDirectory() as tmp:\n pkg = os.path.join(tmp, "shapes")\n os.makedirs(pkg)\n with open(os.path.join(pkg, "__init__.py"), "w") as init:\n init.write("from .square import area\\n__all__ = [\'area\']\\n")\n with open(os.path.join(pkg, "square.py"), "w") as square:\n square.write("def area(side):\\n return side * side\\n")\n sys.path.insert(0, tmp)\n try:\n import shapes\n print(shapes.area(3))\n print(shapes.__all__)\n finally:\n sys.path.remove(tmp)\n sys.modules.pop("shapes", None)\n sys.modules.pop("shapes.square", None)\n```\n\n```output\n9\n[\'area\']\n```\n:::\n\n:::note\n- A package is a module that can contain submodules.\n- Dotted imports should mirror a meaningful project structure.\n- Use `from .submodule import name` inside a package to re-export submodule names; set `__all__` to declare the public surface.\n- Prefer ordinary imports unless the module name is truly dynamic.\n:::\n', 'paramspec.md': '+++\nslug = "paramspec"\ntitle = "ParamSpec"\nsection = "Types"\nsummary = "ParamSpec preserves callable parameter types through wrappers."\ndoc_path = "/library/typing.html#typing.ParamSpec"\n+++\n\n`ParamSpec` lets a wrapper preserve the parameter types of the function it wraps. The pressure that justifies it is decorators: a generic decorator that returns `Callable[..., R]` erases the wrapped function\'s argument types, so callers lose type-checker help on every call.\n\nUse `ParamSpec` when a decorator should be transparent to type checkers — the wrapped function and the decorated name should accept the same arguments. Reach for plain `Callable` when the wrapper deliberately changes the signature.\n\n`P.args` and `P.kwargs` annotate the `*args` and `**kwargs` of the inner wrapper, which is how the parameter spec gets bound. Pair `ParamSpec` with a `TypeVar` for the return type when the wrapper should also stay generic over what the wrapped function returns.\n\n:::program\n```python\nfrom collections.abc import Callable\nfrom typing import ParamSpec, TypeVar\n\nP = ParamSpec("P")\nR = TypeVar("R")\n\n\ndef logged(func: Callable[P, R]) -> Callable[P, R]:\n def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n print("calling", func.__name__)\n return func(*args, **kwargs)\n return wrapper\n\n@logged\ndef add(left: int, right: int) -> int:\n return left + right\n\nprint(add(2, 3))\n```\n:::\n\n:::cell\n`ParamSpec` captures the parameters of a callable.\n\n```python\nfrom collections.abc import Callable\nfrom typing import ParamSpec, TypeVar\n\nP = ParamSpec("P")\nR = TypeVar("R")\n\n\ndef logged(func: Callable[P, R]) -> Callable[P, R]:\n def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n print("calling", func.__name__)\n return func(*args, **kwargs)\n return wrapper\n\n@logged\ndef add(left: int, right: int) -> int:\n return left + right\n\nprint(add(2, 3))\n```\n\n```output\ncalling add\n5\n```\n:::\n\n:::note\n- `ParamSpec` captures the parameters of a callable.\n- Wrappers can forward `*args` and `**kwargs` without erasing the original signature for type checkers.\n- This matters most for decorators.\n:::\n', 'partial-functions.md': '+++\nslug = "partial-functions"\ntitle = "Partial Functions"\nsection = "Functions"\nsummary = "functools.partial pre-fills arguments to make a more specific callable."\ndoc_path = "/library/functools.html#functools.partial"\n+++\n\nfunctools.partial pre-fills arguments to make a more specific callable. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nfrom functools import partial\n\n\ndef apply_tax(rate, amount):\n return round(amount * (1 + rate), 2)\n\nvat = partial(apply_tax, 0.2)\nservice_tax = partial(apply_tax, rate=0.1)\n\nprint(vat(50))\nprint(service_tax(amount=80))\nprint(vat.func.__name__)\n```\n:::\n\n:::cell\nA partial object remembers some arguments.\n\n```python\nfrom functools import partial\n\n\ndef apply_tax(rate, amount):\n return round(amount * (1 + rate), 2)\n\nvat = partial(apply_tax, 0.2)\nservice_tax = partial(apply_tax, rate=0.1)\n\nprint(vat(50))\nprint(service_tax(amount=80))\nprint(vat.func.__name__)\n```\n\n```output\n60.0\n88.0\napply_tax\n```\n:::\n\n:::note\n- A partial object remembers some arguments.\n- The resulting callable can be passed where an ordinary function is expected.\n- Prefer a named function when the pre-filled behavior needs richer logic.\n:::\n', 'positional-only-parameters.md': '+++\nslug = "positional-only-parameters"\ntitle = "Positional-only Parameters"\nsection = "Functions"\nsummary = "Use / to mark parameters that callers must pass by position."\ndoc_path = "/tutorial/controlflow.html#special-parameters"\nsee_also = [\n "keyword-only-arguments",\n "functions",\n "args-and-kwargs",\n]\n+++\n\nA `/` in a function signature marks the parameters before it as positional-only. Callers must pass those arguments by position, not by keyword.\n\nThis is useful when parameter names are implementation details or when an API should match built-in functions that accept positional values.\n\nTogether, `/` and `*` let a signature draw clear boundaries: positional-only inputs, ordinary inputs, and keyword-only options.\n\n:::program\n```python\ndef scale(value, /, factor=2, *, clamp=False):\n result = value * factor\n if clamp:\n result = min(result, 10)\n return result\n\nprint(scale(4))\nprint(scale(4, factor=3))\nprint(scale(4, clamp=True))\n```\n:::\n\n:::cell\nParameters before `/` are positional-only. `value` is the main input, while `factor` remains an ordinary parameter that can be named.\n\n```python\ndef scale(value, /, factor=2, *, clamp=False):\n result = value * factor\n if clamp:\n result = min(result, 10)\n return result\n\nprint(scale(4))\nprint(scale(4, factor=3))\n```\n\n```output\n8\n12\n```\n:::\n\n:::cell\nParameters after `*` are keyword-only. That makes options such as `clamp` explicit at the call site.\n\n```python\nprint(scale(4, clamp=True))\n```\n\n```output\n8\n```\n:::\n\n:::note\n- `/` marks parameters before it as positional-only.\n- `*` marks parameters after it as keyword-only.\n- Use these markers when the call shape is part of the API design.\n:::\n', 'properties.md': '+++\nslug = "properties"\ntitle = "Properties"\nsection = "Classes"\nsummary = "@property keeps attribute syntax while adding computation or validation."\ndoc_path = "/library/functions.html#property"\n+++\n\nProperties let a class keep a simple attribute-style API while running code behind the scenes. Callers write `box.area`, but the class can compute the value from current state.\n\nA property setter can validate assignment without changing the public spelling of the attribute. This is the boundary: plain attributes are enough for plain data, while properties are for computed or protected data.\n\nUse properties for cheap, attribute-like operations. Expensive work or actions with side effects should usually remain explicit methods.\n\n:::program\n```python\nclass Rectangle:\n def __init__(self, width, height):\n self.width = width\n self.height = height\n\n @property\n def area(self):\n return self.width * self.height\n\n @property\n def width(self):\n return self._width\n\n @width.setter\n def width(self, value):\n if value <= 0:\n raise ValueError("width must be positive")\n self._width = value\n\nbox = Rectangle(3, 4)\nprint(box.area)\n\nbox.width = 5\nprint(box.area)\n\ntry:\n box.width = 0\nexcept ValueError as error:\n print(error)\n```\n:::\n\n:::cell\nA read-only property exposes computed data through attribute access. `area` stays current because it is calculated from `width` and `height` each time it is read.\n\n```python\nclass Rectangle:\n def __init__(self, width, height):\n self.width = width\n self.height = height\n\n @property\n def area(self):\n return self.width * self.height\n\n @property\n def width(self):\n return self._width\n\n @width.setter\n def width(self, value):\n if value <= 0:\n raise ValueError("width must be positive")\n self._width = value\n\nbox = Rectangle(3, 4)\nprint(box.area)\n```\n\n```output\n12\n```\n:::\n\n:::cell\nA setter lets assignment keep normal attribute syntax while the class validates or normalizes the value.\n\n```python\nbox.width = 5\nprint(box.area)\n```\n\n```output\n20\n```\n:::\n\n:::cell\nValidation belongs inside the class when every caller should obey the same rule. Invalid assignment raises an exception at the boundary.\n\n```python\ntry:\n box.width = 0\nexcept ValueError as error:\n print(error)\n```\n\n```output\nwidth must be positive\n```\n:::\n\n:::note\n- Properties let APIs start simple and grow validation or computation later.\n- Callers access a property like an attribute, not like a method.\n- Use methods instead when work is expensive or action-like.\n:::\n', 'protocols.md': '+++\nslug = "protocols"\ntitle = "Protocols"\nsection = "Types"\nsummary = "Protocol describes required behavior for structural typing."\ndoc_path = "/library/typing.html#typing.Protocol"\nsee_also = [\n "type-hints",\n "classes",\n "inheritance-and-super",\n "abstract-base-classes",\n]\n+++\n\n`Protocol` describes the methods or attributes an object must provide. It exists for structural typing: if an object has the right shape, type checkers can treat it as compatible.\n\nThis is different from inheritance. Inheritance says a class is explicitly derived from a parent; a protocol says callers only need a particular behavior.\n\nAt runtime, ordinary method lookup still applies. Protocols are mainly for static analysis, documentation, and API boundaries.\n\n:::program\n```python\nfrom typing import Protocol\n\nclass Greeter(Protocol):\n def greet(self) -> str:\n ...\n\nclass Person:\n def __init__(self, name):\n self.name = name\n\n def greet(self):\n return f"hello {self.name}"\n\n\ndef welcome(greeter: Greeter):\n print(greeter.greet())\n\nwelcome(Person("Ada"))\nprint(Greeter.__name__)\n```\n:::\n\n:::cell\nA protocol names required behavior. The ellipsis marks the method body as intentionally unspecified, similar to an interface declaration.\n\n```python\nfrom typing import Protocol\n\nclass Greeter(Protocol):\n def greet(self) -> str:\n ...\n\nprint(Greeter.__name__)\n```\n\n```output\nGreeter\n```\n:::\n\n:::cell\nA class can satisfy the protocol without inheriting from it. `Person` has a compatible `greet()` method, so it has the right shape for static type checkers.\n\n```python\nclass Person:\n def __init__(self, name):\n self.name = name\n\n def greet(self):\n return f"hello {self.name}"\n\nprint(Person("Ada").greet())\n```\n\n```output\nhello Ada\n```\n:::\n\n:::cell\nUse the protocol as an annotation at the API boundary. The function only cares that the object can greet; it does not care about the concrete class.\n\n```python\ndef welcome(greeter: Greeter):\n print(greeter.greet())\n\nwelcome(Person("Ada"))\n```\n\n```output\nhello Ada\n```\n:::\n\n:::note\n- Protocols are for structural typing: compatibility by shape rather than explicit inheritance.\n- Type checkers understand protocols; normal runtime method calls still do the work.\n- Prefer inheritance when shared implementation matters, and protocols when only required behavior matters.\n:::\n', 'recursion.md': '+++\nslug = "recursion"\ntitle = "Recursion"\nsection = "Functions"\nsummary = "Recursive functions solve nested problems by calling themselves on smaller pieces."\ndoc_path = "/tutorial/controlflow.html#defining-functions"\n+++\n\nA recursive function calls itself to solve a smaller piece of the same problem. Recursion exists for data that is naturally nested: trees, menus, expression nodes, and directory-like structures.\n\nEvery recursive function needs a base case that can be answered directly. The recursive case must move toward that base case by passing a smaller part of the data.\n\nPrefer loops for simple repetition over a flat sequence. Prefer recursion when the data shape is recursive too.\n\n:::program\n```python\ntree = {\n "value": 1,\n "children": [\n {"value": 2, "children": []},\n {"value": 3, "children": [{"value": 4, "children": []}]},\n ],\n}\n\ndef total(node):\n subtotal = node["value"]\n for child in node["children"]:\n subtotal += total(child)\n return subtotal\n\nprint(total({"value": 2, "children": []}))\nprint(total(tree))\n```\n:::\n\n:::cell\nA leaf node is the base case. It has no children, so the function can return its own value without making another recursive call.\n\n```python\ndef total(node):\n subtotal = node["value"]\n for child in node["children"]:\n subtotal += total(child)\n return subtotal\n\nprint(total({"value": 2, "children": []}))\n```\n\n```output\n2\n```\n:::\n\n:::cell\nA non-leaf node solves the same problem for each child, then combines those smaller totals with its own value.\n\n```python\ntree = {\n "value": 1,\n "children": [\n {"value": 2, "children": []},\n {"value": 3, "children": [{"value": 4, "children": []}]},\n ],\n}\n\nprint(total(tree))\n```\n\n```output\n10\n```\n:::\n\n:::note\n- Every recursive function needs a base case that stops the calls.\n- Recursion fits nested data better than flat repetition.\n- Python limits recursion depth, so loops are often better for very deep or simple repetition.\n:::\n', 'regular-expressions.md': '+++\nslug = "regular-expressions"\ntitle = "Regular Expressions"\nsection = "Text"\nsummary = "The re module searches and extracts text using regular expressions."\ndoc_path = "/library/re.html"\nsee_also = [\n "strings",\n "string-formatting",\n]\n+++\n\nRegular expressions are a compact language for searching and extracting text patterns. Python\'s `re` module provides the standard interface: `re.match` anchors at the start of the string, `re.search` finds the first occurrence anywhere, `re.findall` collects every match, `re.sub` rewrites matches, and `re.compile` reuses a pattern.\n\nUse regex when the pattern has structure: repeated records, alternatives, optional parts, or pieces you want to capture. Prefer ordinary string methods for simple substring checks because simpler code is easier to maintain.\n\nFlags such as `re.IGNORECASE` adjust matching behavior without rewriting the pattern. Pair them with `re.compile` when the same pattern is used repeatedly.\n\n:::program\n```python\nimport re\n\ntext = "Ada: 10, Grace: 9"\npattern = r"([A-Za-z]+): (\\d+)"\n\nfor name, score in re.findall(pattern, text):\n print(name, int(score))\n\nmatch = re.search(r"Grace: (\\d+)", text)\nprint(match.group(1))\nprint("Grace" in text)\n\nstart = re.match(r"Ada", text)\nprint(start is not None)\nprint(re.match(r"Grace", text))\n\nscoreline = re.compile(pattern)\nprint(scoreline.findall(text))\n\ncasey = "ADA: 11"\nprint(re.search(r"ada", casey, flags=re.IGNORECASE).group(0))\n\nprint(re.sub(r"\\d+", "?", text))\n```\n:::\n\n:::cell\nRaw strings keep backslashes readable in regex patterns. Capturing groups return just the pieces inside parentheses.\n\n```python\nimport re\n\ntext = "Ada: 10, Grace: 9"\npattern = r"([A-Za-z]+): (\\d+)"\n\nfor name, score in re.findall(pattern, text):\n print(name, int(score))\n```\n\n```output\nAda 10\nGrace 9\n```\n:::\n\n:::cell\n`re.search()` finds the first match. A match object exposes captured groups by position.\n\n```python\nmatch = re.search(r"Grace: (\\d+)", text)\nprint(match.group(1))\n```\n\n```output\n9\n```\n:::\n\n:::cell\nFor a simple substring check, ordinary string membership is clearer than regex.\n\n```python\nprint("Grace" in text)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\n`re.match` only matches at the start of the string; `re.search` finds the first match anywhere. Picking the right one keeps anchoring intent visible without an explicit `^`.\n\n```python\nstart = re.match(r"Ada", text)\nprint(start is not None)\nprint(re.match(r"Grace", text))\n```\n\n```output\nTrue\nNone\n```\n:::\n\n:::cell\n`re.compile` produces a reusable pattern object whose methods skip the parser on each call. Reach for it when the same pattern runs in a loop.\n\n```python\nscoreline = re.compile(pattern)\nprint(scoreline.findall(text))\n```\n\n```output\n[(\'Ada\', \'10\'), (\'Grace\', \'9\')]\n```\n:::\n\n:::cell\nFlags such as `re.IGNORECASE` adjust matching without changing the pattern. `re.sub` replaces every match with a replacement string and returns the rewritten text.\n\n```python\ncasey = "ADA: 11"\nprint(re.search(r"ada", casey, flags=re.IGNORECASE).group(0))\n\nprint(re.sub(r"\\d+", "?", text))\n```\n\n```output\nADA\nAda: ?, Grace: ?\n```\n:::\n\n:::note\n- Use raw strings for regex patterns so backslashes are easier to read.\n- Use capturing groups when the point is extraction, not just matching.\n- `re.match` anchors at the start; `re.search` finds the first match anywhere.\n- `re.compile` saves work when the pattern runs more than once.\n- `re.sub` rewrites matches; flags like `re.IGNORECASE` change matching behavior without rewriting the pattern.\n- Reach for string methods before regex when the pattern is simple.\n:::\n', 'runtime-type-checks.md': '+++\nslug = "runtime-type-checks"\ntitle = "Runtime Type Checks"\nsection = "Types"\nsummary = "type, isinstance, and issubclass inspect runtime relationships."\ndoc_path = "/library/functions.html#isinstance"\nsee_also = [\n "type-hints",\n "protocols",\n "casts-and-any",\n "abstract-base-classes",\n]\n+++\n\nRuntime type checks inspect real objects while the program is running. They are different from type hints, which mostly guide tools before the program runs.\n\nUse `type()` when the exact class matters, `isinstance()` when subclasses should count, and `issubclass()` when checking class relationships. Most APIs prefer behavior over type checks, but runtime checks are useful at input boundaries.\n\nDo not turn every function into a wall of `isinstance()` calls. If the code only needs an object that can perform an operation, duck typing or a protocol may be clearer.\n\n:::program\n```python\nclass Animal:\n pass\n\nclass Dog(Animal):\n pass\n\npet = Dog()\n\nprint(type(pet).__name__)\nprint(type(pet) is Animal)\nprint(isinstance(pet, Animal))\nprint(issubclass(Dog, Animal))\n```\n:::\n\n:::cell\n`type()` reports the exact runtime class. A `Dog` instance is not exactly an `Animal` instance.\n\n```python\nclass Animal:\n pass\n\nclass Dog(Animal):\n pass\n\npet = Dog()\nprint(type(pet).__name__)\nprint(type(pet) is Animal)\n```\n\n```output\nDog\nFalse\n```\n:::\n\n:::cell\n`isinstance()` accepts subclasses, which is usually what API boundaries want.\n\n```python\nprint(isinstance(pet, Dog))\nprint(isinstance(pet, Animal))\n```\n\n```output\nTrue\nTrue\n```\n:::\n\n:::cell\n`issubclass()` checks class relationships rather than individual objects.\n\n```python\nprint(issubclass(Dog, Animal))\n```\n\n```output\nTrue\n```\n:::\n\n:::note\n- `type()` is exact; `isinstance()` follows inheritance.\n- Runtime checks inspect objects, not static annotations.\n- Prefer behavior, protocols, or clear validation over scattered type checks.\n:::\n', 'scope-global-nonlocal.md': '+++\nslug = "scope-global-nonlocal"\ntitle = "Global and Nonlocal"\nsection = "Functions"\nsummary = "global and nonlocal choose which outer binding assignment should update."\ndoc_path = "/reference/simple_stmts.html#the-global-statement"\nsee_also = [\n "variables",\n "closures",\n "functions",\n]\n+++\n\nAssignment normally creates or updates a local name inside the current function. `global` and `nonlocal` are explicit escape hatches for rebinding names outside that local scope.\n\nUse `nonlocal` when an inner function should update a name in an enclosing function. Use `global` rarely; passing values and returning results is usually clearer.\n\nThese statements affect name binding, not object mutation. Mutating a shared list is different from rebinding the name itself.\n\n:::program\n```python\ncount = 0\n\ndef bump_global():\n global count\n count += 1\n\nbump_global()\nprint(count)\n\n\ndef make_counter():\n total = 0\n def bump():\n nonlocal total\n total += 1\n return total\n return bump\n\ncounter = make_counter()\nprint(counter())\nprint(counter())\n```\n:::\n\n:::cell\n`global` tells assignment to update a module-level binding. Without it, `count += 1` would try to assign a local `count`.\n\n```python\ncount = 0\n\ndef bump_global():\n global count\n count += 1\n\nbump_global()\nprint(count)\n```\n\n```output\n1\n```\n:::\n\n:::cell\n`nonlocal` tells assignment to update a binding in the nearest enclosing function scope. This is useful for small closures that keep state.\n\n```python\ndef make_counter():\n total = 0\n def bump():\n nonlocal total\n total += 1\n return total\n return bump\n\ncounter = make_counter()\nprint(counter())\nprint(counter())\n```\n\n```output\n1\n2\n```\n:::\n\n:::note\n- Assignment inside a function is local unless declared otherwise.\n- Prefer `nonlocal` for closure state and avoid `global` unless module state is truly intended.\n- Passing values and returning results is usually easier to test than rebinding outer names.\n:::\n', 'sentinel-iteration.md': '+++\nslug = "sentinel-iteration"\ntitle = "Sentinel Iteration"\nsection = "Iteration"\nsummary = "iter(callable, sentinel) repeats calls until a marker value appears."\ndoc_path = "/library/functions.html#iter"\n+++\n\n`iter(callable, sentinel)` keeps calling a zero-argument callable and yields each result until the callable returns the sentinel value. It is the right shape for repeated reads from files, sockets, or queues — sources where each call produces the next chunk and a known marker means "no more".\n\nReach for it instead of writing `while True:` plus a manual break when the loop body would do nothing else but read and check. The two-argument form turns a polling callable into something that composes with `for` loops, comprehensions, and other iterator helpers.\n\nThe callable must take no arguments. Wrap a parameterized reader in a small lambda or method that closes over the parameters when the underlying API needs them.\n\n:::program\n```python\nlines = iter(["alpha", "beta", ""])\n\ndef read_line():\n return next(lines)\n\nfor line in iter(read_line, ""):\n print(line.upper())\n```\n:::\n\n:::cell\nA zero-argument callable produces one value at a time.\n\n```python\nlines = iter(["alpha", "beta", ""])\n\ndef read_line():\n return next(lines)\n\nfor line in iter(read_line, ""):\n print(line.upper())\n```\n\n```output\nALPHA\nBETA\n```\n:::\n\n:::note\n- A zero-argument callable produces one value at a time.\n- The sentinel value stops the loop without appearing in the output.\n- This form is useful for repeated reads from files, sockets, or queues.\n:::\n', 'sets.md': '+++\nslug = "sets"\ntitle = "Sets"\nsection = "Collections"\nsummary = "Sets store unique values and make membership checks explicit."\ndoc_path = "/tutorial/datastructures.html#sets"\n+++\n\nSets store unique hashable values. Use them when membership and de-duplication matter more than order.\n\nA list can answer membership with `in`, but a set communicates that membership is the main operation. Set algebra then expresses how groups relate to each other.\n\nBecause sets are unordered, examples often wrap output in `sorted()` so the display is deterministic.\n\n:::program\n```python\nlanguages = ["python", "go", "python"]\nunique_languages = set(languages)\nprint(sorted(unique_languages))\n\nallowed = {"python", "rust"}\nprint("python" in allowed)\nprint("ruby" in allowed)\n\ncompiled = {"go", "rust"}\nprint(sorted(allowed | compiled))\nprint(sorted(allowed & compiled))\nprint(sorted(allowed - compiled))\n```\n:::\n\n:::cell\nCreating a set removes duplicates. Keep a list when order and repeated values matter; convert to a set when uniqueness is the point.\n\n```python\nlanguages = ["python", "go", "python"]\nunique_languages = set(languages)\nprint(sorted(unique_languages))\n```\n\n```output\n[\'go\', \'python\']\n```\n:::\n\n:::cell\nMembership checks are the everyday set operation. A list can also use `in`, but a set says that membership is central to the data shape.\n\n```python\nallowed = {"python", "rust"}\nprint("python" in allowed)\nprint("ruby" in allowed)\n```\n\n```output\nTrue\nFalse\n```\n:::\n\n:::cell\nUnion, intersection, and difference describe relationships between groups without manual loops.\n\n```python\ncompiled = {"go", "rust"}\nprint(sorted(allowed | compiled))\nprint(sorted(allowed & compiled))\nprint(sorted(allowed - compiled))\n```\n\n```output\n[\'go\', \'python\', \'rust\']\n[\'rust\']\n[\'python\']\n```\n:::\n\n:::note\n- Use lists when order and repeated values matter.\n- Use sets when uniqueness and membership are the main operations.\n- Prefer lists when order or repeated values are part of the meaning.\n- Sets are unordered, so sort them when examples need deterministic display.\n:::\n', 'slices.md': '+++\nslug = "slices"\ntitle = "Slices"\nsection = "Collections"\nsummary = "Slices copy meaningful ranges from ordered sequences."\ndoc_path = "/tutorial/introduction.html#lists"\n+++\n\nSlicing reads a range from an ordered sequence with `start:stop:step`. It exists because Python code often needs a meaningful piece of a sequence: a page, a prefix, a tail, a stride, or a reversed view.\n\nThe stop index is excluded. That convention makes lengths and adjacent ranges line up: `items[:3]` and `items[3:]` split a sequence without overlap.\n\nSlices return new sequence objects for built-in lists and strings. Use indexing for one item; use slicing when the result should still be a sequence.\n\n:::program\n```python\nletters = ["a", "b", "c", "d", "e", "f"]\nfirst_page = letters[:3]\nrest = letters[3:]\nprint(first_page)\nprint(rest)\n\nmiddle = letters[1:5]\nevery_other = letters[::2]\nreversed_letters = letters[::-1]\nprint(middle)\nprint(every_other)\nprint(reversed_letters)\nprint(letters)\n```\n:::\n\n:::cell\nOmitted bounds mean “from the beginning” or “through the end.” Because the stop index is excluded, adjacent slices split a sequence cleanly.\n\n```python\nletters = ["a", "b", "c", "d", "e", "f"]\nfirst_page = letters[:3]\nrest = letters[3:]\nprint(first_page)\nprint(rest)\n```\n\n```output\n[\'a\', \'b\', \'c\']\n[\'d\', \'e\', \'f\']\n```\n:::\n\n:::cell\nUse `start:stop` for a middle range and `step` when you want to skip or walk backward. These operations return new lists; the original list is unchanged.\n\n```python\nmiddle = letters[1:5]\nevery_other = letters[::2]\nreversed_letters = letters[::-1]\nprint(middle)\nprint(every_other)\nprint(reversed_letters)\nprint(letters)\n```\n\n```output\n[\'b\', \'c\', \'d\', \'e\']\n[\'a\', \'c\', \'e\']\n[\'f\', \'e\', \'d\', \'c\', \'b\', \'a\']\n[\'a\', \'b\', \'c\', \'d\', \'e\', \'f\']\n```\n:::\n\n:::note\n- Slice stop indexes are excluded, so adjacent ranges compose cleanly.\n- Omitted bounds mean the beginning or end of the sequence.\n- A negative step walks backward; `[::-1]` is a common reversed-copy idiom.\n:::\n', 'sorting.md': '+++\nslug = "sorting"\ntitle = "Sorting"\nsection = "Collections"\nsummary = "sorted returns a new ordered list and key functions choose the sort value."\ndoc_path = "/howto/sorting.html"\n+++\n\n`sorted()` accepts any iterable and returns a new list. The original collection is left untouched, which makes `sorted()` useful in expressions and pipelines.\n\nUse `key=` to say what value should be compared for each item. This is the idiomatic way to sort records, tuples, dictionaries, and objects by a field.\n\nUse `reverse=True` for descending order. Use `list.sort()` instead when you intentionally want to mutate an existing list in place.\n\n:::program\n```python\nnames = ["Guido", "Ada", "Grace"]\nprint(sorted(names))\nprint(names)\n\nusers = [\n {"name": "Ada", "score": 10},\n {"name": "Guido", "score": 8},\n {"name": "Grace", "score": 10},\n]\nranked = sorted(users, key=lambda user: user["score"], reverse=True)\nprint([user["name"] for user in ranked])\n\nusers.sort(key=lambda user: user["name"])\nprint([user["name"] for user in users])\n```\n:::\n\n:::cell\n`sorted()` returns a new list. Printing the original list afterward shows that the input order did not change.\n\n```python\nnames = ["Guido", "Ada", "Grace"]\nprint(sorted(names))\nprint(names)\n```\n\n```output\n[\'Ada\', \'Grace\', \'Guido\']\n[\'Guido\', \'Ada\', \'Grace\']\n```\n:::\n\n:::cell\nA key function computes the value to compare. Here the records are sorted by score, highest first, and the output shows the resulting order.\n\n```python\nusers = [\n {"name": "Ada", "score": 10},\n {"name": "Guido", "score": 8},\n {"name": "Grace", "score": 10},\n]\nranked = sorted(users, key=lambda user: user["score"], reverse=True)\nprint([user["name"] for user in ranked])\n```\n\n```output\n[\'Ada\', \'Grace\', \'Guido\']\n```\n:::\n\n:::cell\n`list.sort()` sorts the list in place. Use it when mutation is the point and no separate sorted copy is needed.\n\n```python\nusers.sort(key=lambda user: user["name"])\nprint([user["name"] for user in users])\n```\n\n```output\n[\'Ada\', \'Grace\', \'Guido\']\n```\n:::\n\n:::note\n- `sorted()` makes a new list; `list.sort()` mutates an existing list.\n- `key=` should return the value Python compares for each item.\n- Python\'s sort is stable, so equal keys keep their original relative order.\n:::\n', 'special-methods.md': '+++\nslug = "special-methods"\ntitle = "Special Methods"\nsection = "Data Model"\nsummary = "Special methods connect your objects to Python syntax and built-ins."\ndoc_path = "/reference/datamodel.html#special-method-names"\nsee_also = [\n "container-protocols",\n "operator-overloading",\n "callable-objects",\n "context-managers",\n]\n+++\n\nSpecial methods, often called dunder methods, connect user-defined classes to Python syntax and built-ins such as len(), iter(), and repr().\n\nImplementing these methods lets your objects participate in Python protocols rather than forcing callers to learn custom method names for common operations.\n\nGood special methods make objects feel boring in the best way: they work with the language features Python programmers already know.\n\n:::program\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __len__(self):\n return len(self.items)\n\n def __iter__(self):\n return iter(self.items)\n\n def __repr__(self):\n return f"Bag({self.items!r})"\n\n def __str__(self):\n return ", ".join(self.items)\n\n def __eq__(self, other):\n return isinstance(other, Bag) and self.items == other.items\n\n def __hash__(self):\n return hash(tuple(self.items))\n\n def __lt__(self, other):\n return len(self.items) < len(other.items)\n\n def __contains__(self, item):\n return item in self.items\n\n def __getitem__(self, index):\n return self.items[index]\n\n def __setitem__(self, index, value):\n self.items[index] = value\n\n def __bool__(self):\n return bool(self.items)\n\nbag = Bag(["a", "b"])\nprint(len(bag))\nprint(list(bag))\nprint(bag)\nprint(repr(bag))\nprint(Bag(["a", "b"]) == Bag(["a", "b"]))\nprint(Bag(["a"]) < Bag(["a", "b"]))\nprint(hash(Bag(["a"])) == hash(Bag(["a"])))\nprint("a" in bag)\nprint(bag[0])\nbag[1] = "z"\nprint(list(bag))\nprint(bool(Bag([])))\n\n\nclass Multiplier:\n def __init__(self, factor):\n self.factor = factor\n\n def __call__(self, value):\n return value * self.factor\n\ntriple = Multiplier(3)\nprint(triple(5))\n\n\nclass Trace:\n def __enter__(self):\n print("enter")\n return self\n\n def __exit__(self, *exc):\n print("exit")\n return False\n\nwith Trace():\n print("inside")\n```\n:::\n\n:::cell\nStart with a normal class that stores its data. Special methods build on ordinary instance state.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\nbag = Bag(["a", "b"])\nprint(bag.items)\n```\n\n```output\n[\'a\', \'b\']\n```\n:::\n\n:::cell\nImplement `__len__` to let `len()` ask the object for its size using Python\'s standard protocol.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __len__(self):\n return len(self.items)\n\nbag = Bag(["a", "b"])\nprint(len(bag))\n```\n\n```output\n2\n```\n:::\n\n:::cell\nImplement `__iter__` to make the object iterable. Then tools such as `list()` can consume it without a custom method name.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __len__(self):\n return len(self.items)\n\n def __iter__(self):\n return iter(self.items)\n\nbag = Bag(["a", "b"])\nprint(list(bag))\n```\n\n```output\n[\'a\', \'b\']\n```\n:::\n\n:::cell\nImplement `__repr__` to give the object a useful developer-facing representation when it is printed or inspected. With no `__str__` defined, `print()` falls back to `__repr__`.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __len__(self):\n return len(self.items)\n\n def __iter__(self):\n return iter(self.items)\n\n def __repr__(self):\n return f"Bag({self.items!r})"\n\nbag = Bag(["a", "b"])\nprint(bag)\n```\n\n```output\nBag([\'a\', \'b\'])\n```\n:::\n\n:::cell\nAdd `__str__` for an end-user representation. `print()` and `str()` prefer `__str__`; `repr()` and the REPL still use `__repr__`. Keep `__repr__` unambiguous for debugging and let `__str__` be the friendly form.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __repr__(self):\n return f"Bag({self.items!r})"\n\n def __str__(self):\n return ", ".join(self.items)\n\nbag = Bag(["a", "b"])\nprint(bag)\nprint(repr(bag))\n```\n\n```output\na, b\nBag([\'a\', \'b\'])\n```\n:::\n\n:::cell\n`__eq__` decides what equality means for the type. Defining `__eq__` removes the default `__hash__`, so add `__hash__` back when instances should work in sets or as dict keys. `__lt__` enables `<` and, with the rest of the order family, `sorted()`.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __eq__(self, other):\n return isinstance(other, Bag) and self.items == other.items\n\n def __hash__(self):\n return hash(tuple(self.items))\n\n def __lt__(self, other):\n return len(self.items) < len(other.items)\n\nprint(Bag(["a", "b"]) == Bag(["a", "b"]))\nprint(Bag(["a"]) < Bag(["a", "b"]))\nprint(hash(Bag(["a"])) == hash(Bag(["a"])))\n```\n\n```output\nTrue\nTrue\nTrue\n```\n:::\n\n:::cell\nThe container protocols make instances behave like built-in containers. `__contains__` powers `in`, `__getitem__`/`__setitem__` power subscription, and `__bool__` decides truthiness for `if` and `while`. See [container-protocols](/data-model/container-protocols) for the full surface.\n\n```python\nclass Bag:\n def __init__(self, items):\n self.items = list(items)\n\n def __contains__(self, item):\n return item in self.items\n\n def __getitem__(self, index):\n return self.items[index]\n\n def __setitem__(self, index, value):\n self.items[index] = value\n\n def __bool__(self):\n return bool(self.items)\n\nbag = Bag(["a", "b"])\nprint("a" in bag)\nprint(bag[0])\nbag[1] = "z"\nprint(bag.items)\nprint(bool(Bag([])))\n```\n\n```output\nTrue\na\n[\'a\', \'z\']\nFalse\n```\n:::\n\n:::cell\n`__call__` makes an instance callable like a function — useful for stateful operations whose configuration deserves a name. `__enter__` and `__exit__` make a class a context manager so it can be used with `with`. The focused [callable-objects](/data-model/callable-objects) and [context-managers](/data-model/context-managers) pages go deeper.\n\n```python\nclass Multiplier:\n def __init__(self, factor):\n self.factor = factor\n\n def __call__(self, value):\n return value * self.factor\n\ntriple = Multiplier(3)\nprint(triple(5))\n\n\nclass Trace:\n def __enter__(self):\n print("enter")\n return self\n\n def __exit__(self, *exc):\n print("exit")\n return False\n\nwith Trace():\n print("inside")\n```\n\n```output\n15\nenter\ninside\nexit\n```\n:::\n\n:::note\n- Dunder methods are looked up by Python\'s data model protocols.\n- `__repr__` is the developer-facing form; `__str__` is the user-facing form. `print()` falls back to `__repr__` when `__str__` is missing.\n- Defining `__eq__` removes the default `__hash__`; restore it when the type should be hashable.\n- Container protocols (`__contains__`, `__getitem__`, `__setitem__`, `__bool__`) make instances behave like built-in containers.\n- `__call__` makes instances callable; `__enter__`/`__exit__` make them context managers.\n- Implement the smallest protocol that makes your object feel native.\n:::\n', 'string-formatting.md': '+++\nslug = "string-formatting"\ntitle = "String Formatting"\nsection = "Text"\nsummary = "f-strings turn values into readable text at the point of use."\ndoc_path = "/tutorial/inputoutput.html#formatted-string-literals"\n+++\n\nFormatted string literals, or f-strings, exist because programs constantly need to turn values into human-readable text. They keep the expression next to the words it explains.\n\nFormat specifications after `:` control presentation details such as width, alignment, padding, and precision. This separates the value being computed from the way it should be displayed.\n\nUse f-strings for most new formatting code. They relate directly to expressions: anything inside braces is evaluated, then formatted into the surrounding string.\n\n:::program\n```python\nname = "Ada"\nscore = 9.5\nrank = 1\n\nmessage = f"{name} scored {score}"\nprint(message)\n\nrow = f"{rank:>2} | {name:<8} | {score:05.1f}"\nprint(row)\n\nprint(f"{score = }")\n```\n:::\n\n:::cell\nAn f-string evaluates expressions inside braces and inserts their string form into the surrounding text. This is clearer than joining several converted values by hand.\n\n```python\nname = "Ada"\nscore = 9.5\nrank = 1\n\nmessage = f"{name} scored {score}"\nprint(message)\n```\n\n```output\nAda scored 9.5\n```\n:::\n\n:::cell\nFormat specifications after `:` control display without changing the underlying values. Here the rank is right-aligned, the name is left-aligned, and the score is padded to one decimal place.\n\n```python\nrow = f"{rank:>2} | {name:<8} | {score:05.1f}"\nprint(row)\n```\n\n```output\n 1 | Ada | 009.5\n```\n:::\n\n:::cell\nThe debug form with `=` is useful while learning or logging because it prints both the expression and the value.\n\n```python\nprint(f"{score = }")\n```\n\n```output\nscore = 9.5\n```\n:::\n\n:::note\n- Use `f"..."` strings for most new formatting code.\n- Expressions inside braces are evaluated before formatting.\n- Format specifications after `:` control alignment, width, padding, and precision.\n:::\n', 'strings.md': '+++\nslug = "strings"\ntitle = "Strings"\nsection = "Text"\nsummary = "Strings are immutable Unicode text sequences."\ndoc_path = "/library/stdtypes.html#text-sequence-type-str"\n+++\n\nPython strings are immutable Unicode text sequences. A `str` stores text as Unicode code points, so it can represent English, Thai, accented letters, emoji, and ordinary ASCII with the same type.\n\nUnicode matters because text length and byte length are different questions. The English word `"hello"` uses five code points and five UTF-8 bytes because ASCII characters encode as one byte each. The Thai greeting `"สวัสดี"` has six code points but needs eighteen UTF-8 bytes.\n\nUse `str` when you mean text, and encode to `bytes` only at boundaries such as files, network protocols, and binary APIs. String operations such as `upper()` and `strip()` return new strings instead of changing the original.\n\n:::program\n```python\nenglish = "hello"\nfrench = "café"\nthai = "สวัสดี"\n\nfor label, word in [("English", english), ("French", french), ("Thai", thai)]:\n print(label, word, len(word), len(word.encode("utf-8")))\n\nprint(thai[0])\nprint([hex(ord(char)) for char in thai[:2]])\n\ntext = " café "\nclean = text.strip()\nprint(clean)\nprint(clean.upper())\nprint(clean.encode("utf-8"))\n```\n:::\n\n:::cell\nCompare three words by code-point count and UTF-8 byte count. ASCII characters take one byte each (`hello` → 5 bytes); the `é` in `café` is one code point but two UTF-8 bytes; each Thai character takes three. The `str` type abstracts over all three.\n\n```python\nenglish = "hello"\nfrench = "café"\nthai = "สวัสดี"\n\nfor label, word in [("English", english), ("French", french), ("Thai", thai)]:\n print(label, word, len(word), len(word.encode("utf-8")))\n```\n\n```output\nEnglish hello 5 5\nFrench café 4 5\nThai สวัสดี 6 18\n```\n:::\n\n:::cell\nIndexing and iteration work with Unicode code points, not encoded bytes. `ord()` returns the integer code point, which is often displayed in hexadecimal when teaching text encoding.\n\n```python\nprint(thai[0])\nprint([hex(ord(char)) for char in thai[:2]])\n```\n\n```output\nส\n[\'0xe2a\', \'0xe27\']\n```\n:::\n\n:::cell\nString methods return new strings because strings are immutable. Encoding turns text into bytes when another system needs a byte representation.\n\n```python\ntext = " café "\nclean = text.strip()\nprint(clean)\nprint(clean.upper())\nprint(clean.encode("utf-8"))\n```\n\n```output\ncafé\nCAFÉ\nb\'caf\\xc3\\xa9\'\n```\n:::\n\n:::note\n- Use `str` for text and `bytes` for binary data.\n- `len(text)` counts Unicode code points; `len(text.encode("utf-8"))` counts encoded bytes.\n- ASCII text is a useful baseline because each ASCII code point is one UTF-8 byte.\n- String methods return new strings because strings are immutable.\n- User-visible “characters” can be more subtle than code points; combining marks and emoji sequences may need specialized text handling.\n:::\n', 'structured-data-shapes.md': '+++\nslug = "structured-data-shapes"\ntitle = "Structured Data Shapes"\nsection = "Classes"\nsummary = "dataclass, NamedTuple, and TypedDict each model records with different trade-offs."\ndoc_path = "/library/dataclasses.html"\nsee_also = [\n "dataclasses",\n "typed-dicts",\n "tuples",\n "classes",\n]\n+++\n\n`@dataclass`, `typing.NamedTuple`, and `typing.TypedDict` are three ways to give a record a name and a schema. They model the same data but differ in mutability, access syntax, and what the type information costs at runtime.\n\nA dataclass is a regular class with `__init__` and `__repr__` generated for you, so instances are mutable and attribute-accessed. A `NamedTuple` is a tuple subclass with named positions, so instances are immutable and support both `obj.field` and `obj[index]`. A `TypedDict` is a plain dict at runtime; the schema lives only in the type checker.\n\nPick the shape that matches the problem: a dataclass when methods or mutability help; a `NamedTuple` for small immutable records that benefit from unpacking; a `TypedDict` for JSON-shaped data that should stay as a dict at the boundary.\n\n:::program\n```python\nfrom dataclasses import dataclass\nfrom typing import NamedTuple, TypedDict\n\n@dataclass\nclass UserClass:\n name: str\n score: int\n\nclass UserTuple(NamedTuple):\n name: str\n score: int\n\nclass UserDict(TypedDict):\n name: str\n score: int\n\na = UserClass("Ada", 98)\nprint(a)\na.score = 100\nprint(a.score)\n\nb = UserTuple("Ada", 98)\nprint(b)\nprint(b.name, b[1])\nprint(b._replace(score=100))\n\nc: UserDict = {"name": "Ada", "score": 98}\nprint(c)\nprint(c["name"])\nprint(type(c).__name__)\n\nprint(isinstance(a, UserClass))\nprint(isinstance(b, tuple))\nprint(isinstance(c, dict))\n```\n:::\n\n:::cell\nA dataclass is a normal class with `__init__` and `__repr__` generated from the annotated fields. Instances are mutable, support attribute access, and can carry methods like any other class.\n\n```python\nfrom dataclasses import dataclass\n\n@dataclass\nclass UserClass:\n name: str\n score: int\n\na = UserClass("Ada", 98)\nprint(a)\na.score = 100\nprint(a.score)\n```\n\n```output\nUserClass(name=\'Ada\', score=98)\n100\n```\n:::\n\n:::cell\nA `NamedTuple` is a tuple subclass with named positions. Instances are immutable, support both `obj.field` and `obj[index]`, and the helper `_replace` produces a modified copy without mutating the original (since assigning to a field would fail).\n\n```python\nfrom typing import NamedTuple\n\nclass UserTuple(NamedTuple):\n name: str\n score: int\n\nb = UserTuple("Ada", 98)\nprint(b)\nprint(b.name, b[1])\nprint(b._replace(score=100))\n```\n\n```output\nUserTuple(name=\'Ada\', score=98)\nAda 98\nUserTuple(name=\'Ada\', score=100)\n```\n:::\n\n:::cell\nA `TypedDict` is a plain dictionary at runtime. The annotations exist only for the type checker, so the value behaves like any `dict` — useful for JSON-shaped data that crosses an API boundary as a mapping.\n\n```python\nfrom typing import TypedDict\n\nclass UserDict(TypedDict):\n name: str\n score: int\n\nc: UserDict = {"name": "Ada", "score": 98}\nprint(c)\nprint(c["name"])\nprint(type(c).__name__)\n```\n\n```output\n{\'name\': \'Ada\', \'score\': 98}\nAda\ndict\n```\n:::\n\n:::cell\nSame record, three runtime identities. The dataclass is its own class. The `NamedTuple` is literally a tuple. The `TypedDict` is literally a dict. That difference drives the choice: pick the form whose runtime behavior matches what the rest of the program already expects.\n\n```python\nprint(isinstance(a, UserClass))\nprint(isinstance(b, tuple))\nprint(isinstance(c, dict))\n```\n\n```output\nTrue\nTrue\nTrue\n```\n:::\n\n:::note\n- `@dataclass` — mutable, attribute access, methods; good default when behavior travels with data.\n- `typing.NamedTuple` — immutable, attribute + index access, tuple semantics; good for small records that flow through unpacking.\n- `typing.TypedDict` — runtime is `dict`, schema is type-checker-only; good for JSON-shaped data.\n- `collections.namedtuple` is the older, untyped form of `NamedTuple`; prefer the `typing` version in new code.\n:::\n', 'subprocesses.md': '+++\nslug = "subprocesses"\ntitle = "Subprocesses"\nsection = "Standard Library"\nsummary = "subprocess runs external commands with explicit arguments and captured outputs."\ndoc_path = "/library/subprocess.html"\nexpected_output = "child process\\n0\\n"\n+++\n\n`subprocess` is the standard boundary for running external commands. It starts another program, waits for it, and gives you a result object with the exit code and captured output.\n\nUse a list of arguments when possible, capture output when the parent program needs to inspect it, and treat a non-zero return code as a failure. The same ideas apply whether the child program is Python, Git, a compiler, or another command-line tool.\n\nThe important boundary is between Python objects and the operating system process table. Python prepares arguments and environment, then the child program runs independently and reports back through streams and an exit status.\n\n:::program\n```python\nimport subprocess\nimport sys\n\nresult = subprocess.run(\n [sys.executable, "-c", "print(\'child process\')"],\n text=True,\n capture_output=True,\n check=True,\n)\n\nprint(result.stdout.strip())\nprint(result.returncode)\n```\n:::\n\n:::unsupported\n`subprocess.run` spawns a child Python interpreter, captures its stdout and stderr (`capture_output=True`), decodes them as text (`text=True`), and raises `CalledProcessError` if the child exits non-zero (`check=True`). The returned `result` holds the captured streams and exit code as portable evidence the child ran. (This fragment runs in standard Python only — Dynamic Workers don\'t provide child processes.)\n\n```python\nresult = subprocess.run(\n [sys.executable, "-c", "print(\'child process\')"],\n text=True,\n capture_output=True,\n check=True,\n)\n```\n:::\n\n:::cell\n`subprocess.run()` starts a child process and waits for it. `capture_output=True` stores the child\'s standard output and error streams on the result object.\n\n```python\nimport subprocess\nimport sys\n\nresult = subprocess.run(\n [sys.executable, "-c", "print(\'child process\')"],\n text=True,\n capture_output=True,\n check=True,\n)\n\nprint(result.stdout.strip())\nprint(result.returncode)\n```\n\n```output\nchild process\n0\n```\n:::\n\n:::note\n- Use a list of arguments instead of shell strings when possible.\n- Capture output when the parent program needs to inspect it.\n- `check=True` turns non-zero exits into exceptions.\n:::\n', 'testing.md': '+++\nslug = "testing"\ntitle = "Testing"\nsection = "Standard Library"\nsummary = "Tests make expected behavior executable and repeatable."\ndoc_path = "/library/unittest.html"\nsee_also = [\n "assertions",\n "exceptions",\n "modules",\n]\n+++\n\nTests turn expected behavior into code that can be run again. The useful unit is usually a small example of behavior with clear input, action, and assertion.\n\nPython\'s `unittest` library provides test cases, assertions, suites, and runners. Projects often use `pytest` for ergonomics, but the same idea remains: a test names behavior and fails when the behavior changes.\n\nA broad testing practice also includes fixtures, integration tests, property tests, and coverage. This example stays on the smallest standard-library loop: define behavior, assert the result, run the suite, inspect success.\n\n:::program\n```python\nimport io\nimport unittest\n\n\ndef add(left, right):\n return left + right\n\n\ndef divide(left, right):\n if right == 0:\n raise ZeroDivisionError("denominator is zero")\n return left / right\n\n\nclass AddTests(unittest.TestCase):\n def setUp(self):\n self.zero = 0\n\n def test_adds_numbers(self):\n self.assertEqual(add(self.zero + 2, 3), 5)\n\n def test_adds_empty_strings(self):\n self.assertEqual(add("", "py"), "py")\n\n def test_divide_by_zero_raises(self):\n with self.assertRaises(ZeroDivisionError):\n divide(1, 0)\n\nloader = unittest.defaultTestLoader\nsuite = loader.loadTestsFromTestCase(AddTests)\nstream = io.StringIO()\nrunner = unittest.TextTestRunner(stream=stream, verbosity=0)\nresult = runner.run(suite)\nprint(result.testsRun)\nprint(result.wasSuccessful())\n```\n:::\n\n:::cell\nA test starts with behavior small enough to name. The function can be ordinary code; the test supplies a representative input and expected result.\n\n```python\ndef add(left, right):\n return left + right\n\nprint(add(2, 3))\n```\n\n```output\n5\n```\n:::\n\n:::cell\n`unittest.TestCase` groups test methods. `setUp` runs before each test method to build per-test fixtures, `assertEqual` checks values, and `assertRaises` asserts that a block raises the expected exception type.\n\n```python\nimport unittest\n\n\ndef divide(left, right):\n if right == 0:\n raise ZeroDivisionError("denominator is zero")\n return left / right\n\n\nclass AddTests(unittest.TestCase):\n def setUp(self):\n self.zero = 0\n\n def test_adds_numbers(self):\n self.assertEqual(add(self.zero + 2, 3), 5)\n\n def test_adds_empty_strings(self):\n self.assertEqual(add("", "py"), "py")\n\n def test_divide_by_zero_raises(self):\n with self.assertRaises(ZeroDivisionError):\n divide(1, 0)\n\nprint([name for name in dir(AddTests) if name.startswith("test_")])\n```\n\n```output\n[\'test_adds_empty_strings\', \'test_adds_numbers\', \'test_divide_by_zero_raises\']\n```\n:::\n\n:::cell\nA runner executes the suite and records whether every assertion passed. Capturing the runner stream keeps this page\'s output deterministic.\n\n```python\nimport io\n\nloader = unittest.defaultTestLoader\nsuite = loader.loadTestsFromTestCase(AddTests)\nstream = io.StringIO()\nrunner = unittest.TextTestRunner(stream=stream, verbosity=0)\nresult = runner.run(suite)\nprint(result.testsRun)\nprint(result.wasSuccessful())\n```\n\n```output\n3\nTrue\n```\n:::\n\n:::note\n- Test method names should describe behavior, not implementation details.\n- A good unit test is deterministic and independent of test order.\n- Use broader integration tests when the behavior depends on several components working together.\n:::\n', 'threads-and-processes.md': '+++\nslug = "threads-and-processes"\ntitle = "Threads and Processes"\nsection = "Standard Library"\nsummary = "Threads share memory, while processes run in separate interpreters."\ndoc_path = "/library/concurrent.futures.html"\nexpected_output = "[1, 4, 9]\\nProcessPoolExecutor\\n"\n+++\n\nThreads and processes are two ways to run work outside the current control path. Threads are useful for overlapping I/O-shaped waits, while processes are useful when CPU-bound work needs separate interpreter processes.\n\nThis is different from `asyncio`: threads and processes run callables through executors, while `async` code cooperatively awaits coroutines. Choose the smallest concurrency model that matches the bottleneck.\n\nThe executor interface lets callers submit ordinary functions without committing the rest of the code to one scheduling strategy. That makes it easier to compare thread and process boundaries at the call site.\n\n:::program\n```python\nfrom concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor\n\n\ndef square(number):\n return number * number\n\nwith ThreadPoolExecutor(max_workers=2) as pool:\n print(list(pool.map(square, [1, 2, 3])))\n\nprint(ProcessPoolExecutor.__name__)\n```\n:::\n\n:::unsupported\n`ThreadPoolExecutor` runs `square` across two worker threads sharing the same interpreter (and the GIL); `ProcessPoolExecutor` runs `pow` across two child processes with isolated memory. Each `pool.map` returns an iterator over results in input order, and the surrounding `with` block joins the workers when the body exits. (This fragment runs in standard Python only — Dynamic Workers don\'t provide native threads or child processes.)\n\n```python\nwith ThreadPoolExecutor(max_workers=2) as pool:\n print(list(pool.map(square, [1, 2, 3])))\n\nwith ProcessPoolExecutor(max_workers=2) as pool:\n print(list(pool.map(pow, [4, 5], [2, 2])))\n```\n:::\n\n:::cell\nA thread pool runs ordinary callables while sharing memory with the current process. `map()` returns results in input order.\n\n```python\nfrom concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor\n\n\ndef square(number):\n return number * number\n\nwith ThreadPoolExecutor(max_workers=2) as pool:\n print(list(pool.map(square, [1, 2, 3])))\n```\n\n```output\n[1, 4, 9]\n```\n:::\n\n:::cell\nA process pool uses separate Python processes. That boundary is heavier, but it can run CPU-bound work outside the current interpreter.\n\n```python\nprint(ProcessPoolExecutor.__name__)\n```\n\n```output\nProcessPoolExecutor\n```\n:::\n\n:::note\n- Threads share memory, so mutable shared state needs care.\n- Processes avoid shared interpreter state but require values to cross a process boundary.\n- Prefer `asyncio` for coroutine-based I/O and executors for ordinary blocking callables.\n:::\n', 'truth-and-size.md': '+++\nslug = "truth-and-size"\ntitle = "Truth and Size"\nsection = "Data Model"\nsummary = "__bool__ and __len__ decide how objects behave in truth tests and len()."\ndoc_path = "/reference/datamodel.html#object.__bool__"\nsee_also = [\n "truthiness",\n "special-methods",\n "container-protocols",\n]\n+++\n\nTruth tests ask an object whether it should count as true. Containers usually answer through their size, while domain objects can answer with `__bool__` when emptiness is not the right idea.\n\n`__len__` supports `len(obj)` and also provides a fallback truth value: length zero is false, non-zero length is true. `__bool__` is more direct and wins when both are present.\n\nUse these methods to match the meaning of your object. A queue can be false when it has no items; an account might be true only when it is active, regardless of its balance.\n\n:::program\n```python\nclass Inbox:\n def __init__(self, messages):\n self.messages = list(messages)\n\n def __len__(self):\n return len(self.messages)\n\nclass Account:\n def __init__(self, active):\n self.active = active\n\n def __bool__(self):\n return self.active\n\nprint(len(Inbox(["hi", "bye"])))\nprint(bool(Inbox([])))\nprint(bool(Account(False)))\n```\n:::\n\n:::cell\n`__len__` lets `len()` ask an object for its size.\n\n```python\nclass Inbox:\n def __init__(self, messages):\n self.messages = list(messages)\n\n def __len__(self):\n return len(self.messages)\n\nprint(len(Inbox(["hi", "bye"])))\n```\n\n```output\n2\n```\n:::\n\n:::cell\nIf a class has `__len__` but no `__bool__`, Python uses zero length as false.\n\n```python\nprint(bool(Inbox([])))\n```\n\n```output\nFalse\n```\n:::\n\n:::cell\n`__bool__` expresses truth directly when the answer is not just container size.\n\n```python\nclass Account:\n def __init__(self, active):\n self.active = active\n\n def __bool__(self):\n return self.active\n\nprint(bool(Account(False)))\n```\n\n```output\nFalse\n```\n:::\n\n:::note\n- Prefer `__len__` for sized containers.\n- Prefer `__bool__` when truth has domain meaning.\n- Keep truth tests unsurprising; surprising falsy objects make conditionals harder to read.\n:::\n', 'truthiness.md': '+++\nslug = "truthiness"\ntitle = "Truthiness"\nsection = "Basics"\nsummary = "Python conditions use truthiness, not only explicit booleans."\ndoc_path = "/library/stdtypes.html#truth-value-testing"\n+++\n\nTruthiness is one of Python\'s most important conveniences: conditions can test objects directly instead of requiring explicit boolean comparisons everywhere.\n\nEmpty containers, numeric zero, None, and False are false; most other values are true. This makes common checks such as if items: concise and idiomatic.\n\nUse truthiness when it reads naturally, but choose explicit comparisons when the distinction matters, such as checking whether a value is exactly None.\n\n:::program\n```python\nitems = []\nname = "Ada"\n\nif not items:\n print("no items")\n\nif name:\n print("has a name")\n\nprint(bool(0))\nprint(bool(42))\n```\n:::\n\n:::cell\nTruthiness is one of Python\'s most important conveniences: conditions can test objects directly instead of requiring explicit boolean comparisons everywhere.\n\nEmpty containers, numeric zero, None, and False are false; most other values are true. This makes common checks such as if items: concise and idiomatic.\n\n```python\nitems = []\nname = "Ada"\n\nif not items:\n print("no items")\n```\n\n```output\nno items\n```\n:::\n\n:::cell\nUse truthiness when it reads naturally, but choose explicit comparisons when the distinction matters, such as checking whether a value is exactly None.\n\n```python\nif name:\n print("has a name")\n```\n\n```output\nhas a name\n```\n:::\n\n:::cell\nUse truthiness when it reads naturally, but choose explicit comparisons when the distinction matters, such as checking whether a value is exactly None.\n\n```python\nprint(bool(0))\nprint(bool(42))\n```\n\n```output\nFalse\nTrue\n```\n:::\n\n:::note\n- Empty containers and zero-like numbers are false in conditions.\n- Use explicit comparisons when they communicate intent better than truthiness.\n:::\n', 'tuples.md': '+++\nslug = "tuples"\ntitle = "Tuples"\nsection = "Collections"\nsummary = "Tuples group a fixed number of positional values."\ndoc_path = "/tutorial/datastructures.html#tuples-and-sequences"\nsee_also = [\n "lists",\n "unpacking",\n "structured-data-shapes",\n]\n+++\n\nTuples are ordered, immutable sequences. They exist for small fixed groups where position has meaning: coordinates, RGB colors, database rows, and multiple return values.\n\nUse lists for variable-length collections of similar items. Use tuples when the number of positions is part of the data shape and unpacking can give each position a useful name.\n\nBecause tuples are immutable, you cannot append or replace positions in place. If the shape needs to grow or change, a list or dataclass is usually a better fit.\n\n:::program\n```python\npoint = (3, 4)\nx, y = point\nprint(x + y)\n\nred = (255, 0, 0)\nprint(red[0])\nprint(len(red))\n\nrecord = ("Ada", 10)\nname, score = record\nprint(f"{name}: {score}")\n\nscores = [10, 9, 8]\nscores.append(7)\nprint(scores)\n\nstudent = ("Ada", 2024, "math")\nname, year, subject = student\nprint(name, year, subject)\n```\n:::\n\n:::cell\nUse a tuple for a fixed-size record where each position has a known meaning. Unpacking turns those positions into names at the point of use.\n\n```python\npoint = (3, 4)\nx, y = point\nprint(x + y)\n```\n\n```output\n7\n```\n:::\n\n:::cell\nTuples are sequences, so indexing and `len()` work. They are different from lists because their length and item references are fixed after creation.\n\n```python\nred = (255, 0, 0)\nprint(red[0])\nprint(len(red))\n```\n\n```output\n255\n3\n```\n:::\n\n:::cell\nTuples pair naturally with multiple return values and unpacking. If the fields need names everywhere, graduate to a dataclass or named tuple.\n\n```python\nrecord = ("Ada", 10)\nname, score = record\nprint(f"{name}: {score}")\n```\n\n```output\nAda: 10\n```\n:::\n\n:::cell\nLists and tuples carry different intent. A list holds a variable number of similar items and grows with `append`; a tuple has a fixed shape where each position has its own meaning, and unpacking gives those positions names.\n\n```python\nscores = [10, 9, 8]\nscores.append(7)\nprint(scores)\n\nstudent = ("Ada", 2024, "math")\nname, year, subject = student\nprint(name, year, subject)\n```\n\n```output\n[10, 9, 8, 7]\nAda 2024 math\n```\n:::\n\n:::note\n- Tuples are immutable sequences with fixed length.\n- Use tuples for small records where position has meaning.\n- Use lists for variable-length collections of similar items.\n- Reach for a dataclass or `NamedTuple` when fields deserve names everywhere they\'re used.\n:::\n', 'type-aliases.md': '+++\nslug = "type-aliases"\ntitle = "Type Aliases"\nsection = "Types"\nsummary = "Type aliases give a meaningful name to a repeated type shape."\ndoc_path = "/library/typing.html#type-aliases"\nsee_also = [\n "type-hints",\n "newtype",\n "union-and-optional-types",\n]\n+++\n\nA type alias gives a name to an annotation shape. It helps readers and type checkers understand the role of a value without repeating a long type expression everywhere.\n\nPython 3.13 supports the `type` statement for explicit aliases. Older assignment-style aliases still appear in code, but the `type` statement makes the intent clear and creates a `TypeAliasType` object at runtime.\n\nAn alias does not create a new runtime type. If you need a static distinction between compatible values such as user IDs and order IDs, use `NewType` instead.\n\n:::program\n```python\ntype UserId = int\ntype Scores = dict[UserId, int]\nLegacyName = str\n\n\ndef best_user(scores: Scores) -> UserId:\n return max(scores, key=scores.get)\n\nscores: Scores = {1: 98, 2: 91}\nprint(best_user(scores))\nprint(UserId.__name__)\nprint(LegacyName("Ada"))\n```\n:::\n\n:::cell\nThe `type` statement names an annotation shape. Here `Scores` means a dictionary from user IDs to integer scores.\n\n```python\ntype UserId = int\ntype Scores = dict[UserId, int]\n\n\ndef best_user(scores: Scores) -> UserId:\n return max(scores, key=scores.get)\n\nscores: Scores = {1: 98, 2: 91}\nprint(best_user(scores))\n```\n\n```output\n1\n```\n:::\n\n:::cell\nModern aliases are runtime objects that keep their alias name for introspection.\n\n```python\nprint(UserId.__name__)\nprint(Scores.__name__)\n```\n\n```output\nUserId\nScores\n```\n:::\n\n:::cell\nAssignment-style aliases are still common, but they are just ordinary names bound to existing objects.\n\n```python\nLegacyName = str\nprint(LegacyName("Ada"))\nprint(LegacyName is str)\n```\n\n```output\nAda\nTrue\n```\n:::\n\n:::note\n- Use aliases to name repeated or domain-specific annotation shapes.\n- A type alias does not validate values at runtime.\n- Use `NewType` when two values share a runtime representation but should not be mixed statically.\n:::\n', 'type-hints.md': '+++\nslug = "type-hints"\ntitle = "Type Hints"\nsection = "Types"\nsummary = "Annotations document expected types and power static analysis."\ndoc_path = "/library/typing.html"\nsee_also = [\n "union-and-optional-types",\n "type-aliases",\n "generics-and-typevar",\n "runtime-type-checks",\n]\n+++\n\nType hints are annotations that document expected shapes for values, parameters, and return results. They exist so tools and readers can understand API boundaries before the program runs.\n\nPython stores many annotations but does not enforce most of them at runtime. Use type hints for communication and static analysis; use validation or exceptions when runtime checks are required.\n\nThe alternative to an annotation is prose, tests, or runtime validation. Good Python code often uses all three at important boundaries.\n\n:::program\n```python\nfrom typing import TypeAlias\n\ndef total(numbers: list[int]) -> int:\n return sum(numbers)\n\nprint(total([1, 2, 3]))\nprint(total.__annotations__)\n\n\ndef label(score: int) -> str:\n return f"score={score}"\n\nprint(label("high"))\n\n\ndef find(name: str, options: list[str]) -> str | None:\n return name if name in options else None\n\nprint(find("Ada", ["Ada", "Grace"]))\nprint(find("Guido", ["Ada", "Grace"]))\n\n\nfrom typing import Optional\n\ndef lookup(name: str) -> Optional[int]:\n table = {"Ada": 1815, "Grace": 1906}\n return table.get(name)\n\nprint(lookup("Ada"))\nprint(lookup("Guido"))\n\n\nScore: TypeAlias = int\n\ndef grade(score: Score) -> str:\n return "pass" if score >= 50 else "fail"\n\nprint(grade(72))\n```\n:::\n\n:::cell\nType hints document expected parameter and return shapes. Python still runs the function normally at runtime.\n\n```python\ndef total(numbers: list[int]) -> int:\n return sum(numbers)\n\nprint(total([1, 2, 3]))\n```\n\n```output\n6\n```\n:::\n\n:::cell\nPython stores annotations on the function object for tools and introspection. Type checkers use this information without changing the function call syntax.\n\n```python\nprint(total.__annotations__)\n```\n\n```output\n{\'numbers\': list[int], \'return\': }\n```\n:::\n\n:::cell\nMost hints are not runtime validation. This call passes a string where the hint says `int`; Python still calls the function because the body can format any value.\n\n```python\ndef label(score: int) -> str:\n return f"score={score}"\n\nprint(label("high"))\n```\n\n```output\nscore=high\n```\n:::\n\n:::cell\nUse `X | Y` (PEP 604) to express "either type". `str | None` says the result is a string or absent. `typing.Optional[X]` is the older, still-supported spelling for the same idea — `Optional[X]` is equivalent to `X | None`.\n\n```python\ndef find(name: str, options: list[str]) -> str | None:\n return name if name in options else None\n\nprint(find("Ada", ["Ada", "Grace"]))\nprint(find("Guido", ["Ada", "Grace"]))\n\n\nfrom typing import Optional\n\ndef lookup(name: str) -> Optional[int]:\n table = {"Ada": 1815, "Grace": 1906}\n return table.get(name)\n\nprint(lookup("Ada"))\nprint(lookup("Guido"))\n```\n\n```output\nAda\nNone\n1815\nNone\n```\n:::\n\n:::cell\n`TypeAlias` names a type so it can be reused with intent. `Score: TypeAlias = int` keeps the underlying type at runtime but lets the API talk about a domain concept rather than a primitive.\n\n```python\nfrom typing import TypeAlias\n\nScore: TypeAlias = int\n\ndef grade(score: Score) -> str:\n return "pass" if score >= 50 else "fail"\n\nprint(grade(72))\n```\n\n```output\npass\n```\n:::\n\n:::note\n- Python does not enforce most type hints at runtime.\n- Tools like type checkers and editors use annotations to catch mistakes earlier.\n- Use `X | Y` for unions and `Optional[X]` for "X or None"; both spellings mean the same thing.\n- Reach for `TypeAlias` when a domain name reads better than a raw primitive type.\n- Use runtime validation when untrusted input must be rejected while the program runs.\n:::\n', 'typed-dicts.md': '+++\nslug = "typed-dicts"\ntitle = "TypedDict"\nsection = "Types"\nsummary = "TypedDict describes dictionaries with known string keys."\ndoc_path = "/library/typing.html#typing.TypedDict"\nsee_also = [\n "dicts",\n "json",\n "dataclasses",\n "structured-data-shapes",\n]\n+++\n\n`TypedDict` describes dictionary records with known keys. It is useful for JSON-like data that should remain a dictionary instead of becoming a class instance.\n\nThe important boundary is static versus runtime behavior. Type checkers can know that `name` is a string and `score` is an integer, but at runtime the value is still an ordinary `dict`.\n\nUse `TypedDict` for external records and `dataclass` when your own program wants attribute access, methods, and construction behavior.\n\n:::program\n```python\nfrom typing import NotRequired, TypedDict\n\nclass User(TypedDict):\n name: str\n score: int\n nickname: NotRequired[str]\n\n\ndef describe(user: User) -> str:\n return f"{user[\'name\']}: {user[\'score\']}"\n\nrecord: User = {"name": "Ada", "score": 98}\nprint(describe(record))\nprint(isinstance(record, dict))\nprint(record.get("nickname", "none"))\n```\n:::\n\n:::cell\nUse `TypedDict` for JSON-like records that remain dictionaries.\n\n```python\nfrom typing import TypedDict\n\nclass User(TypedDict):\n name: str\n score: int\n\n\ndef describe(user: User) -> str:\n return f"{user[\'name\']}: {user[\'score\']}"\n\nrecord: User = {"name": "Ada", "score": 98}\nprint(describe(record))\n```\n\n```output\nAda: 98\n```\n:::\n\n:::cell\nAt runtime, a `TypedDict` value is still a plain dictionary.\n\n```python\nprint(isinstance(record, dict))\nprint(type(record).__name__)\n```\n\n```output\nTrue\ndict\n```\n:::\n\n:::cell\n`NotRequired` marks a key that type checkers should treat as optional. Runtime lookup still uses normal dictionary tools such as `get()`.\n\n```python\nfrom typing import NotRequired\n\nclass UserWithNickname(TypedDict):\n name: str\n score: int\n nickname: NotRequired[str]\n\nrecord: UserWithNickname = {"name": "Ada", "score": 98}\nprint(record.get("nickname", "none"))\n```\n\n```output\nnone\n```\n:::\n\n:::note\n- Use `TypedDict` for dictionary records from JSON or APIs.\n- Type checkers understand required and optional keys.\n- Runtime behavior is still ordinary dictionary behavior.\n:::\n', 'union-and-optional-types.md': '+++\nslug = "union-and-optional-types"\ntitle = "Union and Optional Types"\nsection = "Types"\nsummary = "The | operator describes values that may have more than one static type."\ndoc_path = "/library/typing.html#typing.Optional"\nsee_also = [\n "none",\n "type-hints",\n "match-statements",\n]\n+++\n\nA union type says that a value may have one of several static shapes. `int | str` means callers may pass either an integer or a string.\n\n`T | None` is the modern spelling for an optional value. The annotation documents that absence is expected, but the code still needs to handle `None` before using the non-optional behavior.\n\nUnions are useful at boundaries where input is flexible. Inside a function, narrow the value with an `is None`, `isinstance()`, or pattern check so the rest of the code has one clear shape.\n\n:::program\n```python\ndef label(value: int | str) -> str:\n return f"item-{value}"\n\n\ndef greeting(name: str | None) -> str:\n if name is None:\n return "hello guest"\n return f"hello {name.upper()}"\n\nprint(label(3))\nprint(label("A"))\nprint(greeting(None))\nprint(greeting("Ada"))\nprint(greeting.__annotations__)\n```\n:::\n\n:::cell\nUse `A | B` when a value may have either type. The function body should use operations that make sense for every member of the union.\n\n```python\ndef label(value: int | str) -> str:\n return f"item-{value}"\n\nprint(label(3))\nprint(label("A"))\n```\n\n```output\nitem-3\nitem-A\n```\n:::\n\n:::cell\n`str | None` means the function accepts either a string or explicit absence. Check for `None` before calling string methods.\n\n```python\ndef greeting(name: str | None) -> str:\n if name is None:\n return "hello guest"\n return f"hello {name.upper()}"\n\nprint(greeting(None))\nprint(greeting("Ada"))\n```\n\n```output\nhello guest\nhello ADA\n```\n:::\n\n:::cell\nUnion annotations are visible at runtime, but Python does not enforce them when the function is called.\n\n```python\nprint(greeting.__annotations__)\n```\n\n```output\n{\'name\': str | None, \'return\': }\n```\n:::\n\n:::note\n- Use `A | B` when a value may have either type.\n- `T | None` means absence is an expected case, not an error by itself.\n- Narrow unions before using behavior that belongs to only one member type.\n:::\n', 'unpacking.md': '+++\nslug = "unpacking"\ntitle = "Unpacking"\nsection = "Collections"\nsummary = "Unpacking binds names from sequences and mappings concisely."\ndoc_path = "/tutorial/datastructures.html#tuples-and-sequences"\n+++\n\nUnpacking binds multiple names from one iterable or mapping. It makes the structure of data visible at the point where values are introduced.\n\nStarred unpacking handles variable-length sequences by collecting the middle or remaining values. This keeps common head-tail patterns readable.\n\nDictionary unpacking with ** connects structured data to function calls. It is widely used in configuration, adapters, and code that bridges APIs.\n\n:::program\n```python\npoint = (3, 4)\nx, y = point\nprint(x, y)\n\nfirst, *middle, last = [1, 2, 3, 4]\nprint(first, middle, last)\n\ndef describe(name, language):\n print(name, language)\n\ndata = {"name": "Ada", "language": "Python"}\ndescribe(**data)\n```\n:::\n\n:::cell\nUnpacking binds multiple names from one iterable or mapping. It makes the structure of data visible at the point where values are introduced.\n\n```python\npoint = (3, 4)\nx, y = point\nprint(x, y)\n```\n\n```output\n3 4\n```\n:::\n\n:::cell\nStarred unpacking handles variable-length sequences by collecting the middle or remaining values. This keeps common head-tail patterns readable.\n\n```python\nfirst, *middle, last = [1, 2, 3, 4]\nprint(first, middle, last)\n```\n\n```output\n1 [2, 3] 4\n```\n:::\n\n:::cell\nDictionary unpacking with ** connects structured data to function calls. It is widely used in configuration, adapters, and code that bridges APIs.\n\nDictionary unpacking with ** connects structured data to function calls. It is widely used in configuration, adapters, and code that bridges APIs.\n\n```python\ndef describe(name, language):\n print(name, language)\n\ndata = {"name": "Ada", "language": "Python"}\ndescribe(**data)\n```\n\n```output\nAda Python\n```\n:::\n\n:::note\n- Starred unpacking collects the remaining values into a list.\n- Dictionary unpacking with ** is common when calling functions with structured data.\n- Prefer indexing when you need one position; prefer unpacking when naming several positions makes the shape clearer.\n:::\n', 'values.md': '+++\nslug = "values"\ntitle = "Values"\nsection = "Basics"\nsummary = "Python programs evaluate expressions into objects such as text, numbers, booleans, and None."\ndoc_path = "/library/stdtypes.html"\n+++\n\nA Python program works by evaluating expressions into values. Values are objects: text, integers, floats, booleans, `None`, and many richer types introduced later.\n\nNames point to values; they are not declarations that permanently fix a type. Operations usually produce new values, which you can print, store, compare, or pass to functions.\n\nThis page is a map, not the whole territory. Later pages explain the boundaries: equality vs identity, mutable vs immutable values, truthiness vs literal booleans, and `None` vs a missing key or an exception.\n\n:::program\n```python\ntext = "python"\ncount = 3\nratio = 2.5\nready = True\nmissing = None\n\nprint(type(text).__name__)\nprint(text.upper())\nprint(count + 4)\nprint(ratio * 2)\n\nprint(ready and count > 0)\nprint(missing is None)\n```\n:::\n\n:::cell\nStart with several built-in values. Python does not require declarations before binding these names, and each value is still an object with a type.\n\n```python\ntext = "python"\ncount = 3\nratio = 2.5\nready = True\nmissing = None\n\nprint(type(text).__name__)\n```\n\n```output\nstr\n```\n:::\n\n:::cell\nMethods and operators evaluate to new values. The original `text`, `count`, and `ratio` bindings remain ordinary objects you can reuse.\n\n```python\nprint(text.upper())\nprint(count + 4)\nprint(ratio * 2)\n```\n\n```output\nPYTHON\n7\n5.0\n```\n:::\n\n:::cell\nBoolean expressions combine facts, and `None` is checked by identity because it is a singleton absence marker.\n\n```python\nprint(ready and count > 0)\nprint(missing is None)\n```\n\n```output\nTrue\nTrue\n```\n:::\n\n:::note\n- Values are objects; names point to them and operations usually create new values.\n- Use `is None` for the absence marker, not `== None`.\n- This overview introduces boundaries that later pages explain in detail.\n:::\n', 'variables.md': '+++\nslug = "variables"\ntitle = "Variables"\nsection = "Basics"\nsummary = "Names are bound to values with assignment."\ndoc_path = "/reference/simple_stmts.html#assignment-statements"\n+++\n\nPython variables are names bound to objects. Assignment creates or rebinds a name; it does not require a declaration and it does not permanently attach a type to the name.\n\nRebinding changes which object a name refers to. Augmented assignment such as `+=` is the idiomatic way to update counters and accumulators.\n\nUse clear names for values that matter later. Python\'s flexibility makes naming more important, not less.\n\nUse assignment when a value needs a name for reuse or explanation. Prefer a direct expression when naming the intermediate value would add noise.\n\n:::program\n```python\nmessage = "hi"\nprint(message)\n\nmessage = "hello"\nprint(message)\n\ncount = 3\ncount += 1\nprint(count)\n```\n:::\n\n:::cell\nAssignment binds a name to a value. Once bound, the name can be used anywhere that value is needed.\n\n```python\nmessage = "hi"\nprint(message)\n```\n\n```output\nhi\n```\n:::\n\n:::cell\nAssignment can rebind the same name to a different value. The name is not permanently attached to the first object.\n\n```python\nmessage = "hello"\nprint(message)\n```\n\n```output\nhello\n```\n:::\n\n:::cell\nAugmented assignment reads the current binding, computes an updated value, and stores the result back under the same name.\n\n```python\ncount = 3\ncount += 1\nprint(count)\n```\n\n```output\n4\n```\n:::\n\n:::note\n- Python variables are names bound to objects, not boxes with fixed types.\n- Rebinding a name is normal.\n- Use augmented assignment for counters and accumulators.\n:::\n', 'virtual-environments.md': '+++\nslug = "virtual-environments"\ntitle = "Virtual Environments"\nsection = "Modules"\nsummary = "Virtual environments isolate a project\'s Python packages."\ndoc_path = "/library/venv.html"\nexpected_output = ".venv\\nTrue\\n"\n+++\n\nVirtual environments isolate a project\'s Python packages. They exist so one project can install dependencies without changing another project\'s environment.\n\nThe usual command-line workflow is `python -m venv .venv`, but Python also exposes the same feature through the `venv` module. This example creates a temporary environment so the example cleans up after itself.\n\nA virtual environment changes where Python looks for installed packages. It does not change the language, and it is separate from package layout, imports, and module names.\n\n:::program\n```python\nimport pathlib\nimport tempfile\nimport venv\n\nwith tempfile.TemporaryDirectory() as directory:\n env_path = pathlib.Path(directory) / ".venv"\n builder = venv.EnvBuilder(with_pip=False)\n builder.create(env_path)\n\n print(env_path.name)\n print((env_path / "pyvenv.cfg").exists())\n```\n:::\n\n:::unsupported\n`venv.EnvBuilder` configures the description of a new environment, then `create(".venv")` materialises it on disk as a directory containing its own interpreter and `site-packages`. `with_pip=False` skips bootstrapping pip — useful when the venv is for an isolated tool that doesn\'t need to install third-party packages. (This fragment runs in standard Python only — Dynamic Workers don\'t provide the `venv` module or a project environment workflow.)\n\n```python\nbuilder = venv.EnvBuilder(with_pip=False)\nbuilder.create(".venv")\n```\n:::\n\n:::cell\n`venv.EnvBuilder` creates the same kind of isolated environment as `python -m venv`. A temporary directory keeps the example from leaving project files behind.\n\n```python\nimport pathlib\nimport tempfile\nimport venv\n\nwith tempfile.TemporaryDirectory() as directory:\n env_path = pathlib.Path(directory) / ".venv"\n builder = venv.EnvBuilder(with_pip=False)\n builder.create(env_path)\n\n print(env_path.name)\n print((env_path / "pyvenv.cfg").exists())\n```\n\n```output\n.venv\nTrue\n```\n:::\n\n:::note\n- A virtual environment gives a project its own install location.\n- Inside a venv, `sys.prefix` usually differs from `sys.base_prefix`.\n- Use `python -m venv .venv` at the command line for everyday project setup.\n:::\n', 'warnings.md': '+++\nslug = "warnings"\ntitle = "Warnings"\nsection = "Errors"\nsummary = "warnings report soft problems without immediately stopping the program."\ndoc_path = "/library/warnings.html"\n+++\n\nwarnings report soft problems without immediately stopping the program. It exists to make a common boundary explicit instead of leaving the behavior implicit in a larger program.\n\nUse it when the problem shape matches the example, and prefer simpler neighboring tools when the extra machinery would hide the intent. The notes call out the boundary so the feature stays practical rather than decorative.\n\nThe example is small, deterministic, and focused on the semantic point. The complete source is editable below, while the walkthrough pairs the source with its output.\n\n:::program\n```python\nimport warnings\n\n\ndef old_name():\n warnings.warn("old_name is deprecated", DeprecationWarning, stacklevel=2)\n return "result"\n\nwarnings.simplefilter("always", DeprecationWarning)\nwith warnings.catch_warnings(record=True) as caught:\n print(old_name())\n print(caught[0].category.__name__)\n print(str(caught[0].message))\n```\n:::\n\n:::cell\nWarnings are useful for deprecations and soft failures.\n\n```python\nimport warnings\n\n\ndef old_name():\n warnings.warn("old_name is deprecated", DeprecationWarning, stacklevel=2)\n return "result"\n\nwarnings.simplefilter("always", DeprecationWarning)\nwith warnings.catch_warnings(record=True) as caught:\n print(old_name())\n print(caught[0].category.__name__)\n print(str(caught[0].message))\n```\n\n```output\nresult\nDeprecationWarning\nold_name is deprecated\n```\n:::\n\n:::note\n- Warnings are useful for deprecations and soft failures.\n- Filters decide whether warnings are ignored, shown, or turned into errors.\n- Tests often capture warnings to assert migration behavior.\n:::\n', 'while-loops.md': '+++\nslug = "while-loops"\ntitle = "While Loops"\nsection = "Control Flow"\nsummary = "while repeats until changing state makes a condition false."\ndoc_path = "/reference/compound_stmts.html#while"\n+++\n\nA `while` loop repeats while a condition remains true. Unlike `for`, which consumes an existing iterable, `while` is for state-driven repetition where the next step depends on what happened so far.\n\nThe loop body must make progress toward stopping. That progress might be decrementing a counter, reading until a sentinel value, or waiting until some external state changes.\n\nReach for `for` when you already have values to consume. Reach for `while` when the loop\'s own state decides whether another iteration is needed.\n\n:::program\n```python\nremaining = 3\nwhile remaining > 0:\n print(f"launch in {remaining}")\n remaining -= 1\nprint("liftoff")\n\nresponses = iter(["retry", "retry", "ok"])\nstatus = next(responses)\nwhile status != "ok":\n print(f"status: {status}")\n status = next(responses)\nprint(f"status: {status}")\n```\n:::\n\n:::cell\nUse `while` when the condition, not an iterable, controls repetition. Here the loop owns the countdown state and updates it each time through the body.\n\n```python\nremaining = 3\nwhile remaining > 0:\n print(f"launch in {remaining}")\n remaining -= 1\nprint("liftoff")\n```\n\n```output\nlaunch in 3\nlaunch in 2\nlaunch in 1\nliftoff\n```\n:::\n\n:::cell\nA sentinel loop stops when a special value appears. The loop does not know in advance how many retries it will need; it keeps going until the state says to stop.\n\n```python\nresponses = iter(["retry", "retry", "ok"])\nstatus = next(responses)\nwhile status != "ok":\n print(f"status: {status}")\n status = next(responses)\nprint(f"status: {status}")\n```\n\n```output\nstatus: retry\nstatus: retry\nstatus: ok\n```\n:::\n\n:::note\n- Use `while` when changing state decides whether the loop continues.\n- Update loop state inside the body so the condition can become false.\n- Prefer `for` when you already have a collection, range, iterator, or generator to consume.\n:::\n', 'yield-from.md': '+++\nslug = "yield-from"\ntitle = "Yield From"\nsection = "Iteration"\nsummary = "yield from delegates part of a generator to another iterable."\ndoc_path = "/reference/expressions.html#yield-expressions"\nsee_also = [\n "generators",\n "generator-expressions",\n "itertools",\n]\n+++\n\n`yield from` lets one generator yield every value from another iterable. It is a compact way to delegate part of a stream.\n\nUse it when a generator is mostly stitching together other iterables or sub-generators. It keeps the producer pipeline visible without writing a nested `for` loop.\n\nThe consumer still sees one stream of values.\n\n:::program\n```python\ndef page():\n yield "header"\n yield from ["intro", "body"]\n yield "footer"\n\nprint(list(page()))\n\n\ndef flatten(rows):\n for row in rows:\n yield from row\n\nprint(list(flatten([[1, 2], [3]])))\n```\n:::\n\n:::cell\n`yield from` delegates to another iterable. The caller receives one stream even though part of it came from a list.\n\n```python\ndef page():\n yield "header"\n yield from ["intro", "body"]\n yield "footer"\n\nprint(list(page()))\n```\n\n```output\n[\'header\', \'intro\', \'body\', \'footer\']\n```\n:::\n\n:::cell\nDelegation is useful when flattening nested iterables. `yield from row` replaces an inner loop that would yield each item by hand.\n\n```python\ndef flatten(rows):\n for row in rows:\n yield from row\n\nprint(list(flatten([[1, 2], [3]])))\n```\n\n```output\n[1, 2, 3]\n```\n:::\n\n:::note\n- `yield from iterable` yields each value from that iterable.\n- It keeps generator pipelines compact.\n- Use a plain `yield` when producing one value directly.\n:::\n'} diff --git a/src/marginalia.py b/src/marginalia.py new file mode 100644 index 0000000..bd2305b --- /dev/null +++ b/src/marginalia.py @@ -0,0 +1,2152 @@ +"""Marginalia attachments and figure registry. + +Authors of example markdown never touch this file. The project owner curates +named figures here and attaches them to cells via slug + anchor. + +Anchors today: + "cell-0", "cell-1", … each literate-program cell, zero-indexed. + The figure renders in a banner row AFTER the named cell. + +The renderer in app.py interleaves cells and banners: every cell keeps +its prose|code 2-column grid intact, and a banner row spanning both +columns sits between cells (or after the only cell on single-cell +examples). Multiple figures attached to the same cell share one +banner as a small multiple. Cells without an attached figure render +exactly as before. + +See docs/visual-explainer-spec.md for the full design and +docs/journey-visualisation-rubric.md for the figure-quality rubric. +""" + +from __future__ import annotations + +import html +from typing import Callable + +try: + from .marginalia_grammar import Canvas +except ImportError: # Cloudflare Workers import siblings without the package prefix. + from marginalia_grammar import Canvas + + +# ─── Named figures ───────────────────────────────────────────────────── + + +def aliasing_mutation(c: Canvas) -> None: + """Two names binding to one mutable list, before and after a mutation.""" + c.tag(0, 12, "before") + c.name_box(0, 18, "first") + c.name_box(0, 48, "second") + c.closed_arrow(60, 30, 86, 46, emphasis=False) + c.closed_arrow(60, 60, 86, 46, emphasis=False) + c.object_box(88, 32, "", '["python"]', w=88, h=28) + + c.tag(0, 100, "after append") + c.name_box(0, 108, "first") + c.name_box(0, 138, "second") + c.closed_arrow(60, 120, 86, 136, emphasis=False) + c.closed_arrow(60, 150, 86, 136, emphasis=False) + c.object_box(88, 122, "", '["python","workers"]', w=130, h=28) + + +def tuple_no_mutation(c: Canvas) -> None: + """The contrast: two names binding to one immutable tuple — no mutation possible.""" + c.tag(0, 12, "tuple — frozen") + c.name_box(0, 18, "first") + c.name_box(0, 48, "second") + c.closed_arrow(60, 30, 86, 46, emphasis=False) + c.closed_arrow(60, 60, 86, 46, emphasis=False) + c.object_box(88, 32, "", '("python",)', w=110, h=28) + + c.tag(0, 100, "no .append") + c.name_box(0, 108, "first") + c.name_box(0, 138, "second") + c.closed_arrow(60, 120, 86, 136, emphasis=False) + c.closed_arrow(60, 150, 86, 136, emphasis=False) + c.object_box(88, 122, "", '("python",)', w=110, h=28) + c.label(150, 170, "tuples raise AttributeError", anchor="middle") + + +def iterator_unroll(c: Canvas) -> None: + """Four passes of next() over a sequence, with a caret advancing each row.""" + items = list("abcd") + for i in range(4): + y = 8 + i * 30 + c.cells(20, y, items) + c.caret(20 + i * 24 + 12, y, emphasis=(i == 3)) + suffix = " — last" if i == 3 else "" + c.label(124, y + 16, f"next(){suffix}") + + +def scope_rings(c: Canvas) -> None: + """LEGB lookup: nested rings, lookup path traced from innermost outward.""" + c.frame(8, 6, 200, 100, label="B · built-in") + c.frame(28, 22, 160, 76, label="G · global") + c.frame(48, 38, 120, 52, label="E · enclosing") + c.frame(68, 54, 80, 28, label="L · local") + c.dot(108, 68, emphasis=True) + c.dashed(108, 78, 108, 100) + + +def closure_cell(c: Canvas) -> None: + """Inner function references a cell created by the outer call. + + Outer scope holds the `cell` (the captured `factor`); the inner function + keeps a reference into it, so the call survives the outer return. + """ + c.frame(0, 16, 240, 96, label="make_multiplier") + c.tag(16, 32, "cell") + c.cell(16, 38, "factor=2", w=84, h=22) + c.frame(112, 38, 122, 60, label="multiply") + c.label(173, 76, "uses cell", anchor="middle") + c.closed_arrow(128, 76, 102, 56, emphasis=True) + + +def slice_ruler(c: Canvas) -> None: + """Adjacent slices [:3] and [3:] split a sequence at index 3.""" + items = list("abcdef") + for i, v in enumerate(items): + c.cell(20 + i * 32, 30, v, w=32, h=24) + c.hairline(20, 64, 20 + 6 * 32, 64) + for i in range(7): + c.tick(20 + i * 32, 64) + c.label(20 + i * 32, 76, str(i), anchor="middle") + # bracket above for [:3] + c.dashed(20, 22, 20, 28) + c.dashed(20 + 3 * 32, 22, 20 + 3 * 32, 28) + c.dashed(20, 22, 20 + 3 * 32, 22) + c.label(20 + 1.5 * 32, 18, "[:3]", anchor="middle") + # bracket below for [3:] + c.dashed(20 + 3 * 32, 92, 20 + 3 * 32, 86) + c.dashed(20 + 6 * 32, 92, 20 + 6 * 32, 86) + c.dashed(20 + 3 * 32, 92, 20 + 6 * 32, 92) + c.label(20 + 4.5 * 32, 104, "[3:]", anchor="middle") + + +def branch_fork(c: Canvas) -> None: + """Make decisions explicitly: a value flows through a predicate to one branch.""" + c.cell(0, 36, "value", w=52, h=22) + c.closed_arrow(52, 47, 80, 47, emphasis=False) + c.node(98, 47, "?", r=14) + c.closed_arrow(110, 38, 158, 14, emphasis=False) + c.closed_arrow(110, 56, 158, 80, emphasis=False) + c.cell(160, 4, "case A", w=68, h=22) + c.cell(160, 70, "case B", w=68, h=22) + + +def loop_repetition(c: Canvas) -> None: + """The shape of a loop: walk the sequence, run the body, return.""" + c.cells(0, 28, ["a", "b", "c", "d"], w=28) + c.caret(0 + 14, 28, emphasis=False) + c.closed_arrow(116, 40, 142, 40, emphasis=False) + c.cell(144, 28, "body", w=56, h=24) + c.dashed(172, 54, 172, 76) + c.dashed(172, 76, 14, 76) + c.dashed(14, 76, 14, 54) + c.closed_arrow(14, 56, 14, 40, emphasis=True) + + +def iter_protocol(c: Canvas) -> None: + """The hidden machinery behind for: iterable → iter() → next() … values.""" + c.object_box(0, 30, "iterable", "[a,b,c]", w=82, h=28) + c.dashed(84, 44, 118, 44) + c.label(101, 38, "iter()", anchor="middle") + c.object_box(120, 30, "iterator", "", w=80, h=28) + c.closed_arrow(200, 44, 232, 44, emphasis=True) + c.label(216, 38, "next()", anchor="middle") + c.cells(234, 32, ["a", "b", "c"], w=22) + + +# ─── Runtime journey ────────────────────────────────────────────────── + + +def program_output(c: Canvas) -> None: + """Runtime · Start with executable evidence: a program produces visible output.""" + c.cell(0, 28, 'print("…")', w=92, h=28) + c.closed_arrow(92, 42, 124, 42, emphasis=False) + c.label(108, 36, "stdout", anchor="middle") + c.cell(126, 28, "hello world", w=110, h=28, soft=True) + + +def identity_and_equality(c: Canvas) -> None: + """Runtime · Separate value, identity, and absence: same object vs equal objects.""" + c.tag(0, 14, "is + ==") + c.name_box(0, 22, "a") + c.name_box(0, 50, "b") + c.closed_arrow(60, 34, 100, 50, emphasis=False) + c.closed_arrow(60, 62, 100, 50, emphasis=False) + c.cell(102, 38, "[1,2]", w=46, h=24, soft=True) + c.tag(170, 14, "== only") + c.name_box(170, 22, "a") + c.name_box(170, 50, "b") + c.closed_arrow(230, 34, 252, 34, emphasis=False) + c.closed_arrow(230, 62, 252, 62, emphasis=False) + c.cell(254, 22, "[1,2]", w=44, h=22, soft=True) + c.cell(254, 52, "[1,2]", w=44, h=22, soft=True) + + +def operator_dispatch(c: Canvas) -> None: + """Runtime · Read expressions as object operations: syntax becomes a method call.""" + c.cell(0, 30, "a + b", w=70, h=28) + c.closed_arrow(70, 44, 116, 44, emphasis=True) + c.label(93, 22, "dispatches", anchor="middle") + c.cell(118, 30, "a.__add__(b)", w=140, h=28, soft=True) + + +# ─── Shapes journey ─────────────────────────────────────────────────── + + +def container_questions(c: Canvas) -> None: + """Shapes · Pick the container that matches the question — each answers a different one.""" + pairs = [ + ("list", "[a,b]", "ordered"), + ("tuple", "(a,b)", "fixed"), + ("dict", "{k:v}", "lookup"), + ("set", "{a,b}", "unique"), + ] + for i, (tag, val, q) in enumerate(pairs): + x = i * 70 + c.object_box(x, 26, tag, val, w=64, h=28) + c.label(x + 32, 70, q, anchor="middle") + + +def reshape_pipeline(c: Canvas) -> None: + """Shapes · Move between shapes deliberately: one input, one transform, one result.""" + c.cell(0, 30, "[3,1,4]", w=80, h=28) + c.closed_arrow(80, 44, 120, 44, emphasis=True) + c.label(100, 36, "sorted", anchor="middle") + c.cell(122, 30, "[1,3,4]", w=80, h=28, soft=True) + + +def text_data_boundary(c: Canvas) -> None: + """Shapes · Cross text and data boundaries: text in, structured value out.""" + c.cell(0, 30, '"42"', w=70, h=28) + c.tag(0, 24, "text") + c.closed_arrow(70, 44, 110, 44, emphasis=True) + c.label(90, 36, "parse", anchor="middle") + c.object_box(112, 30, "int", "42", w=58, h=28) + + +# ─── Interfaces journey ─────────────────────────────────────────────── + + +def function_signature(c: Canvas) -> None: + """Interfaces · Functions as named behavior: input → body → output.""" + c.closed_arrow(0, 44, 32, 44, emphasis=False) + c.label(16, 36, "args", anchor="middle") + c.frame(34, 26, 110, 36, label="def f(...)") + c.closed_arrow(144, 44, 176, 44, emphasis=False) + c.label(160, 36, "return", anchor="middle") + + +def function_as_value(c: Canvas) -> None: + """Interfaces · Functions as values: name binds to a function object.""" + c.frame(0, 22, 80, 36, label="fn") + c.mono(40, 44, "def f") + c.closed_arrow(80, 40, 116, 40, emphasis=True) + c.cell(118, 26, "g = fn", w=80, h=28, soft=True) + + +def class_with_state(c: Canvas) -> None: + """Interfaces · Bundle behavior with state: a class groups fields and methods.""" + c.frame(0, 8, 150, 92, label="class Box") + c.tag(12, 26, "state") + c.cell(12, 32, "x · y", w=126, h=22) + c.tag(12, 64, "methods") + c.cell(12, 70, "move(...)", w=126, h=22) + + +# ─── Types journey ──────────────────────────────────────────────────── + + +def annotation_ghost(c: Canvas) -> None: + """Types · Keep runtime and static separate: annotations describe; runtime accepts any object.""" + c.mono(0, 36, "def f(x: int, y: str) -> bool: …", anchor="start") + c.dashed(54, 28, 76, 28) + c.dashed(102, 28, 124, 28) + c.dashed(150, 28, 192, 28) + + +def union_types(c: Canvas) -> None: + """Types · Describe realistic shapes: a slot accepting one of several types.""" + c.tag(0, 14, "x: int|str|None") + c.cell(0, 22, "x", w=44, h=28) + c.closed_arrow(44, 36, 90, 14, emphasis=False) + c.closed_arrow(44, 36, 90, 36, emphasis=False) + c.closed_arrow(44, 36, 90, 58, emphasis=False) + c.cell(92, 4, "int", w=70, h=22, soft=True) + c.cell(92, 26, "str", w=70, h=22, soft=True) + c.cell(92, 48, "None", w=70, h=22, soft=True) + + +def generic_preservation(c: Canvas) -> None: + """Types · Scale annotations: a generic preserves the input type through the call.""" + c.cell(0, 30, "T", w=36, h=28, soft=True) + c.closed_arrow(36, 44, 72, 44, emphasis=False) + c.frame(74, 26, 100, 36, label="fn[T]") + c.closed_arrow(174, 44, 210, 44, emphasis=True) + c.cell(212, 30, "T", w=36, h=28, soft=True) + + +# ─── Reliability journey ────────────────────────────────────────────── + + +def exception_lanes(c: Canvas) -> None: + """Reliability · Make failure explicit: try/except/else/finally as parallel lanes.""" + ys = [(20, "try"), (40, "except"), (60, "else"), (80, "finally")] + path = [(50, 20), (110, 20), (130, 60), (200, 60), (220, 80), (290, 80)] + c.lanes(ys, x0=40, x1=300, path=path) + + +def context_bowtie(c: Canvas) -> None: + """Reliability · Control resource boundaries: enter → body → exit, with raise routed through exit.""" + c.node(20, 48, "in", r=14) + c.closed_arrow(34, 48, 76, 48, emphasis=False) + c.cell(78, 36, "body", w=86, h=24) + c.closed_arrow(164, 48, 206, 48, emphasis=False) + c.node(220, 48, "out", r=14) + # Land the dashed exit path on the circle's left-side tangent so it + # doesn't terminate inside the "out" glyph. With body bottom-mid at + # (122, 60) → circle (220, 48) of r=14, the tangent meets at ≈(206, 50). + c.dashed(122, 60, 206, 50) + + +def async_swimlane(c: Canvas) -> None: + """Reliability · Operations that outlive one expression: loop and coroutine swap on await.""" + c.lane(28, x0=20, x1=270, label="loop") + c.lane(64, x0=20, x1=270, label="coro") + c.cell(40, 58, "", w=34, h=12) + c.dashed(76, 64, 110, 28) + c.cell(112, 22, "", w=58, h=12) + c.dashed(172, 28, 206, 64) + c.cell(208, 58, "", w=34, h=12) + c.label(95, 16, "await", anchor="middle") + c.label(190, 16, "resume", anchor="middle") + + +# ─── Control flow journey ───────────────────────────────────────────── + + +def naming_decisions(c: Canvas) -> None: + """Control flow · Name and shape decisions: the walrus binds while comparing.""" + c.cell(0, 30, "len(xs)", w=80, h=28) + c.closed_arrow(80, 44, 110, 44, emphasis=True) + c.label(95, 36, ":=", anchor="middle") + c.cell(112, 4, "n", w=40, h=22, soft=True) + c.tag(112, 0, "name") + c.cell(112, 50, "value", w=78, h=22, soft=True) + c.dashed(152, 16, 196, 16) + c.cell(198, 4, "n > 10", w=72, h=22) + + +def early_exit(c: Canvas) -> None: + """Control flow · Stop as soon as the answer is known: the loop exits on first match.""" + c.cells(0, 28, ["a", "b", "c", "d", "e"], w=28) + c.dot(70, 40) + c.closed_arrow(70, 56, 70, 78, emphasis=True) + c.cell(40, 80, "found · break", w=80, h=24, soft=True) + c.label(70, 14, "first match", anchor="middle") + + +# ─── Iteration journey ──────────────────────────────────────────────── + + +# ─── Example figures (promoted from the gestalt) ────────────────────── + + +def variables_bind(c: Canvas) -> None: + """Variables · names bind to objects: the canonical Python picture.""" + c.bind(0, 6, "x", "int", "42", object_w=70, gap=20) + + +def call_stack(c: Canvas) -> None: + """Recursion · stacked frames of the same function with different arguments.""" + chain = [3, 2, 1, 0] + for i, n in enumerate(chain): + suffix = " ← base" if n == 0 else "" + c.cell(0, i * 22, f"factorial({n}){suffix}", w=180, h=20) + c.dashed(192, 90, 192, 18) + c.closed_arrow(192, 30, 192, 18, emphasis=True) + + +def decorator_rebind(c: Canvas) -> None: + """Decorators · before: name binds to function. After @dec: name binds to wrapper.""" + c.tag(0, 12, "before") + c.bind(0, 18, "f", "fn", "f₀", object_w=50, gap=20) + c.tag(0, 70, "after @dec") + c.name_box(0, 78, "f") + c.closed_arrow(60, 90, 96, 90, emphasis=True) + c.cell(98, 76, "wrapper", w=80, h=28) + c.object_box(186, 78, "", "f₀", w=44, h=24) + c.dashed(178, 90, 186, 90) + + +def mro_chain(c: Canvas) -> None: + """Inheritance · diamond becomes a linear MRO via C3 linearization.""" + # Diamond ghost above + c.frame(80, 0, 40, 22, ghost=True) + c.mono(100, 16, "A") + c.ghost(98, 22, 58, 42) + c.ghost(102, 22, 142, 42) + c.frame(40, 42, 40, 22, ghost=True) + c.mono(60, 58, "B") + c.frame(120, 42, 40, 22, ghost=True) + c.mono(140, 58, "C") + c.ghost(60, 64, 96, 80) + c.ghost(140, 64, 104, 80) + c.frame(80, 80, 40, 22, ghost=True) + c.mono(100, 96, "D") + # MRO chain below + c.tag(0, 118, "mro") + chain = [("D", 36), ("B", 36), ("C", 36), ("A", 36), ("object", 56)] + x = 0 + for v, w in chain: + c.cell(x, 124, v, w=w, h=22) + x += w + + +def dataclass_fields(c: Canvas) -> None: + """Dataclasses · fields declared once become __init__ parameters.""" + c.tag(0, 12, "declaration") + fields = [("name", "str"), ("age", "int"), ("tags", "list")] + for i, (n, t) in enumerate(fields): + c.cell(0, 18 + i * 20, f"{n} : {t}", w=110, h=20) + c.closed_arrow(110, 48, 146, 48, emphasis=True) + c.object_box(148, 32, "", "__init__(name, age, tags)", w=160, h=32) + + +def class_triangle(c: Canvas) -> None: + """Classes · instance → class → type — every Python value sits on this triangle.""" + c.dot(20, 28) + c.label(20, 54, "instance", anchor="middle") + c.closed_arrow(26, 28, 86, 28, emphasis=False) + c.frame(88, 10, 60, 36, label="class") + c.mono(118, 32, "Class") + c.closed_arrow(148, 28, 208, 28, emphasis=False) + c.frame(210, 10, 60, 36, label="type") + c.mono(240, 32, "type") + + +def exception_cause_context(c: Canvas) -> None: + """Exception chaining · explicit `__cause__` (raise from) vs implicit `__context__`.""" + c.cell(0, 20, "ValueError", w=100, h=32) + c.closed_arrow(100, 28, 180, 28, emphasis=True) + c.label(140, 20, "__cause__", anchor="middle") + c.dashed(100, 44, 180, 44) + c.label(140, 62, "__context__", anchor="middle") + c.cell(182, 20, "RuntimeError", w=100, h=32) + + +def unpacking_bind(c: Canvas) -> None: + """Unpacking · left-side names bind to right-side positions; *rest gathers the middle.""" + items = ["1", "2", "3", "4", "5"] + for i, v in enumerate(items): + c.cell(i * 30, 0, v, w=30, h=22) + c.cell(0, 58, "a", w=30, h=22) + c.cell(30, 58, "*rest", w=90, h=22, ghost=True) + c.cell(120, 58, "b", w=30, h=22) + c.dashed(15, 22, 15, 58) + c.dashed(45, 22, 75, 58) + c.dashed(75, 22, 75, 58) + c.dashed(105, 22, 75, 58) + c.dashed(135, 22, 135, 58) + + +def comprehension_equivalence(c: Canvas) -> None: + """Comprehensions · the comprehension above and the equivalent for-loop below.""" + c.cell(0, 0, "[x*2 for x in xs if x > 0]", w=280, h=22, soft=True) + c.cell(0, 30, "out = []", w=280, h=14, ghost=True) + c.cell(0, 44, "for x in xs:", w=280, h=14, ghost=True) + c.cell(0, 58, " if x > 0: out.append(x*2)", w=280, h=14, ghost=True) + + +def list_append(c: Canvas) -> None: + """Lists · mutable sequence; `.append` extends the same list object.""" + c.cells(0, 8, ["3", "1", "4"], w=24) + c.cell(72, 8, "+1", ghost=True) + c.closed_arrow(98, 20, 132, 20, emphasis=True) + c.label(136, 18, ".append", anchor="start") + + +def dict_buckets(c: Canvas) -> None: + """Dictionaries · hashed buckets; collisions chain into a neighbouring slot.""" + c.tag(0, 12, "hash → bucket") + rows = [("0", '"a" → 1'), ("1", '"b" → 2'), ("2", '"c" → 3')] + for i, (idx, body) in enumerate(rows): + y = 18 + i * 24 + c.label(0, y + 16, idx, anchor="start") + c.cell(14, y, body, w=80, h=24) + c.closed_arrow(96, 54, 132, 54, emphasis=True) + c.cell(134, 42, '"d" → 4', w=80, h=24, soft=True) + c.label(218, 58, "collision", anchor="start") + + +# ─── Workers journey (abstract sections; designs tentative) ────────── + + +def workers_portable_evidence(c: Canvas) -> None: + """Workers · the process API is unavailable; a captured value crosses instead.""" + c.tag(0, 4, "unavailable") + c.cell(0, 12, "multiprocessing.Process()", w=180, h=22, ghost=True) + c.dashed(0, 24, 180, 24) + c.tag(0, 50, "portable evidence") + c.cell(0, 58, "value", w=60, h=22, soft=True) + c.closed_arrow(60, 69, 100, 69, emphasis=True) + c.cell(102, 58, "asserted in-process", w=120, h=22) + + +def workers_protocol_local(c: Canvas) -> None: + """Workers · the protocol shape is the lesson; no real socket is opened.""" + c.tag(0, 4, "request shape") + c.cell(0, 12, "GET /resource", w=160, h=22) + c.closed_arrow(80, 38, 80, 56, emphasis=True) + c.tag(0, 76, "response shape · asserted locally") + c.cell(0, 84, "200 OK · { … }", w=160, h=22) + + +# ─── Examples promoted from the gestalt: new paint code ────────────── + + +def number_lines(c: Canvas) -> None: + """Numbers · int unbounded; float spacing widens at the extremes.""" + c.tag(0, 8, "int · unbounded") + c.register(0, 22, 260, divisions=8) + c.tag(0, 50, "float · representable spacing widens") + c.register(0, 64, 260) + for x in (10, 30, 60, 100, 130, 140, 148, 160, 188, 230, 260): + c.tick(x, 64) + c.dot(140, 64, emphasis=True) + + +def expression_tree(c: Canvas) -> None: + """Operators and Literals · expression `(2+3)*4` parsed as a tree.""" + c.node(120, 12, "*", r=12) + c.node(80, 50, "+", r=12) + c.node(180, 50, "4", r=10) + c.node(56, 80, "2", r=10) + c.node(104, 80, "3", r=10) + c.connect(120, 12, 12, 80, 50, 12) + c.connect(120, 12, 12, 180, 50, 10) + c.connect(80, 50, 12, 56, 80, 10) + c.connect(80, 50, 12, 104, 80, 10) + + +def none_singleton(c: Canvas) -> None: + """None · three names converging on the single None object.""" + for i, n in enumerate("abc"): + c.name_box(0, i * 28, n) + for y in (12, 40, 68): + c.closed_arrow(60, y, 158, 40, emphasis=False) + c.object_box(160, 24, "NoneType", "None", w=80) + + +def codepoints_bytes(c: Canvas) -> None: + """Strings · text is unicode; bytes are a separate encoding layer.""" + c.tag(0, 8, "codepoints") + for i, ch in enumerate("café"): + c.cell(i * 40, 16, ch, w=40, h=28) + c.tag(0, 60, "utf-8 bytes") + widths = [40, 40, 40, 20, 20] + bytes_ = ["63", "61", "66", "c3", "a9"] + x = 0 + for w, b in zip(widths, bytes_): + c.cell(x, 68, b, w=w, h=14) + x += w + + +def sort_stability(c: Canvas) -> None: + """Sorting · stable sort preserves equal keys' original order.""" + c.tag(0, 4, "input") + c.tag(180, 4, "stable sort by key") + inputs = ["2 · Ada", "1 · Bo", "2 · Eve", "1 · Cy"] + outputs = ["1 · Bo", "1 · Cy", "2 · Ada", "2 · Eve"] + for i, (a, b) in enumerate(zip(inputs, outputs)): + c.cell(0, 12 + i * 22, a, w=80, h=20) + c.cell(180, 12 + i * 22, b, w=80, h=20) + c.dashed(80, 44, 180, 22) + c.dashed(80, 88, 180, 44) + c.dashed(80, 22, 180, 66) + c.dashed(80, 66, 180, 88) + + +def kw_only_separator(c: Canvas) -> None: + """Keyword-only arguments · `*` divides positional from keyword-only.""" + c.mono(0, 18, "def f(a, b, *, c, d): …", anchor="start") + # JetBrains Mono advances ~6px per char at fs=10; '*' sits at index 12. + c.dashed(75, 22, 75, 38) + c.label(33, 50, "positional or kw", anchor="middle") + c.label(120, 50, "keyword only", anchor="middle") + + +def positional_only_separator(c: Canvas) -> None: + """Positional-only parameters · `/` divides positional-only from positional-or-kw.""" + c.mono(0, 18, "def f(a, b, /, c, d): …", anchor="start") + # JetBrains Mono advances ~6px per char at fs=10; '/' sits at index 12. + c.dashed(75, 22, 75, 38) + c.label(33, 50, "positional only", anchor="middle") + c.label(120, 50, "positional or kw", anchor="middle") + + +def generator_ribbon(c: Canvas) -> None: + """Generators · execution paused between yields, resumed by next().""" + c.tag(0, 8, "paused between yields · resumed by next()") + c.ribbon(0, 16, 260, h=30, gates=[64, 136, 208], soft_segments=[(0, 64), (136, 208)]) + c.mono(32, 36, "…") + c.mono(100, 36, "yield") + c.mono(172, 36, "…") + c.mono(244, 36, "yield") + + +def truth_and_size(c: Canvas) -> None: + """Truth and size · bool(x) checks __bool__, then __len__, else True.""" + c.cell(0, 22, "x", w=40, h=24) + c.closed_arrow(40, 34, 70, 34, emphasis=True) + c.cell(72, 0, "__bool__()", w=160, h=22) + c.cell(72, 22, "__len__() != 0", w=160, h=22, ghost=True) + c.cell(72, 44, "default: True", w=160, h=22, ghost=True) + + +def descriptor_protocol(c: Canvas) -> None: + """Descriptors · attribute access routes to __get__/__set__/__delete__.""" + c.cell(0, 22, "obj.attr", w=80, h=24) + c.closed_arrow(80, 34, 110, 34, emphasis=True) + c.frame(112, 0, 110, 70, label="descriptor") + c.mono(167, 22, "__get__") + c.mono(167, 38, "__set__") + c.mono(167, 54, "__delete__") + + +def bound_unbound(c: Canvas) -> None: + """Bound vs unbound methods · instance.method binds self; Class.method does not.""" + c.cell(0, 0, "obj.method", w=110, h=22) + c.closed_arrow(110, 11, 140, 11, emphasis=True) + c.cell(142, 0, "bound · self filled", w=152, h=22, soft=True) + c.cell(0, 32, "Class.method", w=110, h=22) + c.closed_arrow(110, 43, 140, 43, emphasis=False) + c.cell(142, 32, "function · self required", w=152, h=22) + + +def method_kinds(c: Canvas) -> None: + """Method kinds · classmethod, staticmethod, instance — first-arg differs.""" + rows = [ + ("@classmethod", "Cls"), + ("@staticmethod", "(none)"), + ("instance", "self"), + ] + for i, (decorator, first_arg) in enumerate(rows): + y = i * 24 + c.cell(0, y, decorator, w=110, h=22) + c.closed_arrow(110, y + 11, 140, y + 11, emphasis=False) + c.cell(142, y, f"first arg · {first_arg}", w=130, h=22, soft=True) + + +def callable_objects(c: Canvas) -> None: + """Callable objects · `__call__` makes any object look like a function.""" + c.frame(0, 4, 100, 36, label="object") + c.mono(50, 26, "__call__") + c.closed_arrow(100, 22, 138, 22, emphasis=True) + c.cell(140, 10, "obj(...)", w=80, h=24, soft=True) + + +def attribute_lookup(c: Canvas) -> None: + """Attribute access · instance dict, then class dict, then __getattr__; first hit wins.""" + c.cell(0, 22, "obj.x", w=70, h=24) + c.closed_arrow(70, 34, 100, 34, emphasis=True) + c.cell(102, 0, "instance __dict__", w=140, h=22) + c.cell(102, 22, "class __dict__", w=140, h=22) + c.cell(102, 44, "__getattr__", w=140, h=22, ghost=True) + + +def guard_clauses(c: Canvas) -> None: + """Guard clauses · early returns first; main work runs only when guards fall through.""" + c.cell(0, 0, "if not valid: return", w=180, h=22, soft=True) + c.cell(0, 24, "if missing: return None", w=180, h=22, soft=True) + c.cell(0, 48, "if special_case: return X", w=180, h=22, soft=True) + c.cell(0, 78, "main work …", w=180, h=22) + c.closed_arrow(190, 11, 220, 11, emphasis=False) + c.closed_arrow(190, 35, 220, 35, emphasis=False) + c.closed_arrow(190, 59, 220, 59, emphasis=False) + c.label(222, 38, "exit", anchor="start") + + +def bytes_vs_bytearray(c: Canvas) -> None: + """Bytes vs bytearray · frozen sequence of integers vs mutable counterpart.""" + c.tag(0, 4, "bytes — frozen") + c.cell(0, 12, "b'\\\\x63\\\\x61\\\\x66'", w=160, h=24) + c.tag(0, 50, "bytearray — mutable") + c.cell(0, 58, "bytearray(b'\\\\x63\\\\x61')", w=180, h=24) + c.closed_arrow(180, 70, 218, 70, emphasis=True) + c.label(222, 67, ".append(0x66)", anchor="start") + + +def sentinel_iteration(c: Canvas) -> None: + """Sentinel iteration · `iter(callable, sentinel)` calls until the sentinel returns.""" + c.cell(0, 22, "iter(read, '')", w=120, h=24) + c.closed_arrow(120, 34, 152, 34, emphasis=True) + c.cell(154, 0, "value", w=70, h=20) + c.cell(154, 22, "value", w=70, h=20) + c.cell(154, 44, "value", w=70, h=20) + c.cell(154, 66, "''", w=70, h=20, ghost=True) + c.label(228, 80, "sentinel · stop", anchor="start") + + +def partial_functions(c: Canvas) -> None: + """Partial functions · `partial(f, 1)` pre-fills arguments, returning a thinner callable.""" + c.cell(0, 12, "f(a, b, c)", w=100, h=24) + c.closed_arrow(100, 24, 130, 24, emphasis=True) + c.cell(132, 0, "partial(f, 1)", w=100, h=24) + c.closed_arrow(232, 24, 262, 24, emphasis=False) + c.cell(264, 12, "g(b, c)", w=70, h=24, soft=True) + + +# ─── More example figures (third coverage push) ─────────────────────── + + +def args_kwargs(c: Canvas) -> None: + """Args and kwargs · *args gathers extra positionals; **kwargs gathers extra keywords.""" + c.mono(20, 22, "def f(*args, **kwargs): …", anchor="start") + # *args occupies signature indices 6-10 (center x≈20+8*6=68 with mono advance ≈6); + # **kwargs occupies indices 13-20 (center x≈20+17*6=122). + c.dashed(68, 26, 68, 44) + c.dashed(122, 26, 122, 44) + c.label(68, 56, "→ tuple", anchor="middle") + c.label(122, 56, "→ dict", anchor="middle") + + +def multiple_return(c: Canvas) -> None: + """Multiple return values · the function returns a tuple; the caller unpacks it.""" + c.cell(0, 0, "def f(): return a, b", w=180, h=24) + c.closed_arrow(90, 26, 90, 44, emphasis=True) + c.cell(58, 44, "(a, b)", w=64, h=22, soft=True) + c.closed_arrow(90, 68, 90, 86, emphasis=False) + c.cell(50, 86, "x, y", w=80, h=22) + + +def lambda_expression(c: Canvas) -> None: + """Lambdas · a function as a value: parameters on the left, expression on the right.""" + c.cell(0, 0, "lambda x: x + 1", w=170, h=28, soft=True) + c.dashed(40, 28, 40, 50) + c.dashed(120, 28, 120, 50) + c.label(40, 62, "params", anchor="middle") + c.label(120, 62, "expression", anchor="middle") + + +def property_fork(c: Canvas) -> None: + """Properties · obj.x routes through fget/fset instead of touching __dict__.""" + c.cell(0, 22, "obj.x", w=70, h=24) + c.closed_arrow(70, 22, 110, 4, emphasis=True) + c.cell(112, 0, "fget / fset", w=120, h=22, soft=True) + c.closed_arrow(70, 46, 110, 56, emphasis=False) + c.cell(112, 50, "__dict__", w=120, h=22, ghost=True) + + +def metaclass_triangle(c: Canvas) -> None: + """Metaclasses · instance → class → metaclass; the metaclass is the type of the class.""" + c.dot(20, 30) + c.label(20, 56, "instance", anchor="middle") + c.closed_arrow(26, 30, 86, 30, emphasis=False) + c.frame(88, 12, 60, 36, label="class") + c.mono(118, 34, "Class") + c.closed_arrow(148, 30, 218, 30, emphasis=False) + c.frame(220, 12, 80, 36, label="metaclass") + c.mono(260, 34, "type") + + +def sys_path_resolution(c: Canvas) -> None: + """Modules · imports walk sys.path; the first hit wins.""" + c.tag(0, 4, "sys.path") + paths = ["cwd", "site-packages", "stdlib", "…"] + for i, p in enumerate(paths): + c.cell(0, 14 + i * 20, p, w=120, h=20) + c.closed_arrow(120, 46, 156, 46, emphasis=True) + c.label(145, 30, "first hit", anchor="middle") + c.cell(158, 34, "mymod.py", w=100, h=24, soft=True) + + +def import_alias(c: Canvas) -> None: + """Import aliases · `import x as y` makes y point at the same module object as x.""" + c.mono(0, 8, "import numpy as np", anchor="start") + c.cell(0, 24, "np", w=40, h=22) + c.closed_arrow(40, 35, 80, 35, emphasis=True) + c.cell(82, 24, "numpy module", w=130, h=22, soft=True) + + +def protocol_check(c: Canvas) -> None: + """Protocols · structural check; an object satisfies a protocol if it has the required methods.""" + c.frame(0, 4, 100, 70, label="object") + c.mono(50, 20, "read()") + c.mono(50, 36, "write()") + c.mono(50, 52, "close()") + c.closed_arrow(100, 38, 138, 38, emphasis=True) + c.label(119, 0, "structural", anchor="middle") + c.frame(140, 4, 80, 70, label="protocol", ghost=True) + c.mono(180, 28, "read()") + c.mono(180, 46, "close()") + + +def enum_members(c: Canvas) -> None: + """Enums · a fixed set of named symbolic values; no new members appear at runtime.""" + c.frame(0, 0, 280, 60, label="Color · closed set", ghost=True) + members = ["RED", "GREEN", "BLUE", "no more"] + for i, m in enumerate(members): + c.cell(20 + i * 60, 16, m, w=50, h=28) + + +def datetime_instant(c: Canvas) -> None: + """Datetime · one instant; the offset names which clock face you're reading.""" + c.register(10, 40, 260, divisions=8) + c.dashed(150, 30, 150, 50) + c.label(150, 24, "one instant", anchor="middle") + c.node(90, 70, "−5h", r=12) + c.dashed(90, 58, 150, 50) + c.node(210, 70, "+0h", r=12) + c.dashed(210, 58, 150, 50) + + +def json_python_mapping(c: Canvas) -> None: + """JSON ↔ Python · six type pairs map across the text boundary.""" + c.hairline(120, 0, 120, 110) + c.tag(60, 4, "json", anchor="middle") + c.tag(180, 4, "python", anchor="middle") + rows = [ + ("object", "dict"), + ("array", "list"), + ("string", "str"), + ("number", "int / float"), + ("true / false", "True / False"), + ("null", "None"), + ] + for i, (a, b) in enumerate(rows): + y = 24 + i * 14 + c.mono(0, y, a, anchor="start", size=10) + c.mono(132, y, b, anchor="start", size=10) + + +def regex_anchors(c: Canvas) -> None: + """Regular expressions · anchors and quantifiers shape what the pattern matches.""" + c.tag(0, 4, "pattern") + c.mono(0, 24, "^\\d{2}-\\d{2}$", anchor="start") + c.tag(0, 56, "input") + c.cell(0, 64, "", w=200, h=20) + c.cell(40, 64, "12-34", w=120, h=20, soft=True) + + +def number_parse(c: Canvas) -> None: + """Number parsing · text → typed number, raising on bad input.""" + c.cell(0, 22, '"42"', w=70, h=24) + c.closed_arrow(70, 34, 102, 34, emphasis=True) + c.label(86, 26, "int()", anchor="middle") + c.cell(104, 10, "42", w=60, h=22, soft=True) + c.cell(104, 36, "ValueError", w=100, h=22, ghost=True) + + +def format_spec(c: Canvas) -> None: + """String formatting · the format spec is a railroad of named optional fields.""" + c.tag(0, 4, "format spec") + stations = [("align", 36), ("sign", 30), ("width", 40), (",", 22), (".prec", 44), ("type", 32)] + x = 0 + for label_text, w in stations: + c.cell(x, 16, label_text, w=w, h=18) + x += w + 2 + c.label(0, 54, "{:>6,.2f}") + + +def truthy_check(c: Canvas) -> None: + """Truthiness · bool(x) is True except for a small fixed set of falsy values.""" + c.cell(0, 0, "x", w=40, h=24) + c.closed_arrow(40, 12, 70, 12, emphasis=True) + c.label(55, 6, "bool", anchor="middle") + c.cell(72, 0, "True or False", w=130, h=24) + c.tag(0, 38, "falsy values") + falsy = [("0", 22), ("0.0", 30), ('""', 26), ("[]", 22), ("{}", 22), ("None", 40), ("False", 40)] + x = 0 + for label_text, w in falsy: + c.cell(x, 46, label_text, w=w, h=20) + x += w + 2 + + +def boolean_truth_table(c: Canvas) -> None: + """Booleans · `a and b` is True only when both are True; otherwise False.""" + c.tag(64, 0, "a and b", anchor="middle") + c.label(80, 16, "T", anchor="middle") + c.label(112, 16, "F", anchor="middle") + c.label(58, 36, "T", anchor="end") + c.label(58, 56, "F", anchor="end") + c.cell(64, 22, "T", w=32, h=20, soft=True) + c.cell(96, 22, "F", w=32, h=20) + c.cell(64, 42, "F", w=32, h=20) + c.cell(96, 42, "F", w=32, h=20) + + +def set_buckets(c: Canvas) -> None: + """Sets · hash buckets with no values; membership is O(1).""" + c.tag(0, 4, "keys only") + for i, k in enumerate("abc"): + c.cell(0, 14 + i * 22, k, w=50, h=20) + c.closed_arrow(50, 36, 90, 36, emphasis=True) + c.label(70, 28, "x in s", anchor="middle") + c.cell(92, 24, "O(1)", w=60, h=22, soft=True) + + +def tuple_frozen(c: Canvas) -> None: + """Tuples · ordered, immutable sequence; .append doesn't exist.""" + c.tag(0, 0, "frozen sequence") + c.cell(0, 12, "(3, 1, 4, 1)", w=180, h=26) + c.dashed(45, 8, 45, 42) + c.dashed(90, 8, 90, 42) + c.dashed(135, 8, 135, 42) + c.cell(190, 12, ".append", w=80, h=26, ghost=True) + c.dashed(190, 24, 270, 24) + + +def value_types(c: Canvas) -> None: + """Values · every literal is a typed object: int, str, list, dict each carry their behaviour.""" + rows = [("int", "42"), ("str", '"hi"'), ("list", "[1,2,3]"), ("dict", "{k:v}")] + for i, (t, v) in enumerate(rows): + y = i * 30 + c.object_box(0, y, t, v, w=160, h=26, tag_position="inside") + + +def literal_forms(c: Canvas) -> None: + """Literals · each type has its own literal spellings; the source spelling determines the value type.""" + rows = [ + ("int", "42 · 0x2a · 0b101"), + ("float", "3.14 · 1e-3"), + ("str", '"hi" · \'hi\''), + ("list", "[1, 2, 3]"), + ("dict", "{k: v}"), + ("set", "{1, 2, 3}"), + ] + for i, (t, spellings) in enumerate(rows): + y = i * 22 + c.cell(0, y, t, w=50, h=20, soft=True) + c.cell(52, y, spellings, w=200, h=20) + + +def function_with_body(c: Canvas) -> None: + """Functions · `def greet(name): return "Hello, " + name` takes input, computes, returns output.""" + c.closed_arrow(0, 36, 30, 36, emphasis=False) + c.label(15, 28, "name", anchor="middle") + c.frame(32, 18, 150, 44, label="def greet(name):") + c.mono(107, 44, '"Hello, " + name') + c.closed_arrow(182, 36, 212, 36, emphasis=True) + c.cell(214, 24, '"Hello, Ada"', w=120, h=24, soft=True) + + +def yield_delegation(c: Canvas) -> None: + """Yield from · delegate iteration to an inner generator; its yields surface here.""" + c.tag(0, 4, "outer") + c.ribbon(0, 14, 240, h=20, gates=[100, 180]) + c.mono(140, 28, "yield from inner") + c.tag(100, 46, "inner") + c.ribbon(100, 56, 80, h=24, gates=[124, 152]) + c.dashed(140, 56, 140, 34) + + +def itertools_chain(c: Canvas) -> None: + """Itertools · chain joins two iterables into one stream without materialising either.""" + c.object_box(0, 14, "iter A", "1 · 2", w=70, h=24) + c.object_box(0, 52, "iter B", "3 · 4", w=70, h=24) + c.closed_arrow(70, 26, 100, 36, emphasis=False) + c.closed_arrow(70, 64, 100, 54, emphasis=False) + c.object_box(102, 30, "chain", "1 · 2 · 3 · 4", w=140, h=28) + + +def assertion_check(c: Canvas) -> None: + """Assertions · assert tests a condition; True passes, False raises AssertionError.""" + c.cell(0, 22, "assert cond", w=110, h=24) + c.closed_arrow(110, 22, 140, 0, emphasis=True) + c.cell(142, 0, "True · pass", w=120, h=20, soft=True) + c.closed_arrow(110, 46, 140, 56, emphasis=False) + c.cell(142, 50, "False · AssertionError", w=160, h=20) + + +def custom_exception_chain(c: Canvas) -> None: + """Custom exceptions · subclass an existing exception; gain a domain name without changing semantics.""" + chain = ["BaseException", "Exception", "ValueError", "MyDomainError"] + for i, name in enumerate(chain): + emph = i == len(chain) - 1 + c.cell(0, i * 24, name, w=220, h=22, soft=emph) + + +def exception_group_peel(c: Canvas) -> None: + """Exception groups · except* peels matching leaves; survivors regroup.""" + c.tag(0, 0, "before") + c.dot(40, 14) + for x in (20, 36, 52, 68): + c.ghost(40, 14, x, 44) + c.dot(20, 44) + c.dot(36, 44) + c.dot(52, 44) + c.dot(68, 44) + c.closed_arrow(90, 30, 140, 30, emphasis=True) + c.label(115, 22, "except*", anchor="middle") + c.tag(160, 0, "after") + c.dot(200, 14) + c.ghost(200, 14, 180, 44) + c.ghost(200, 14, 220, 44) + c.dot(180, 44) + c.dot(220, 44) + + +def delete_name_erased(c: Canvas) -> None: + """Delete statements · `del x` removes the name; the object survives if any other name holds it.""" + c.tag(0, 0, "before") + c.name_box(0, 12, "x") + c.closed_arrow(60, 23, 100, 23, emphasis=False) + c.cell(102, 12, "[1, 2, 3]", w=90, h=22, soft=True) + c.tag(0, 52, "after del x") + c.name_box(0, 60, "x") + c.dashed(0, 70, 60, 70) + c.dashed(60, 60, 0, 80) + c.cell(102, 60, "[1, 2, 3]", w=90, h=22, soft=True) + + +# ─── Fourth coverage push: constraint-shaped examples ───────────────── + + +def package_tree(c: Canvas) -> None: + """Packages · a directory with __init__.py becomes an importable package; submodules nest.""" + c.frame(70, 0, 100, 22, label="mypackage") + c.mono(120, 14, "__init__.py") + c.stroke(120, 22, 40, 50) + c.stroke(120, 22, 120, 50) + c.stroke(120, 22, 200, 50) + c.cell(10, 50, "a.py", w=60, h=22) + c.cell(90, 50, "b.py", w=60, h=22) + c.cell(170, 50, "sub/", w=60, h=22, soft=True) + + +def venv_boundary(c: Canvas) -> None: + """Virtual environments · a venv isolates a project's interpreter and packages from the system.""" + c.frame(0, 0, 110, 70, label="project") + c.cell(12, 18, "code", w=84, h=20) + c.cell(12, 42, "requirements", w=84, h=20) + c.closed_arrow(110, 35, 142, 35, emphasis=True) + c.frame(144, 0, 130, 70, label="venv") + c.mono(209, 22, "python") + c.mono(209, 42, "site-packages") + + +def subprocess_spawn(c: Canvas) -> None: + """Subprocesses · spawn a child process; capture stdout, stderr, and exit code as portable evidence.""" + c.cell(0, 22, "parent", w=70, h=24) + c.closed_arrow(70, 34, 110, 34, emphasis=True) + c.label(90, 26, "spawn", anchor="middle") + c.cell(112, 22, "child process", w=110, h=24, soft=True) + c.closed_arrow(222, 34, 252, 34, emphasis=False) + c.cell(254, 22, "output", w=70, h=24) + + +def logging_levels(c: Canvas) -> None: + """Logging · five levels; messages below the configured threshold are dropped.""" + levels = [("CRITICAL", "50"), ("ERROR", "40"), ("WARNING", "30"), ("INFO", "20"), ("DEBUG", "10")] + for i, (name, num) in enumerate(levels): + c.cell(0, i * 22, name, w=120, h=20) + c.cell(122, i * 22, num, w=40, h=20, soft=True) + + +def aaa_pattern(c: Canvas) -> None: + """Testing · arrange-act-assert: set up, run the behavior, compare the result.""" + rows = [("arrange", "set up state"), ("act", "perform behavior"), ("assert", "compare result")] + for i, (label_text, body) in enumerate(rows): + c.cell(0, i * 24, label_text, w=80, h=22, soft=(i == 2)) + c.closed_arrow(80, i * 24 + 11, 108, i * 24 + 11, emphasis=False) + c.cell(110, i * 24, body, w=140, h=22) + + +def socket_byte_boundary(c: Canvas) -> None: + """Networking · sockets carry bytes; encode marks the python → wire boundary, decode the wire → python boundary.""" + c.object_box(0, 18, "str", '"ping"', w=64, h=22) + c.closed_arrow(64, 29, 96, 29, emphasis=False) + c.label(80, 23, "encode", anchor="middle") + c.object_box(98, 18, "bytes", "b'ping'", w=70, h=22, soft=True) + c.dashed(168, 29, 192, 29) + c.tag(180, 8, "socket", anchor="middle") + c.object_box(194, 18, "bytes", "b'ping'", w=70, h=22, soft=True) + c.closed_arrow(264, 29, 296, 29, emphasis=False) + c.label(280, 23, "decode", anchor="middle") + c.object_box(298, 18, "str", '"ping"', w=64, h=22) + + +def gil_lanes(c: Canvas) -> None: + """Threads and processes · the GIL serialises Python bytecode across threads; processes run in parallel.""" + c.lane(20, x0=54, x1=294, label="GIL") + c.lane(50, x0=54, x1=294, label="thread A") + c.lane(80, x0=54, x1=294, label="thread B") + c.cell(64, 44, "", w=30, h=12) + c.cell(124, 74, "", w=30, h=12) + c.cell(184, 44, "", w=30, h=12) + c.cell(244, 74, "", w=30, h=12) + + +def cast_escape(c: Canvas) -> None: + """Casts and any · cast(T, x) tells the type checker to treat x as T; runtime is unaffected.""" + c.cell(0, 22, "Any", w=70, h=24, ghost=True) + c.closed_arrow(70, 34, 110, 34, emphasis=True) + c.label(90, 12, "cast(T, x)", anchor="middle") + c.cell(112, 22, "T", w=70, h=24, soft=True) + + +def newtype_phantom(c: Canvas) -> None: + """NewType · two static identities backed by the same runtime type.""" + c.tag(0, 0, "runtime: int") + c.cell(0, 12, "42", w=60, h=24) + c.tag(0, 50, "static: UserId") + c.cell(0, 62, "UserId(42)", w=90, h=24, soft=True) + + +def overload_signatures(c: Canvas) -> None: + """Overloads · @overload declares multiple signatures; one implementation routes to the right return type.""" + c.tag(0, 0, "@overload") + c.cell(0, 12, "def f(x: int) -> str", w=180, h=20) + c.cell(0, 36, "def f(x: str) -> int", w=180, h=20) + c.closed_arrow(180, 32, 220, 32, emphasis=True) + c.cell(222, 22, "one impl", w=80, h=22, soft=True) + + +def paramspec_preserve(c: Canvas) -> None: + """ParamSpec · the decorator preserves the wrapped function's full signature, parameter for parameter.""" + c.cell(0, 22, "f(P)", w=50, h=24) + c.closed_arrow(50, 34, 80, 34, emphasis=False) + c.frame(82, 12, 100, 44, label="@dec") + c.mono(132, 36, "P preserved") + c.closed_arrow(182, 34, 212, 34, emphasis=True) + c.cell(214, 22, "wrapper(P)", w=80, h=24, soft=True) + + +def literal_constrained(c: Canvas) -> None: + """Literal · the type narrows the slot to a fixed set of constant values.""" + c.tag(0, 0, "Literal[…]") + c.cell(0, 12, "x", w=40, h=24) + c.closed_arrow(40, 24, 70, 4, emphasis=False) + c.closed_arrow(40, 24, 70, 24, emphasis=False) + c.closed_arrow(40, 24, 70, 44, emphasis=False) + c.cell(72, 0, "'red'", w=70, h=20, soft=True) + c.cell(72, 22, "'green'", w=70, h=20, soft=True) + c.cell(72, 44, "'blue'", w=70, h=20, soft=True) + + +def callable_type(c: Canvas) -> None: + """Callable types · the annotation captures the call shape: argument types and return type.""" + c.tag(0, 0, "Callable[[int, str], bool]") + c.cell(0, 12, "(int, str)", w=100, h=22) + c.closed_arrow(100, 23, 130, 23, emphasis=False) + c.cell(132, 12, "bool", w=60, h=22) + + +def isinstance_check(c: Canvas) -> None: + """Runtime type checks · isinstance asks the runtime; the answer is a bool, not a refinement.""" + c.cell(0, 22, "isinstance(x, T)", w=140, h=24) + c.closed_arrow(140, 22, 170, 4, emphasis=True) + c.cell(172, 0, "True", w=60, h=20, soft=True) + c.closed_arrow(140, 46, 170, 56, emphasis=False) + c.cell(172, 50, "False", w=60, h=20) + + +def collections_containers(c: Canvas) -> None: + """Collections module · four specialised containers for shapes the built-in types don't cover well.""" + rows = [("deque", "fast appends both ends"), ("Counter", "key → count"), ("defaultdict", "missing key default"), ("namedtuple", "tuple with names")] + for i, (name, role) in enumerate(rows): + c.cell(0, i * 22, name, w=110, h=20) + c.cell(112, i * 22, role, w=170, h=20, soft=True) + + +def typed_dict_shape(c: Canvas) -> None: + """Structured data shapes · TypedDict names each key's value type; the dict obeys the declared shape.""" + c.frame(0, 0, 200, 86, label="User TypedDict") + rows = [("id", "int"), ("name", "str"), ("active", "bool")] + for i, (k, v) in enumerate(rows): + c.cell(14, 18 + i * 20, f"{k}: {v}", w=172, h=18) + + +def csv_records(c: Canvas) -> None: + """CSV data · rows of records; each line has the same columns in the same order.""" + c.tag(0, 0, "rows · records") + headers = ["id", "name", "score"] + rows = [["1", "Ada", "97"], ["2", "Bo", "88"], ["3", "Cy", "76"]] + for j, h in enumerate(headers): + c.cell(j * 70, 12, h, w=70, h=20, soft=True) + for i, r in enumerate(rows): + for j, v in enumerate(r): + c.cell(j * 70, 32 + i * 20, v, w=70, h=18) + + +def warning_signal(c: Canvas) -> None: + """Warnings · a soft signal: the warning is reported, execution continues.""" + c.cell(0, 22, "code path", w=90, h=24) + c.closed_arrow(90, 22, 120, 4, emphasis=False) + c.cell(122, 0, "DeprecationWarning", w=170, h=22, soft=True) + c.closed_arrow(90, 46, 120, 56, emphasis=True) + c.cell(122, 50, "execution continues", w=170, h=22) + + +def object_lifecycle(c: Canvas) -> None: + """Object lifecycle · __init__ creates; the object lives while refcount > 0; __del__ finalises.""" + c.cell(0, 22, "__init__", w=80, h=24) + c.closed_arrow(80, 34, 110, 34, emphasis=True) + c.cell(112, 22, "live · refcount > 0", w=140, h=24, soft=True) + c.closed_arrow(252, 34, 282, 34, emphasis=False) + c.cell(284, 22, "__del__", w=80, h=24) + + +# ─── Fifth pass: tightened figures for slugs that were on reuse-floors ─ + + +def type_alias_name(c: Canvas) -> None: + """Type aliases · complex annotation collapses to a single readable name.""" + c.cell(0, 30, "dict[str, list[tuple[int, str]]]", w=240, h=24, ghost=True) + c.closed_arrow(120, 54, 120, 70, emphasis=True) + c.label(96, 66, "type Index = …", anchor="middle") + c.cell(80, 76, "Index", w=80, h=24, soft=True) + + +def match_dispatch_ladder(c: Canvas) -> None: + """Match statements · the value flows down the patterns; the first match wins.""" + c.cell(0, 0, "match value", w=170, h=22) + cases = ["case 0:", "case [x, y]:", "case Point(0, _):", "case _:"] + for i, txt in enumerate(cases): + c.cell(0, 30 + i * 22, txt, w=170, h=20) + c.dashed(186, 32, 186, 122) + c.dot(186, 74) + c.closed_arrow(186, 110, 186, 124, emphasis=True) + c.label(196, 76, "first match", anchor="start") + + +def match_pattern_variants(c: Canvas) -> None: + """Advanced match patterns · capture, alternative, guard, class — four pattern shapes.""" + rows = [("capture", "[x, y]"), ("alternative", "P() | Q()"), ("guard", "[x] if x > 0"), ("class", "Point(x=0, y=_)")] + for i, (kind, shape) in enumerate(rows): + y = i * 22 + c.cell(0, y, kind, w=90, h=20) + c.cell(92, y, shape, w=180, h=20, soft=(kind == "class")) + + +def loop_else_gate(c: Canvas) -> None: + """Loop else · runs when the loop falls through naturally; break skips it.""" + c.cell(0, 20, "loop body", w=110, h=24) + c.closed_arrow(110, 20, 150, 0, emphasis=True) + c.cell(152, 0, "fell through · else runs", w=160, h=20, soft=True) + c.closed_arrow(110, 44, 150, 56, emphasis=False) + c.cell(152, 50, "broke · else skipped", w=160, h=20) + + +def workers_lesson_runtime(c: Canvas) -> None: + """Workers · lesson uses captured output as evidence when the runtime forbids the process API.""" + c.cell(0, 22, "lesson question", w=130, h=24) + c.closed_arrow(130, 22, 160, 0, emphasis=False) + c.cell(162, 0, "process API", w=130, h=20, ghost=True) + c.dashed(162, 10, 292, 10) + c.closed_arrow(130, 46, 160, 56, emphasis=True) + c.cell(162, 50, "captured output", w=130, h=20, soft=True) + + +def lazy_stream(c: Canvas) -> None: + """Iteration · Compose lazy value streams: filter and map flow values without materialising.""" + c.object_box(0, 26, "source", "[a,b,c]", w=78, h=24) + c.dashed(78, 38, 102, 38) + c.object_box(104, 26, "filter", "x>0", w=68, h=24) + c.dashed(172, 38, 196, 38) + c.object_box(198, 26, "map", "x*2", w=64, h=24) + c.closed_arrow(262, 38, 294, 38, emphasis=True) + c.label(278, 30, "next()", anchor="middle") + + +# Registry: figure_name -> (paint_fn, viewbox_w, viewbox_h) +FIGURES: dict[str, tuple[Callable[[Canvas], None], int, int]] = { + "aliasing-mutation": (aliasing_mutation, 220, 175), + "tuple-no-mutation": (tuple_no_mutation, 220, 185), + "iterator-unroll": (iterator_unroll, 220, 130), + "scope-rings": (scope_rings, 216, 116), + "closure-cell": (closure_cell, 240, 120), + "slice-ruler": (slice_ruler, 232, 120), + "branch-fork": (branch_fork, 232, 100), + "loop-repetition": (loop_repetition, 204, 90), + "iter-protocol": (iter_protocol, 304, 70), + # Runtime + "program-output": (program_output, 240, 80), + "identity-and-equality": (identity_and_equality, 304, 96), + "operator-dispatch": (operator_dispatch, 260, 70), + # Shapes + "container-questions": (container_questions, 280, 88), + "reshape-pipeline": (reshape_pipeline, 204, 80), + "text-data-boundary": (text_data_boundary, 172, 70), + # Interfaces + "function-signature": (function_signature, 188, 80), + "function-as-value": (function_as_value, 200, 66), + "class-with-state": (class_with_state, 152, 108), + # Types + "annotation-ghost": (annotation_ghost, 220, 52), + "union-types": (union_types, 166, 80), + "generic-preservation": (generic_preservation, 250, 70), + # Reliability + "exception-lanes": (exception_lanes, 320, 100), + "context-bowtie": (context_bowtie, 244, 76), + "async-swimlane": (async_swimlane, 280, 84), + # Control flow + Iteration coverage gap (see audit) + "naming-decisions": (naming_decisions, 274, 80), + "early-exit": (early_exit, 144, 116), + "lazy-stream": (lazy_stream, 300, 56), + # Promoted from the gestalt — wired to example pages via ATTACHMENTS + "variables-bind": (variables_bind, 180, 44), + "call-stack": (call_stack, 200, 100), + "decorator-rebind": (decorator_rebind, 232, 110), + "mro-chain": (mro_chain, 200, 152), + "dataclass-fields": (dataclass_fields, 312, 76), + "class-triangle": (class_triangle, 274, 60), + "exception-cause-context": (exception_cause_context, 282, 70), + "unpacking-bind": (unpacking_bind, 152, 80), + "comprehension-equivalence": (comprehension_equivalence, 280, 76), + "list-append": (list_append, 220, 36), + "dict-buckets": (dict_buckets, 270, 88), + # Workers journey (constraint-shaped sections; tightened designs) + "workers-portable-evidence": (workers_portable_evidence, 222, 84), + "workers-protocol-local": (workers_protocol_local, 200, 110), + "workers-lesson-runtime": (workers_lesson_runtime, 300, 80), + # Newly designed paint code for examples that lacked a figure + "number-lines": (number_lines, 260, 78), + "expression-tree": (expression_tree, 220, 92), + "none-singleton": (none_singleton, 240, 84), + "codepoints-bytes": (codepoints_bytes, 200, 84), + "sort-stability": (sort_stability, 270, 100), + "kw-only-separator": (kw_only_separator, 200, 56), + "positional-only-separator": (positional_only_separator, 200, 56), + "generator-ribbon": (generator_ribbon, 260, 50), + "truth-and-size": (truth_and_size, 232, 70), + "descriptor-protocol": (descriptor_protocol, 222, 76), + "bound-unbound": (bound_unbound, 296, 56), + "method-kinds": (method_kinds, 272, 70), + "callable-objects": (callable_objects, 220, 44), + "attribute-lookup": (attribute_lookup, 242, 70), + "guard-clauses": (guard_clauses, 264, 104), + "bytes-vs-bytearray": (bytes_vs_bytearray, 308, 86), + "sentinel-iteration": (sentinel_iteration, 320, 92), + "partial-functions": (partial_functions, 334, 36), + # Third coverage push: 24 more figures + "args-kwargs": (args_kwargs, 280, 68), + "multiple-return": (multiple_return, 180, 110), + "lambda-expression": (lambda_expression, 170, 76), + "property-fork": (property_fork, 232, 72), + "metaclass-triangle": (metaclass_triangle, 300, 60), + "sys-path-resolution": (sys_path_resolution, 258, 100), + "import-alias": (import_alias, 212, 56), + "protocol-check": (protocol_check, 220, 78), + "enum-members": (enum_members, 280, 60), + "datetime-instant": (datetime_instant, 280, 88), + "json-python-mapping": (json_python_mapping, 220, 116), + "regex-anchors": (regex_anchors, 200, 92), + "number-parse": (number_parse, 204, 64), + "format-spec": (format_spec, 220, 64), + "truthy-check": (truthy_check, 240, 70), + "boolean-truth-table": (boolean_truth_table, 132, 64), + "set-buckets": (set_buckets, 156, 90), + "tuple-frozen": (tuple_frozen, 280, 48), + "value-types": (value_types, 160, 116), + "yield-delegation": (yield_delegation, 240, 84), + "itertools-chain": (itertools_chain, 246, 82), + "assertion-check": (assertion_check, 304, 76), + "custom-exception-chain": (custom_exception_chain, 220, 90), + "exception-group-peel": (exception_group_peel, 240, 50), + "delete-name-erased": (delete_name_erased, 200, 84), + # Fourth coverage push: 19 figures for constraint-shaped examples + "package-tree": (package_tree, 240, 76), + "venv-boundary": (venv_boundary, 274, 76), + "subprocess-spawn": (subprocess_spawn, 324, 60), + "logging-levels": (logging_levels, 164, 124), + "aaa-pattern": (aaa_pattern, 250, 80), + "socket-byte-boundary": (socket_byte_boundary, 364, 46), + "gil-lanes": (gil_lanes, 300, 100), + "cast-escape": (cast_escape, 184, 56), + "newtype-phantom": (newtype_phantom, 96, 92), + "overload-signatures": (overload_signatures, 304, 64), + "paramspec-preserve": (paramspec_preserve, 294, 60), + "literal-constrained": (literal_constrained, 144, 76), + "callable-type": (callable_type, 196, 40), + "isinstance-check": (isinstance_check, 232, 76), + "collections-containers": (collections_containers, 284, 92), + "typed-dict-shape": (typed_dict_shape, 200, 92), + "csv-records": (csv_records, 212, 96), + "warning-signal": (warning_signal, 292, 80), + "object-lifecycle": (object_lifecycle, 366, 60), + # Fifth pass: slug-specific figures lifting attached scores off the 8.0 floor + "type-alias-name": (type_alias_name, 240, 104), + "match-dispatch-ladder": (match_dispatch_ladder, 260, 130), + "match-pattern-variants": (match_pattern_variants, 272, 96), + "loop-else-gate": (loop_else_gate, 312, 76), + # Sixth pass: lift the lingering 8.0-band figures with slug-specific paint + "literal-forms": (literal_forms, 252, 132), + "function-with-body": (function_with_body, 334, 68), +} + + +# ─── Attachments ─────────────────────────────────────────────────────── + +# slug -> [(anchor, figure_name, caption_or_None), …] +ATTACHMENTS: dict[str, list[tuple[str, str, str | None]]] = { + "mutability": [ + ( + "cell-0", + "aliasing-mutation", + "Two names share one mutable list — appending through one name changes the object visible through both.", + ), + ], + "variables": [ + ( + "cell-0", + "variables-bind", + "A name is a label that points at an object. Assignment binds the label; the object exists independently.", + ), + ], + "lists": [ + ( + "cell-0", + "list-append", + "Lists are mutable sequences. `.append` extends the same list object — no new list is created.", + ), + ], + "dicts": [ + ( + "cell-0", + "dict-buckets", + "Each key is hashed to a bucket; collisions chain into the next slot. Lookup is constant-time on average.", + ), + ], + "unpacking": [ + ( + "cell-0", + "unpacking-bind", + "Left-side names bind to right-side positions; `*rest` gathers the middle into a list.", + ), + ], + "comprehensions": [ + ( + "cell-0", + "comprehension-equivalence", + "A comprehension is a compact spelling of the equivalent for-loop with append, made into one expression.", + ), + ], + "classes": [ + ( + "cell-0", + "class-triangle", + "Every Python value sits on the instance → class → type triangle; the metaclass is the type of the class.", + ), + ], + "inheritance-and-super": [ + ( + "cell-0", + "mro-chain", + "Multiple inheritance forms a graph; C3 linearisation flattens it into the MRO Python uses for attribute lookup.", + ), + ], + "dataclasses": [ + ( + "cell-0", + "dataclass-fields", + "Field declarations become the generated __init__ signature: declaration is the constructor.", + ), + ], + "special-methods": [ + ( + "cell-0", + "operator-dispatch", + "Operators are method calls. `a + b` dispatches to `a.__add__(b)`; the data model exposes the syntax.", + ), + ], + "decorators": [ + ( + "cell-0", + "decorator-rebind", + "@dec rebinds the name to wrapper(f₀); the original function survives only in the wrapper's closure cell.", + ), + ], + "recursion": [ + ( + "cell-1", + "call-stack", + "Each call pushes a new frame with the same name and a smaller argument; the base case unwinds back up the stack.", + ), + ], + "exception-chaining": [ + ( + "cell-0", + "exception-cause-context", + "`raise X from Y` sets `__cause__` (explicit); raising during except sets `__context__` (implicit).", + ), + ], + # Promoted from gestalt with newly-written paint code + "hello-world": [( + "cell-0", "program-output", + "Every Python program starts as source and produces text on standard output. The smallest mental model.", + )], + "numbers": [( + "cell-1", "number-lines", + "Ints have unbounded precision; floats use IEEE doubles whose representable values thin out near the extremes.", + )], + "operators": [( + "cell-0", "expression-tree", + "An expression like `(2 + 3) * 4` parses as a tree; operator precedence and parentheses determine its shape.", + )], + "none": [( + "cell-0", "none-singleton", + "`None` is a single object: every name that points at None points at the same object.", + )], + "equality-and-identity": [( + "cell-0", "identity-and-equality", + "Two names can share one object (`is` and `==` both true) or hold two equal-but-distinct objects (only `==` true).", + )], + "strings": [( + "cell-0", "codepoints-bytes", + "Strings are sequences of Unicode codepoints. UTF-8 encoding turns them into bytes; `é` takes two bytes, `c` takes one.", + )], + "for-loops": [( + "cell-1", "iterator-unroll", + "Each call to next() advances the caret one cell along the iterable — the same shape behind range(), strings, and any sequence.", + )], + "sorting": [( + "cell-1", "sort-stability", + "Python's sort is stable: items with equal keys keep their original order, so chained sorts compose predictably.", + )], + "keyword-only-arguments": [( + "cell-0", "kw-only-separator", + "A bare `*` divides positional or keyword arguments from keyword-only ones; callers must pass `c` and `d` by name.", + )], + "positional-only-parameters": [( + "cell-0", "positional-only-separator", + "A bare `/` divides positional-only arguments from positional-or-keyword ones; callers cannot name `a` or `b`.", + )], + "closures": [( + "cell-0", "closure-cell", + "The inner function keeps a reference into the outer scope's cell, so the captured factor survives the outer return.", + )], + "scope-global-nonlocal": [( + "cell-0", "scope-rings", + "Name lookup walks LEGB — local, enclosing, global, built-in — outward, returning the first binding it finds.", + )], + "generators": [( + "cell-0", "generator-ribbon", + "A generator's body is a timeline cut by yield gates: each next() advances to the next gate; locals survive the pause.", + )], + "type-hints": [( + "cell-0", "annotation-ghost", + "Annotations describe expected types for tools; the runtime accepts any object regardless.", + )], + "exceptions": [( + "cell-0", "exception-lanes", + "try, except, else, and finally as parallel lanes; a single coral path traces what actually runs.", + )], + "context-managers": [( + "cell-0", "context-bowtie", + "A context manager pairs setup with reliable cleanup; the raise path still routes through __exit__.", + )], + "async-await": [( + "cell-0", "async-swimlane", + "On await, the coroutine yields to the loop; the loop runs other work and resumes when the awaitable is ready.", + )], + "iterators": [( + "cell-0", "iter-protocol", + "iter() exposes the iterator behind for; next() pulls one value at a time until exhausted.", + )], + "slices": [( + "cell-0", "slice-ruler", + "Slice indices sit between cells; [:3] and [3:] partition the sequence at index 3, never overlapping or losing an item.", + )], + # Mappings of existing FIGURES to new examples added on main + "operator-overloading": [( + "cell-0", "operator-dispatch", + "Defining `__add__` on a class lets `+` dispatch into the class's own behavior.", + )], + "iterator-vs-iterable": [( + "cell-0", "iter-protocol", + "An iterable knows how to produce an iterator (via iter()); the iterator knows how to produce values (via next()).", + )], + "type-aliases": [( + "cell-0", "type-alias-name", + "A type alias names a complex annotation once so call sites read as the domain meaning, not the type composition.", + )], + "typed-dicts": [( + "cell-0", "typed-dict-shape", + "TypedDict gives each key a typed value, so `obj['x']` is checked against the declared shape.", + )], + "union-and-optional-types": [( + "cell-0", "union-types", + "`int | str | None` says one slot may hold any of three shapes — including expected absence.", + )], + "generics-and-typevar": [( + "cell-0", "generic-preservation", + "A generic preserves the input type through the call: the same T flows in and out of fn[T].", + )], + "abstract-base-classes": [( + "cell-0", "class-triangle", + "An ABC sits on the same triangle as concrete classes; subclasses inherit the abstract methods they must implement.", + )], + "copying-collections": [( + "cell-0", "aliasing-mutation", + "Without copy() two names share the same object; mutating through one is visible through the other.", + )], + # Newly designed figures for examples that previously had none + "truth-and-size": [( + "cell-0", "truth-and-size", + "bool(x) calls __bool__ first; if absent, __len__() != 0; if neither, defaults to True.", + )], + "descriptors": [( + "cell-0", "descriptor-protocol", + "Attribute access on an instance routes through the descriptor's __get__/__set__/__delete__ when the attribute is a descriptor.", + )], + "bound-and-unbound-methods": [( + "cell-0", "bound-unbound", + "Accessing a method via an instance binds self; accessing it via the class returns the underlying function.", + )], + "classmethods-and-staticmethods": [( + "cell-0", "method-kinds", + "Three method kinds, three first-argument conventions: classmethod gets the class, staticmethod gets nothing, instance gets self.", + )], + "callable-objects": [( + "cell-0", "callable-objects", + "Defining __call__ makes any object callable; functions are just one shape that satisfies this protocol.", + )], + "attribute-access": [( + "cell-0", "attribute-lookup", + "obj.x checks instance __dict__, then class __dict__, then __getattr__; the first hit wins.", + )], + "guard-clauses": [( + "cell-0", "guard-clauses", + "Early returns handle the exceptional cases first so the main work is the body of the function, not its tail.", + )], + "bytes-and-bytearray": [( + "cell-0", "bytes-vs-bytearray", + "bytes is a frozen sequence of integers; bytearray is the mutable counterpart with append/extend/etc.", + )], + "sentinel-iteration": [( + "cell-0", "sentinel-iteration", + "`iter(callable, sentinel)` calls the callable repeatedly, stopping when it returns the sentinel.", + )], + "partial-functions": [( + "cell-0", "partial-functions", + "`functools.partial(f, 1)` pre-fills `a=1`, returning a thinner callable `g(b, c)` that only needs the rest.", + )], + # Third coverage push: 24 more attachments — newly designed figures and journey-figure reuse + "args-and-kwargs": [( + "cell-0", "args-kwargs", + "*args captures the extra positionals as a tuple; **kwargs captures the extra keywords as a dict.", + )], + "multiple-return-values": [( + "cell-0", "multiple-return", + "A function returning multiple values really returns one tuple; the caller unpacks it into named bindings.", + )], + "lambdas": [( + "cell-0", "lambda-expression", + "A lambda is a function literal: parameters before the colon, a single expression after, no statement body.", + )], + "properties": [( + "cell-0", "property-fork", + "When x is a property, attribute access routes through fget/fset instead of touching __dict__.", + )], + "metaclasses": [( + "cell-0", "metaclass-triangle", + "A metaclass is the type of a class, just as a class is the type of its instances; type is the default metaclass.", + )], + "modules": [( + "cell-0", "sys-path-resolution", + "An import walks sys.path entry by entry; the first directory containing the module wins.", + )], + "import-aliases": [( + "cell-0", "import-alias", + "`import x as y` binds the name y to the same module object x would have.", + )], + "protocols": [( + "cell-0", "protocol-check", + "An object satisfies a protocol structurally — by having the required methods — not by inheriting it.", + )], + "enums": [( + "cell-0", "enum-members", + "An enum names a fixed set of symbolic values; no new members appear at runtime.", + )], + "datetime": [( + "cell-0", "datetime-instant", + "An aware datetime carries a UTC offset; one instant in time reads differently on two clocks.", + )], + "json": [( + "cell-0", "json-python-mapping", + "Six type pairs bridge the JSON text boundary; each json value maps to one Python type.", + )], + "regular-expressions": [( + "cell-0", "regex-anchors", + "^ and $ anchor the pattern; quantifiers like {2} bound how many times a token repeats.", + )], + "number-parsing": [( + "cell-0", "number-parse", + "int() turns text into a typed number; malformed input raises ValueError instead of guessing.", + )], + "string-formatting": [( + "cell-0", "format-spec", + "The format spec is a railroad of named optional fields: alignment, sign, width, precision, type.", + )], + "truthiness": [( + "cell-0", "truthy-check", + "bool(x) is True except for a small fixed set: 0, 0.0, \"\", [], {}, None, False.", + )], + "booleans": [( + "cell-0", "boolean-truth-table", + "`a and b` returns True only when both are True; otherwise it returns the first falsy value.", + )], + "sets": [( + "cell-0", "set-buckets", + "Sets are hash buckets without values; `x in s` averages O(1) regardless of size.", + )], + "tuples": [( + "cell-0", "tuple-frozen", + "Tuples are ordered, immutable sequences; positions matter, contents do not change once constructed.", + )], + "values": [( + "cell-0", "value-types", + "Every literal is an object with a type; the type carries the behaviour, not the variable name.", + )], + "yield-from": [( + "cell-0", "yield-delegation", + "`yield from inner` delegates iteration to an inner generator; its yields surface here unchanged.", + )], + "itertools": [( + "cell-0", "itertools-chain", + "chain stitches two iterables into one stream without materialising either: values arrive lazily.", + )], + "assertions": [( + "cell-0", "assertion-check", + "assert tests a condition; True passes silently, False raises AssertionError with the optional message.", + )], + "custom-exceptions": [( + "cell-0", "custom-exception-chain", + "Subclassing an existing exception gains a domain name without changing semantics.", + )], + "exception-groups": [( + "cell-0", "exception-group-peel", + "except* peels matched leaves out of an ExceptionGroup; survivors regroup and propagate.", + )], + "delete-statements": [( + "cell-0", "delete-name-erased", + "`del x` removes the name; the object survives if any other reference holds it, otherwise gets collected.", + )], + # Easy promotions: existing journey figures, reused on examples that fit + "conditionals": [( + "cell-0", "branch-fork", + "A predicate sorts a value into one of several branches; if/elif/else is the explicit spelling.", + )], + "match-statements": [( + "cell-0", "match-dispatch-ladder", + "match dispatches by pattern shape; the value flows down the patterns and the first match wins.", + )], + "assignment-expressions": [( + "cell-0", "naming-decisions", + "The walrus binds a name during the surrounding expression; one expression, two outputs.", + )], + "iterating-over-iterables": [( + "cell-0", "iter-protocol", + "`for` desugars to iter()+next(): one iter() call, then next() until StopIteration ends the loop.", + )], + "generator-expressions": [( + "cell-0", "lazy-stream", + "A generator expression composes filter and map lazily; values flow only when next() pulls them.", + )], + "async-iteration-and-context": [( + "cell-0", "async-swimlane", + "async iteration and async with both rest on the same loop-vs-coroutine handoff as await.", + )], + "loop-else": [( + "cell-0", "loop-else-gate", + "The loop's else branch runs only when the loop falls through naturally; break skips it.", + )], + "break-and-continue": [( + "cell-0", "early-exit", + "break exits the loop; continue skips to the next iteration. Both interrupt the natural fall-through.", + )], + "comprehension-patterns": [( + "cell-0", "comprehension-equivalence", + "Nested clauses compose left to right; the comprehension is still equivalent to a for-loop with append.", + )], + "container-protocols": [( + "cell-0", "iter-protocol", + "Container protocols share the iter/next backbone; __iter__ + __next__ make any object iterable.", + )], + "functions": [( + "cell-0", "function-with-body", + "A function takes inputs, evaluates a body, and returns a value: `greet('Ada')` produces `'Hello, Ada'`.", + )], + "constants": [( + "cell-0", "variables-bind", + "UPPER_CASE is a naming convention, not a language constraint; the binding behaves like any other variable.", + )], + "while-loops": [( + "cell-0", "loop-repetition", + "while repeats the body until the condition becomes false; the back-edge returns to the test each pass.", + )], + "advanced-match-patterns": [( + "cell-0", "match-pattern-variants", + "Capture, alternative, guard, and class patterns each name a different way a value can match a case.", + )], + "literals": [( + "cell-0", "literal-forms", + "Each Python type has its own literal spellings; ints accept decimal, hex, and binary; strings accept either quote.", + )], + # Fourth coverage push: constraint-shaped examples + "packages": [( + "cell-0", "package-tree", + "A directory with __init__.py becomes an importable package; submodules and subpackages nest beneath it.", + )], + "virtual-environments": [( + "cell-0", "venv-boundary", + "A venv carries its own interpreter and site-packages, isolating a project's dependencies from the system.", + )], + "subprocesses": [( + "cell-0", "subprocess-spawn", + "subprocess.run spawns a child process and captures its stdout, stderr, and exit code as portable evidence.", + )], + "logging": [( + "cell-0", "logging-levels", + "Five severity levels; the logger's configured threshold drops everything below it.", + )], + "testing": [( + "cell-0", "aaa-pattern", + "arrange-act-assert: set up the state, perform the behavior under test, compare the result to expectations.", + )], + "networking": [( + "cell-0", "socket-byte-boundary", + "Text crosses the socket as bytes — `encode` marks the python → wire boundary, `decode` brings the bytes back to a Python `str`.", + )], + "threads-and-processes": [( + "cell-0", "gil-lanes", + "Threads share memory but the GIL serialises Python bytecode; processes run in parallel with isolated memory.", + )], + "casts-and-any": [( + "cell-0", "cast-escape", + "cast(T, x) tells the type checker to treat x as T; the runtime is unaffected.", + )], + "newtype": [( + "cell-0", "newtype-phantom", + "NewType creates a distinct static identity backed by the same runtime type — UserId is int with a name.", + )], + "overloads": [( + "cell-0", "overload-signatures", + "@overload declares multiple call signatures; one underlying implementation routes input shape to return type.", + )], + "paramspec": [( + "cell-0", "paramspec-preserve", + "ParamSpec preserves the wrapped function's signature through a decorator, parameter for parameter.", + )], + "literal-and-final": [( + "cell-0", "literal-constrained", + "Literal narrows a slot to a fixed set of constant values; Final says the binding will not change.", + )], + "callable-types": [( + "cell-0", "callable-type", + "Callable[[A, B], R] captures the call shape: a tuple of argument types and one return type.", + )], + "runtime-type-checks": [( + "cell-0", "isinstance-check", + "isinstance and issubclass ask the runtime; the answer is a bool, not a static type refinement.", + )], + "collections-module": [( + "cell-0", "collections-containers", + "Four specialised containers for shapes the built-in types don't cover well: deque, Counter, defaultdict, namedtuple.", + )], + "structured-data-shapes": [( + "cell-0", "typed-dict-shape", + "TypedDict names each key's value type; the dict obeys the declared shape at static-check time.", + )], + "csv-data": [( + "cell-0", "csv-records", + "CSV files are rows of records; each line has the same columns in the same order.", + )], + "warnings": [( + "cell-0", "warning-signal", + "A warning is a soft signal: the message is reported, but execution continues unless filters elevate it.", + )], + "object-lifecycle": [( + "cell-0", "object-lifecycle", + "__init__ constructs the object; it lives while at least one reference holds it; __del__ runs when refcount hits zero.", + )], +} + + +# ─── Render helpers ──────────────────────────────────────────────────── + + +def _render_svg(figure_name: str) -> str: + paint, w, h = FIGURES[figure_name] + canvas = Canvas(w=w, h=h) + paint(canvas) + return canvas.to_svg() + + +def render_for_anchor(slug: str, anchor: str) -> str: + """HTML for a banner row sitting AFTER the named cell. Empty if none. + + Cells always keep their prose|code 2-column grid. Figures live in + banner rows that span both columns BETWEEN cells (and after the + walkthrough for single-cell examples). Multiple figures attached to + the same cell share one banner as a small multiple. + """ + attachments = ATTACHMENTS.get(slug, []) + matched = [(name, caption) for (a, name, caption) in attachments if a == anchor] + if not matched: + return "" + figures: list[str] = [] + for name, caption in matched: + cap = f"
    {html.escape(caption)}
    " if caption else "" + figures.append(f"
    {_render_svg(name)}{cap}
    ") + count_class = f" cell-banner--{len(matched)}" + return f'
    {"".join(figures)}
    ' + + +# ─── Journey-section figures ────────────────────────────────────────── +# One figure per journey section, keyed by section title. The figure +# depicts the conceptual shift the section's examples share — the +# journey-section rubric (docs/journey-visualisation-rubric.md) scores +# these. Rendered inline on /journeys/ between the section +# heading and the example list. Reviewed all together on +# /prototyping/journey-figures-gestalt. + +SECTION_FIGURES: dict[str, tuple[str, str]] = { + # Runtime + "Start with executable evidence.": ( + "program-output", + "Every page is a runnable program. The smallest mental model: source produces visible output.", + ), + "Separate value, identity, and absence.": ( + "identity-and-equality", + "Two names can share one object (left, both `is` and `==` true) or hold two equal-but-distinct objects (right, only `==` true).", + ), + "Read expressions as object operations.": ( + "operator-dispatch", + "Operators are method calls. `a + b` dispatches to `a.__add__(b)`; the data model exposes the syntax.", + ), + # Control Flow + "Choose between paths.": ( + "branch-fork", + "A value flows through a predicate to one of several branches.", + ), + "Name and shape decisions.": ( + "naming-decisions", + "The walrus binds a name while the surrounding expression uses its value: one expression, two outputs.", + ), + "Stop as soon as the answer is known.": ( + "early-exit", + "The loop exits at the first match — break short-circuits the rest of the sequence.", + ), + # Iteration + "Choose the right loop shape.": ( + "loop-repetition", + "Walk the sequence, run the body, return; the shape behind for and while.", + ), + "See the protocol behind `for`.": ( + "iter-protocol", + "iter() exposes the iterator behind for; next() pulls one value at a time until exhausted.", + ), + "Compose lazy value streams.": ( + "lazy-stream", + "Filters and maps compose without materialising intermediate lists; values flow through the pipeline only when next() pulls them.", + ), + # Shapes + "Pick the container that matches the question.": ( + "container-questions", + "Each container answers a different question: ordered, fixed, lookup, unique.", + ), + "Move between shapes deliberately.": ( + "reshape-pipeline", + "Most everyday code reshapes data: one input, one transform, one new value.", + ), + "Cross text and data boundaries.": ( + "text-data-boundary", + "Programs receive text and produce structured data; parsing makes the boundary explicit.", + ), + # Interfaces + "Start with functions as named behavior.": ( + "function-signature", + "A function is the first abstraction boundary: arguments in, body, return value out.", + ), + "Use functions as values.": ( + "function-as-value", + "Functions are first-class values. A second name binds to the same function object.", + ), + "Bundle behavior with state.": ( + "class-with-state", + "Classes group fields and methods so data and behavior move together behind one interface.", + ), + # Types + "Keep runtime and static analysis separate.": ( + "annotation-ghost", + "Annotations describe expected types for tools; the runtime accepts any object regardless.", + ), + "Describe realistic data shapes.": ( + "union-types", + "A typed slot can accept one of several shapes — `int | str | None` covers expected absence and alternatives.", + ), + "Scale annotations for reusable libraries.": ( + "generic-preservation", + "A generic type variable preserves shape across a call: the same T flows in and out.", + ), + # Reliability + "Make failure explicit.": ( + "exception-lanes", + "try, except, else, and finally as parallel lanes; the path traced through them is the actual control flow.", + ), + "Control resource and module boundaries.": ( + "context-bowtie", + "A context manager pairs setup with reliable cleanup; the raise path still routes through __exit__.", + ), + "Handle operations that outlive one expression.": ( + "async-swimlane", + "On await, the coroutine yields to the loop; the loop runs other work and resumes when the awaitable is ready.", + ), + # Workers — constraint-shaped sections. + "Replace unavailable process boundaries with portable evidence.": ( + "workers-portable-evidence", + "Worker isolation breaks the usual cross-process pathways; the lesson preserves a captured value as portable evidence instead.", + ), + "Keep network lessons local to the protocol boundary.": ( + "workers-protocol-local", + "Demonstrate the protocol shape (request and response) rather than calling out over the network.", + ), + "Preserve the lesson while respecting the runtime.": ( + "workers-lesson-runtime", + "The lesson's evidence survives across the boundary that the worker runtime enforces.", + ), +} + + +def render_for_section(section_title: str) -> str: + """HTML for a section figure on a journey page. Empty if no + figure is registered for this section title. + """ + entry = SECTION_FIGURES.get(section_title) + if not entry: + return "" + name, caption = entry + cap = f"
    {html.escape(caption)}
    " if caption else "" + return f'
    {_render_svg(name)}{cap}
    ' + + +# ─── Scores (v2 rubric — see docs/example-figure-rubric.md) ──────────── +# Score every attached example figure against the v2 rubric. The dict is +# the single source of truth for both the gestalt review pages +# (scripts/build_marginalia.py, scripts/build_prototypes.py) and any +# future per-example scoring surface. + +SCORES: dict[str, tuple[float, str]] = { + # 9.5 — canonical, definitive depictions of their cell's move + "variables": (9.5, "the canonical name → object picture"), + "mutability": (9.5, "three-state small multiple of aliased mutation"), + "copying-collections": (9.5, "same picture as mutability, perfect match"), + # 9.0 — strong mechanism, runs match the cell, all craft criteria full credit + "hello-world": (9.0, "program → output, smallest mechanism"), + "numbers": (9.0, "int unbounded vs float thinning, both registers"), + "operators": (9.0, "expression tree mechanism"), + "none": (9.0, "three names converging on one None"), + "equality-and-identity": (9.0, "shared vs separate object, side-by-side"), + "strings": (9.0, "codepoints + bytes registers"), + "for-loops": (9.0, "4-row caret advance"), + "sorting": (9.0, "stability ribbons preserved across keys"), + "keyword-only-arguments": (9.0, "signature with explicit `*` separator"), + "positional-only-parameters": (9.0, "signature with explicit `/` separator"), + "closures": (9.0, "captured cell reference"), + "scope-global-nonlocal": (9.0, "LEGB nested rings"), + "recursion": (9.0, "stacked frames with same name, different argument"), + "lists": (9.0, "cells with append mechanism"), + "dicts": (9.0, "hash buckets with collision chain"), + "slices": (9.0, "ruler with bracket overlay"), + "comprehensions": (9.0, "comprehension over equivalent for-loop"), + "type-hints": (9.0, "ghost annotations over runtime values"), + "generators": (9.0, "ribbon cut by yield gates"), + "exceptions": (9.0, "try/except/else/finally lanes with traced path"), + "context-managers": (9.0, "enter / body / exit bowtie"), + "async-await": (9.0, "loop/coro swimlane with await handoffs"), + "classes": (9.0, "instance/class/type triangle"), + "inheritance-and-super": (9.0, "MRO chain with diamond ghost"), + "dataclasses": (9.0, "fields → generated __init__ signature"), + "decorators": (9.0, "before/after rebinding through cell"), + "special-methods": (9.0, "syntax → method dispatch"), + "unpacking": (9.0, "binding-line mechanism with *rest"), + "exception-chaining": (9.0, "__cause__ vs __context__ distinguished"), + "iterating-over-iterables": (9.0, "iter() exposes the iterator"), + "iterators": (9.0, "three-state machine"), + "iterator-vs-iterable": (9.0, "the protocol exposed"), + "container-protocols": (9.0, "iter/next backbone"), + "operator-overloading": (9.0, "dispatch arrow"), + "union-and-optional-types": (9.0, "type fork to several shapes"), + "abstract-base-classes": (9.0, "same triangle as concrete classes"), + "conditionals": (9.0, "predicate forks value to branch"), + "match-statements": (9.0, "dispatch ladder; first match wins"), + "advanced-match-patterns": (9.0, "four pattern variants"), + "loop-else": (9.0, "fell-through vs broke, two outcomes"), + "while-loops": (9.0, "back-edge mechanism"), + "type-aliases": (9.0, "complex annotation collapses to a name"), + "typed-dicts": (9.0, "keys with declared value types"), + "comprehension-patterns": (9.0, "nested clauses compose"), + "lambdas": (9.0, "function literal: params / expression"), + "string-formatting": (9.0, "format-spec railroad"), + "regular-expressions": (9.0, "pattern ruler with anchors"), + "json": (9.0, "two-column type mapping"), + "metaclasses": (9.0, "extended triangle to metaclass"), + "datetime": (9.0, "one instant, two clock offsets"), + "values": (9.0, "every literal is a typed object"), + "literals": (9.0, "literal spellings per type"), + "booleans": (9.0, "2×2 truth table"), + "sets": (9.0, "hash buckets without values"), + "yield-from": (9.0, "stitched ribbons; delegation"), + "generator-expressions": (9.0, "lazy filter→map pipeline"), + "async-iteration-and-context": (9.0, "loop/coro lanes with await yields"), + "assignment-expressions": (9.0, "walrus binds while comparing"), + "break-and-continue": (9.0, "early exit at first match"), + "delete-statements": (9.0, "name erased; object survives if referenced"), + "exception-groups": (9.0, "except* peels matching leaves"), + "custom-exceptions": (9.0, "subclass chain to a domain name"), + "modules": (9.0, "sys.path resolution; first hit wins"), + "protocols": (9.0, "structural duck check"), + "enums": (9.0, "closed set of symbolic values"), + "functions": (9.0, "specific call: greet('Ada') → 'Hello, Ada'"), + "constants": (9.0, "name binding; UPPER_CASE is convention"), + "import-aliases": (9.0, "two names bind to the same module"), + "number-parsing": (9.0, "int() success path vs ValueError"), + "tuples": (9.0, "frozen sequence with struck-through .append"), + "truthiness": (9.0, "bool(x) with the falsy set as a strip"), + "itertools": (9.0, "chain joins two iterables into one stream"), + "assertions": (9.0, "True passes, False raises"), + "descriptors": (9.0, "get/set/delete protocol routed through descriptor"), + "attribute-access": (9.0, "instance __dict__ → class __dict__ → __getattr__"), + "bound-and-unbound-methods": (9.0, "instance.method bound vs Class.method unbound"), + "classmethods-and-staticmethods": (9.0, "three method kinds, three first-arg conventions"), + "callable-objects": (9.0, "__call__ makes any object callable"), + "generics-and-typevar": (9.0, "the same T flows in and out"), + "truth-and-size": (9.0, "__bool__ → __len__ → True fallback chain"), + "bytes-and-bytearray": (9.0, "frozen vs mutable contrast"), + "sentinel-iteration": (9.0, "iter(callable, sentinel) stop condition"), + "partial-functions": (9.0, "f → partial(f, 1) → g"), + "guard-clauses": (9.0, "early returns, main body at the tail"), + "packages": (9.0, "__init__.py + nested submodules"), + "virtual-environments": (9.0, "project / venv boundary"), + "subprocesses": (9.0, "spawn → child → captured output"), + "logging": (9.0, "five thresholded levels"), + "testing": (9.0, "arrange-act-assert three-row pattern"), + "networking": (9.0, "text ↔ bytes across the socket boundary"), + "casts-and-any": (9.0, "Any → cast(T, x) → T, runtime unchanged"), + "newtype": (9.0, "same runtime, distinct static identity"), + "paramspec": (9.0, "P preserved through decorator"), + "literal-and-final": (9.0, "slot narrows to a fixed set"), + "runtime-type-checks": (9.0, "isinstance returns bool"), + "collections-module": (9.0, "deque / Counter / defaultdict / namedtuple"), + "structured-data-shapes": (9.0, "TypedDict named keys with value types"), + "csv-data": (9.0, "rows × columns; same shape per line"), + "warnings": (9.0, "soft signal; execution continues"), + "object-lifecycle": (9.0, "__init__ → live → __del__"), + "args-and-kwargs": (9.0, "*args tuple, **kwargs dict regions"), + "multiple-return-values": (9.0, "function returns tuple; caller unpacks"), + "properties": (9.0, "obj.x routes through fget instead of __dict__"), + # 8.5 — abstract by nature; the figure mostly is the diagram itself + "overloads": (8.5, "multiple signatures → one impl; abstract"), + "callable-types": (8.5, "Callable[[A, B], R] shape; static-only"), + "threads-and-processes": (8.5, "GIL lanes; abstract concurrency model"), +} + + +def figure_score(slug: str) -> tuple[float, str] | None: + """Return the v2 score and rationale for an attached example slug, if any.""" + return SCORES.get(slug) diff --git a/src/marginalia_grammar.py b/src/marginalia_grammar.py new file mode 100644 index 0000000..bf8d5a8 --- /dev/null +++ b/src/marginalia_grammar.py @@ -0,0 +1,403 @@ +"""Marginalia grammar — primitives for the Python By Example diagram set. + +The grammar enforces a single visual language. Cards compose figures from +WORDS and PHRASES; metrics, palette, stroke weights, and typography are +locked at module level. There is no escape hatch for raw SVG. + +Hierarchy: + TOKENS — atomic marks. Never called directly by cards. + WORDS — composable shapes (name_box, object_box, cell, register, …). + PHRASES — recurring multi-word constructions (bind, dispatch, lanes, …). +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass, field +from typing import Callable + +# ─── Palette ─────────────────────────────────────────────────────────── +# Aligned with public/site.css design tokens. These are the only +# colours figures may use; cards never pick a colour directly. +# INK site --text (warm dark brown) +# INK_SOFT site --muted (--text at 70%) +# EMPHASIS site --accent (the brand orange) +# SOFT_FILL site --accent-soft (--accent at ~8%) +INK = "#521000" +INK_SOFT = "rgba(82, 16, 0, 0.7)" +EMPHASIS = "#FF4801" +# A neutral warm tint built from --text at 5%. Object boxes need to read as +# quiet containers; tinting them with --accent-soft made every box look +# highlighted, which broke the "emphasis is scarce" rule. +SOFT_FILL = "rgba(82, 16, 0, 0.05)" + +# ─── Stroke weights ──────────────────────────────────────────────────── +W_HAIRLINE = 0.6 +W_STROKE = 1.0 +W_EMPHASIS = 1.4 +W_GHOST = 0.5 +GHOST_OPACITY = 0.4 +DASH = "2 2" + +# ─── Locked geometry — never override per-card ───────────────────────── +GAP_S = 8 +GAP_L = 16 +DOT_R = 2.5 +TICK_LEN = 6 +NODE_R = 14 +ARROW_OPEN = 5 +ARROW_CLOSED = 7 + +NAME_W = 60 +NAME_H = 24 +OBJECT_W = 80 +OBJECT_H = 32 +CELL = 24 +WORD_W = 44 + +# ─── Typography ──────────────────────────────────────────────────────── +FONT_SERIF = "'Iowan Old Style', Charter, Georgia, serif" +FONT_MONO = "'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" +FONT_SANS = "-apple-system, 'Source Sans Pro', sans-serif" +SIZE_BODY = 11 +SIZE_MONO = 10 +SIZE_SMALL = 9 +SIZE_TAG = 8 +BASELINE = 4 # add to box-center y to render text vertically centered + + +@dataclass +class Canvas: + w: int = 320 + h: int = 110 + parts: list[str] = field(default_factory=list) + + # ── tokens (private; cards should not reach for these) ──────────── + def _add(self, s: str) -> None: + self.parts.append(s) + + def _line(self, x1, y1, x2, y2, *, color=INK, weight=W_STROKE, dash=None, opacity=1.0): + attrs = [f'x1="{x1}"', f'y1="{y1}"', f'x2="{x2}"', f'y2="{y2}"', + f'stroke="{color}"', f'stroke-width="{weight}"'] + if dash: + attrs.append(f'stroke-dasharray="{dash}"') + if opacity < 1.0: + attrs.append(f'opacity="{opacity}"') + self._add(f"") + + def hairline(self, x1, y1, x2, y2): + self._line(x1, y1, x2, y2, weight=W_HAIRLINE) + + def stroke(self, x1, y1, x2, y2): + self._line(x1, y1, x2, y2) + + def ghost(self, x1, y1, x2, y2): + self._line(x1, y1, x2, y2, weight=W_GHOST, opacity=GHOST_OPACITY) + + def dashed(self, x1, y1, x2, y2): + self._line(x1, y1, x2, y2, weight=W_HAIRLINE, dash=DASH) + + def dot(self, x, y, *, emphasis=False): + self._add(f'') + + def tick(self, x, y, *, length=TICK_LEN): + self.hairline(x, y - length / 2, x, y + length / 2) + + def open_arrow(self, x1, y1, x2, y2): + """Axis-style arrow: hairline ending in tiny open V.""" + self.hairline(x1, y1, x2, y2) + dx, dy = x2 - x1, y2 - y1 + L = math.hypot(dx, dy) or 1 + ux, uy = dx / L, dy / L + bx, by = x2 - ARROW_OPEN * ux, y2 - ARROW_OPEN * uy + px, py = -uy * (ARROW_OPEN / 2), ux * (ARROW_OPEN / 2) + self._add( + f'' + ) + + def closed_arrow(self, x1, y1, x2, y2, *, emphasis=False): + """Becomes-this / dispatches-to: line + filled wedge. + + Defaults to ink. Pass emphasis=True only for THE single live arrow + per figure — the one mark the surrounding prose explicitly names. + Saturated --accent strokes everywhere break visual scarcity. + """ + color = EMPHASIS if emphasis else INK + weight = W_EMPHASIS if emphasis else W_STROKE + dx, dy = x2 - x1, y2 - y1 + L = math.hypot(dx, dy) or 1 + ux, uy = dx / L, dy / L + end_x, end_y = x2 - ARROW_CLOSED * ux, y2 - ARROW_CLOSED * uy + self._line(x1, y1, end_x, end_y, color=color, weight=weight) + bx, by = x2 - ARROW_CLOSED * ux, y2 - ARROW_CLOSED * uy + px, py = -uy * (ARROW_CLOSED / 2.5), ux * (ARROW_CLOSED / 2.5) + self._add(f'') + + # ── text ────────────────────────────────────────────────────────── + def _text(self, x, y, s, *, family, size, anchor, color, italic=False, tracking=None): + attrs = [f'x="{x}"', f'y="{y}"', f'font-family="{family}"', f'font-size="{size}"', + f'fill="{color}"', f'text-anchor="{anchor}"'] + if italic: + attrs.append('font-style="italic"') + if tracking: + attrs.append(f'letter-spacing="{tracking}"') + self._add(f"{s}") + + def mono(self, x, y, s, *, anchor="middle", size=SIZE_MONO, color=INK): + self._text(x, y, s, family=FONT_MONO, size=size, anchor=anchor, color=color) + + def ident(self, x, y, s, *, anchor="middle", color=INK): + self._text(x, y, s, family=FONT_SERIF, size=SIZE_BODY, anchor=anchor, color=color, italic=True) + + def label(self, x, y, s, *, anchor="start"): + self._text(x, y, s, family=FONT_SANS, size=SIZE_SMALL, anchor=anchor, color=INK_SOFT) + + def tag(self, x, y, s, *, anchor="start"): + self._text(x, y, s.upper(), family=FONT_SANS, size=SIZE_TAG, + anchor=anchor, color=INK_SOFT, tracking="0.5") + + # ── words ───────────────────────────────────────────────────────── + def name_box(self, x, y, name): + """Open rect with italic identifier. Returns right-edge midpoint.""" + self._add( + f'' + ) + self.ident(x + NAME_W / 2, y + NAME_H / 2 + BASELINE, name) + return (x + NAME_W, y + NAME_H / 2) + + def object_box(self, x, y, type_tag, value, *, w=OBJECT_W, h=OBJECT_H, soft=True, tag_position="above"): + """Filled rect with type tag and value centered. Returns left-edge midpoint. + + tag_position="above" (default) places the type tag at y - 3, just + above the box — natural for an isolated box. Pass + tag_position="inside" when callers stack object_boxes vertically: + the tag then sits in the box's top-left corner instead of + colliding with the box above it. + """ + fill = SOFT_FILL if soft else "none" + self._add( + f'' + ) + if type_tag: + tag_y = y + SIZE_TAG + 2 if tag_position == "inside" else y - 5 + self.tag(x + 4, tag_y, type_tag) + if value: + self.mono(x + w / 2, y + h / 2 + BASELINE, value) + return (x, y + h / 2) + + def cell(self, x, y, content="", *, w=CELL, h=CELL, ghost=False, soft=False): + weight = W_GHOST if ghost else W_STROKE + opacity = f' opacity="{GHOST_OPACITY}"' if ghost else "" + fill = SOFT_FILL if soft else "none" + self._add( + f'' + ) + if content: + self.mono(x + w / 2, y + h / 2 + BASELINE, content) + + def cells(self, x, y, items, *, w=CELL, h=CELL): + """Row of cells. items is a list of strings (use '' for empty).""" + for i, c in enumerate(items): + self.cell(x + i * w, y, c, w=w, h=h) + return (x, y, x + len(items) * w, y + h) + + def caret(self, x, y_top, *, emphasis=True): + """Triangular caret pointing down into the cell whose top is at y_top. + + Defaults to the orange emphasis colour because a caret typically + marks the live position. Set emphasis=False when multiple carets + appear in the same figure (small multiples) and the surrounding + prose only names one of them — the others paint in ink so the + scarce-emphasis rule still holds. + """ + fill = EMPHASIS if emphasis else INK + self._add(f'') + + def register(self, x, y, w, *, divisions=None, between=False): + """Hairline with regular ticks.""" + self.hairline(x, y, x + w, y) + if divisions is None: + return + step = w / divisions + for i in range(divisions + 1): + self.tick(x + i * step, y) + if between: + for i in range(divisions): + self.hairline(x + i * step + step / 2, y - TICK_LEN / 3, + x + i * step + step / 2, y + TICK_LEN / 3) + + def node(self, x, y, label, *, r=NODE_R, ghost=False): + weight = W_GHOST if ghost else W_STROKE + opacity = f' opacity="{GHOST_OPACITY}"' if ghost else "" + self._add( + f'' + ) + self.mono(x, y + BASELINE, label, size=SIZE_SMALL) + + def frame(self, x, y, w, h, *, label=None, ghost=False): + weight = W_GHOST if ghost else W_STROKE + opacity = f' opacity="{GHOST_OPACITY}"' if ghost else "" + self._add( + f'' + ) + if label: + self.tag(x + 6, y - 3, label) + + def gate(self, x, y_top, y_bot): + """Vertical EMPHASIS line crossing a ribbon.""" + self._line(x, y_top, x, y_bot, color=EMPHASIS, weight=W_EMPHASIS) + + def ribbon(self, x, y, w, *, h=30, gates=(), soft_segments=()): + """Horizontal track with optional gates and soft fills.""" + for x0, x1 in soft_segments: + self._add( + f'' + ) + self._add( + f'' + ) + for gx in gates: + self.gate(gx, y, y + h) + + def lane(self, y, *, x0=20, x1=300, label=None): + """Horizontal hairline used for parallel dispatch lanes.""" + self.hairline(x0, y, x1, y) + if label: + self.tag(x0 - 6, y + 3, label, anchor="end") + + # ── phrases ─────────────────────────────────────────────────────── + def bind(self, x, y, name, type_tag, value, *, object_w=OBJECT_W, gap=40): + """name → object. The foundational picture.""" + nx, ny = self.name_box(x, y + (OBJECT_H - NAME_H) / 2, name) + ox, oy = self.object_box(nx + gap, y, type_tag, value, w=object_w) + self.closed_arrow(nx + 2, ny, ox - 2, oy) + return (x, y, nx + gap + object_w, y + OBJECT_H) + + def dispatch(self, x, y, src, dst, *, src_w=70, dst_w=120): + """Source form → method form.""" + self.object_box(x, y, "", src, w=src_w, soft=False) + self.closed_arrow(x + src_w + 4, y + OBJECT_H / 2, x + src_w + 36, y + OBJECT_H / 2) + self.object_box(x + src_w + 40, y, "", dst, w=dst_w, soft=True) + + def connect(self, ax, ay, ar, bx, by, br, *, kind="stroke", offset=0): + """Edge between two circles, terminating tangentially at each boundary. + + Endpoints are computed from the line of centers so the edge meets each + circle exactly — never short of it, never inside it. + + kind: "stroke" | "ghost" | "dashed" | "arrow" | "emphasis" + offset: extra gap past each circle, in viewBox units + (0 for tree edges, 2 for state-machine arrows) + """ + dx, dy = bx - ax, by - ay + L = math.hypot(dx, dy) or 1 + ux, uy = dx / L, dy / L + sx = ax + (ar + offset) * ux + sy = ay + (ar + offset) * uy + ex = bx - (br + offset) * ux + ey = by - (br + offset) * uy + if kind == "stroke": + self.stroke(sx, sy, ex, ey) + elif kind == "ghost": + self.ghost(sx, sy, ex, ey) + elif kind == "dashed": + self.dashed(sx, sy, ex, ey) + elif kind == "arrow": + self.closed_arrow(sx, sy, ex, ey, emphasis=False) + elif kind == "emphasis": + self.closed_arrow(sx, sy, ex, ey, emphasis=True) + else: + raise ValueError(f"unknown connect kind: {kind!r}") + + def lanes(self, ys_labels, *, x0=40, x1=300, path=None): + """Stack of parallel lanes; optional traced emphasis path through them.""" + for y, lab in ys_labels: + self.lane(y, x0=x0, x1=x1, label=lab) + if path: + d = " ".join(("M" if i == 0 else "L") + f"{px},{py}" for i, (px, py) in enumerate(path)) + self._add(f'') + self.dot(path[-1][0], path[-1][1], emphasis=True) + + # ── render ──────────────────────────────────────────────────────── + # Figures render at INTRINSIC_SCALE × their viewBox dimensions. The + # viewBox preserves the geometry (so paint coords, contracts, and + # collision math don't change); the explicit width/height grow so + # browsers render the figure larger by default. CSS max-width on the + # banner container clamps the upper bound; CSS max-width: 100% on + # the SVG scales it down for narrow viewports. The net effect: + # figures fill ~1.6× more horizontal space on desktop and still + # shrink cleanly to mobile widths. + # + # The PAD_* offsets give every figure a small margin around its + # registered canvas. Most figures place a type-tag at y - 5 above + # the topmost box, which without padding renders outside the + # viewBox and gets clipped. PAD_TOP=14 covers the SIZE_TAG=8 font + # plus its baseline offset. PAD_X handles the rare paint function + # that draws slightly negative x. PAD_BOTTOM absorbs small + # accidental overflows. + INTRINSIC_SCALE = 1.6 + + def to_svg(self) -> str: + pad_top, pad_x, pad_bottom = 14, 14, 14 + vb_w = self.w + 2 * pad_x + vb_h = self.h + pad_top + pad_bottom + out_w = round(vb_w * self.INTRINSIC_SCALE) + out_h = round(vb_h * self.INTRINSIC_SCALE) + return ( + f'' + + "".join(self.parts) + + "" + ) + + +@dataclass +class Card: + slug: str + title: str + section: str + order: int | str + figure: Callable[[Canvas], None] + note: str = "" + width: int = 320 + height: int = 110 + is_journey: bool = False + score: float | None = None + score_note: str = "" + + def render_html(self) -> str: + c = Canvas(w=self.width, h=self.height) + self.figure(c) + kind = " journey" if self.is_journey else "" + if isinstance(self.order, int): + eyebrow = f"{self.section} · {self.order:02d}" + else: + eyebrow = f"Journey · {self.order}" + note_html = f'

    {self.note}

    \n' if self.note else "" + score_html = "" + if self.score is not None: + band = ( + "score-high" if self.score >= 9.0 + else "score-mid" if self.score >= 8.0 + else "score-low" + ) + note = f" · {self.score_note}" if self.score_note else "" + score_html = f'

    {self.score:.1f}{note}

    \n' + return ( + f'
    \n' + f'

    {eyebrow}

    \n' + f'

    {self.title}

    \n' + f" {c.to_svg()}\n" + f"{score_html}" + f"{note_html}" + f"
    " + ) diff --git a/src/templates/example.html b/src/templates/example.html index 8d65d86..aa50583 100644 --- a/src/templates/example.html +++ b/src/templates/example.html @@ -6,8 +6,8 @@

    __TITLE__

    __SUMMARY__

    __WALKTHROUGH__
    -

    Notes

    -
      __NOTES__
    +

    Notes

    +
      __NOTES__
    __SEE_ALSO__
    diff --git a/src/templates/layout.html b/src/templates/layout.html index 3ad7f5e..dadc319 100644 --- a/src/templates/layout.html +++ b/src/templates/layout.html @@ -23,6 +23,5 @@
    __CONTENT__
    -
    Examples execute in Cloudflare Dynamic Python Workers.
    diff --git a/tests/fixtures/golden_examples.py b/tests/fixtures/golden_examples.py index 757400e..795364b 100644 --- a/tests/fixtures/golden_examples.py +++ b/tests/fixtures/golden_examples.py @@ -1252,19 +1252,22 @@ 'prose': 'Use `is` and `id()` to observe identity while two names refer to the ' 'same object.'}]}, {'cells': [{'code': 'english = "hello"\n' + 'french = "café"\n' 'thai = "สวัสดี"\n' '\n' - 'for label, word in [("English", english), ("Thai", thai)]:\n' + 'for label, word in [("English", english), ("French", french), ("Thai", ' + 'thai)]:\n' ' print(label, word, len(word), len(word.encode("utf-8")))', 'kind': 'cell', 'line': 17, - 'output': 'English hello 5 5\nThai สวัสดี 6 18', - 'prose': ['Compare an English greeting with a Thai greeting. Both are Python `str` ' - 'values, but UTF-8 uses one byte for each ASCII code point and multiple ' - 'bytes for many non-ASCII code points.']}, + 'output': 'English hello 5 5\nFrench café 4 5\nThai สวัสดี 6 18', + 'prose': ['Compare three words by code-point count and UTF-8 byte count. ASCII ' + 'characters take one byte each (`hello` → 5 bytes); the `é` in `café` is ' + 'one code point but two UTF-8 bytes; each Thai character takes three. The ' + '`str` type abstracts over all three.']}, {'code': 'print(thai[0])\nprint([hex(ord(char)) for char in thai[:2]])', 'kind': 'cell', - 'line': 34, + 'line': 36, 'output': "ส\n['0xe2a', '0xe27']", 'prose': ['Indexing and iteration work with Unicode code points, not encoded bytes. ' '`ord()` returns the integer code point, which is often displayed in ' @@ -1275,14 +1278,15 @@ 'print(clean.upper())\n' 'print(clean.encode("utf-8"))', 'kind': 'cell', - 'line': 48, + 'line': 50, 'output': "café\nCAFÉ\nb'caf\\xc3\\xa9'", 'prose': ['String methods return new strings because strings are immutable. Encoding ' 'turns text into bytes when another system needs a byte representation.']}], 'code': 'english = "hello"\n' + 'french = "café"\n' 'thai = "สวัสดี"\n' '\n' - 'for label, word in [("English", english), ("Thai", thai)]:\n' + 'for label, word in [("English", english), ("French", french), ("Thai", thai)]:\n' ' print(label, word, len(word), len(word.encode("utf-8")))\n' '\n' 'print(thai[0])\n' @@ -1296,6 +1300,7 @@ 'doc_path': '/library/stdtypes.html#text-sequence-type-str', 'doc_url': 'https://docs.python.org/3.13/library/stdtypes.html#text-sequence-type-str', 'expected_output': 'English hello 5 5\n' + 'French café 4 5\n' 'Thai สวัสดี 6 18\n' 'ส\n' "['0xe2a', '0xe27']\n" @@ -1328,13 +1333,16 @@ 'version_notes': None, 'version_sensitive': False, 'walkthrough': [{'code': 'english = "hello"\n' + 'french = "café"\n' 'thai = "สวัสดี"\n' '\n' - 'for label, word in [("English", english), ("Thai", thai)]:\n' + 'for label, word in [("English", english), ("French", french), ("Thai", ' + 'thai)]:\n' ' print(label, word, len(word), len(word.encode("utf-8")))', - 'prose': 'Compare an English greeting with a Thai greeting. Both are Python ' - '`str` values, but UTF-8 uses one byte for each ASCII code point and ' - 'multiple bytes for many non-ASCII code points.'}, + 'prose': 'Compare three words by code-point count and UTF-8 byte count. ASCII ' + 'characters take one byte each (`hello` → 5 bytes); the `é` in `café` ' + 'is one code point but two UTF-8 bytes; each Thai character takes ' + 'three. The `str` type abstracts over all three.'}, {'code': 'print(thai[0])\nprint([hex(ord(char)) for char in thai[:2]])', 'prose': 'Indexing and iteration work with Unicode code points, not encoded ' 'bytes. `ord()` returns the integer code point, which is often ' @@ -7679,8 +7687,13 @@ 'kind': 'unsupported', 'line': 18, 'output': '', - 'prose': ['Dynamic Workers do not provide the `venv` module or a project environment ' - 'workflow.']}, + 'prose': ['`venv.EnvBuilder` configures the description of a new environment, then ' + '`create(".venv")` materialises it on disk as a directory containing its ' + 'own interpreter and `site-packages`. `with_pip=False` skips bootstrapping ' + "pip — useful when the venv is for an isolated tool that doesn't need to " + 'install third-party packages. (This fragment runs in standard Python only ' + "— Dynamic Workers don't provide the `venv` module or a project environment " + 'workflow.)']}, {'code': 'import pathlib\n' 'import tempfile\n' 'import venv\n' @@ -9812,7 +9825,8 @@ 'logger = logging.getLogger("example")\n' 'logger.setLevel(logging.INFO)\n' 'handler = logging.StreamHandler(sys.stdout)\n' - 'handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))\n' + 'formatter = logging.Formatter("%(levelname)s:%(message)s")\n' + 'handler.setFormatter(formatter)\n' 'logger.handlers[:] = [handler]\n' '\n' 'logger.debug("hidden")\n' @@ -9828,7 +9842,8 @@ 'logger = logging.getLogger("example")\n' 'logger.setLevel(logging.INFO)\n' 'handler = logging.StreamHandler(sys.stdout)\n' - 'handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))\n' + 'formatter = logging.Formatter("%(levelname)s:%(message)s")\n' + 'handler.setFormatter(formatter)\n' 'logger.handlers[:] = [handler]\n' '\n' 'logger.debug("hidden")\n' @@ -9864,7 +9879,8 @@ 'logger = logging.getLogger("example")\n' 'logger.setLevel(logging.INFO)\n' 'handler = logging.StreamHandler(sys.stdout)\n' - 'handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))\n' + 'formatter = logging.Formatter("%(levelname)s:%(message)s")\n' + 'handler.setFormatter(formatter)\n' 'logger.handlers[:] = [handler]\n' '\n' 'logger.debug("hidden")\n' @@ -9911,9 +9927,11 @@ '`assertRaises` asserts that a block raises the expected exception type.']}, {'code': 'import io\n' '\n' - 'suite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)\n' + 'loader = unittest.defaultTestLoader\n' + 'suite = loader.loadTestsFromTestCase(AddTests)\n' 'stream = io.StringIO()\n' - 'result = unittest.TextTestRunner(stream=stream, verbosity=0).run(suite)\n' + 'runner = unittest.TextTestRunner(stream=stream, verbosity=0)\n' + 'result = runner.run(suite)\n' 'print(result.testsRun)\n' 'print(result.wasSuccessful())', 'kind': 'cell', @@ -9949,9 +9967,11 @@ ' with self.assertRaises(ZeroDivisionError):\n' ' divide(1, 0)\n' '\n' - 'suite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)\n' + 'loader = unittest.defaultTestLoader\n' + 'suite = loader.loadTestsFromTestCase(AddTests)\n' 'stream = io.StringIO()\n' - 'result = unittest.TextTestRunner(stream=stream, verbosity=0).run(suite)\n' + 'runner = unittest.TextTestRunner(stream=stream, verbosity=0)\n' + 'result = runner.run(suite)\n' 'print(result.testsRun)\n' 'print(result.wasSuccessful())\n', 'doc_path': '/library/unittest.html', @@ -10011,10 +10031,11 @@ 'type.'}, {'code': 'import io\n' '\n' - 'suite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)\n' + 'loader = unittest.defaultTestLoader\n' + 'suite = loader.loadTestsFromTestCase(AddTests)\n' 'stream = io.StringIO()\n' - 'result = unittest.TextTestRunner(stream=stream, ' - 'verbosity=0).run(suite)\n' + 'runner = unittest.TextTestRunner(stream=stream, verbosity=0)\n' + 'result = runner.run(suite)\n' 'print(result.testsRun)\n' 'print(result.wasSuccessful())', 'prose': 'A runner executes the suite and records whether every assertion ' @@ -10029,7 +10050,12 @@ 'kind': 'unsupported', 'line': 18, 'output': '', - 'prose': ['Dynamic Workers do not provide child processes.']}, + 'prose': ['`subprocess.run` spawns a child Python interpreter, captures its stdout ' + 'and stderr (`capture_output=True`), decodes them as text (`text=True`), ' + 'and raises `CalledProcessError` if the child exits non-zero ' + '(`check=True`). The returned `result` holds the captured streams and exit ' + 'code as portable evidence the child ran. (This fragment runs in standard ' + "Python only — Dynamic Workers don't provide child processes.)"]}, {'code': 'import subprocess\n' 'import sys\n' '\n' @@ -10108,7 +10134,13 @@ 'kind': 'unsupported', 'line': 18, 'output': '', - 'prose': ['Dynamic Workers do not provide native threads or child processes.']}, + 'prose': ['`ThreadPoolExecutor` runs `square` across two worker threads sharing the ' + 'same interpreter (and the GIL); `ProcessPoolExecutor` runs `pow` across ' + 'two child processes with isolated memory. Each `pool.map` returns an ' + 'iterator over results in input order, and the surrounding `with` block ' + 'joins the workers when the body exits. (This fragment runs in standard ' + "Python only — Dynamic Workers don't provide native threads or child " + 'processes.)']}, {'code': 'from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor\n' '\n' '\n' @@ -10184,8 +10216,12 @@ 'kind': 'unsupported', 'line': 18, 'output': '', - 'prose': ['Dynamic Workers do not provide arbitrary low-level sockets, and this app ' - 'disables Dynamic Worker outbound access.']}, + 'prose': ['`socketpair()` returns two connected endpoints. `sendall` writes encoded ' + 'bytes into one end, and `recv` reads up to 16 bytes off the other. The ' + 'byte boundary is the whole point: `"ping".encode("utf-8")` produces ' + "`b'ping'`, which is what the socket actually moves. (This fragment runs in " + "standard Python only — Dynamic Workers don't expose arbitrary sockets and " + 'this app disables Worker outbound access.)']}, {'code': 'import socket\n' '\n' 'left, right = socket.socketpair()\n' @@ -10201,8 +10237,11 @@ 'kind': 'cell', 'line': 28, 'output': "b'ping'\nping", - 'prose': ['Sockets exchange bytes. Encoding and decoding make the boundary between ' - 'Python text and network data visible.']}], + 'prose': ['The complete version adds two things: a `try`/`finally` so both endpoints ' + 'close even if `recv` or the surrounding work raises, and a second `print` ' + 'that `decode`s the received bytes back into a Python `str` for display. ' + "The first `print` shows the raw bytes `b'ping'`; the second shows the " + 'decoded text `ping`.']}], 'code': 'import socket\n' '\n' 'left, right = socket.socketpair()\n' @@ -10251,8 +10290,11 @@ 'finally:\n' ' left.close()\n' ' right.close()', - 'prose': 'Sockets exchange bytes. Encoding and decoding make the boundary ' - 'between Python text and network data visible.'}]}, + 'prose': 'The complete version adds two things: a `try`/`finally` so both ' + 'endpoints close even if `recv` or the surrounding work raises, and a ' + 'second `print` that `decode`s the received bytes back into a Python ' + "`str` for display. The first `print` shows the raw bytes `b'ping'`; " + 'the second shows the decoded text `ping`.'}]}, {'cells': [{'code': 'from datetime import date, datetime, time, timedelta, timezone\n' '\n' 'release_day = date(2026, 5, 4)\n' @@ -10280,7 +10322,8 @@ 'prose': ['Use `timedelta` for durations. Adding one to a `datetime` produces another ' '`datetime` without manually changing calendar fields.']}, {'code': 'print(created_at.strftime("%Y-%m-%d %H:%M %Z"))\n' - 'parsed = datetime.fromisoformat("2026-05-04T12:30:00+00:00")\n' + 'iso_text = "2026-05-04T12:30:00+00:00"\n' + 'parsed = datetime.fromisoformat(iso_text)\n' 'print(parsed == created_at)', 'kind': 'cell', 'line': 56, @@ -10301,7 +10344,8 @@ 'print(expires_at.isoformat())\n' '\n' 'print(created_at.strftime("%Y-%m-%d %H:%M %Z"))\n' - 'parsed = datetime.fromisoformat("2026-05-04T12:30:00+00:00")\n' + 'iso_text = "2026-05-04T12:30:00+00:00"\n' + 'parsed = datetime.fromisoformat(iso_text)\n' 'print(parsed == created_at)\n', 'doc_path': '/library/datetime.html', 'doc_url': 'https://docs.python.org/3.13/library/datetime.html', @@ -10371,7 +10415,8 @@ 'prose': 'Use `timedelta` for durations. Adding one to a `datetime` produces ' 'another `datetime` without manually changing calendar fields.'}, {'code': 'print(created_at.strftime("%Y-%m-%d %H:%M %Z"))\n' - 'parsed = datetime.fromisoformat("2026-05-04T12:30:00+00:00")\n' + 'iso_text = "2026-05-04T12:30:00+00:00"\n' + 'parsed = datetime.fromisoformat(iso_text)\n' 'print(parsed == created_at)', 'prose': 'Use `strftime()` for human-facing formatting and `fromisoformat()` ' 'when reading ISO 8601 text back into a `datetime`.'}]}, diff --git a/tests/test_app.py b/tests/test_app.py index c419b1f..25c4a1a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -40,7 +40,7 @@ def test_every_example_renders_literate_cells_with_output(self): for example in list_examples(): with self.subTest(slug=example["slug"]): html = render_example_page(example) - self.assertIn('class="lesson-step lp-cell"', html) + self.assertIn('lesson-step lp-cell', html) self.assertIn('class="cell-source"', html) self.assertIn('class="cell-output"', html) self.assertNotIn('

    Output

    ', html) @@ -274,7 +274,7 @@ def test_example_page_contains_code_docs_and_run_form(self): self.assertIn('