diff --git a/.gitattributes b/.gitattributes index 399843b..79588a9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,11 @@ -# `src/asset_manifest.py` is generated by `scripts/fingerprint_assets.py`. +# `src/asset_manifest.py` is generated by `scripts/fingerprint_assets.py`, +# `src/example_sources_data.py` by `scripts/embed_example_sources.py`, and +# `src/editorial_registry_data.py` by `scripts/embed_editorial_registry.py`. # On merge/rebase, keep our side of the conflict — the post-merge and -# post-rewrite hooks regenerate the file deterministically afterwards. +# post-rewrite hooks regenerate these files 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 +src/example_sources_data.py merge=ours +src/editorial_registry_data.py merge=ours diff --git a/.githooks/post-merge b/.githooks/post-merge index 0dd8606..0f0e54f 100755 --- a/.githooks/post-merge +++ b/.githooks/post-merge @@ -1,9 +1,10 @@ #!/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. +# Regenerate generated files after a merge or pull so embedded data, +# asset manifests, fingerprints, and prototype pages reflect 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" +make build >/dev/null +if ! git diff --quiet src/example_sources_data.py src/editorial_registry_data.py src/asset_manifest.py public/_headers public/prototyping; then + echo "post-merge: generated files regenerated; stage and amend if needed" fi diff --git a/.githooks/post-rewrite b/.githooks/post-rewrite index 04c37d4..86f9612 100755 --- a/.githooks/post-rewrite +++ b/.githooks/post-rewrite @@ -1,9 +1,10 @@ #!/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. +# Regenerate generated files after rebase/amend so embedded data, +# asset manifests, fingerprints, and prototype pages match 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" +make build >/dev/null +if ! git diff --quiet src/example_sources_data.py src/editorial_registry_data.py src/asset_manifest.py public/_headers public/prototyping; then + echo "post-rewrite: generated files regenerated; stage and amend if needed" fi diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c346cb0..5ecc75f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,7 +5,6 @@ ## Verification - [ ] `make verify` -- [ ] `scripts/check_example_migration_parity.py` - [ ] `scripts/format_examples.py --check` - [ ] `make verify-python-version VERSION=3.13` - [ ] `git diff --check` diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 97d206f..7853c54 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -38,21 +38,29 @@ jobs: env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - run: npx --yes wrangler whoami + run: npx --yes wrangler@4.90.0 whoami - name: Sync Python Workers vendor run: uv run pywrangler sync + # Workflow inputs are passed through env vars rather than interpolated + # into the script, so a crafted input cannot inject shell commands into + # steps that hold the Cloudflare API token. - name: Upload Cloudflare Preview env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + PREVIEW_NAME: ${{ inputs.name || 'preview' }} + PREVIEW_MESSAGE: ${{ github.sha }} run: | set -x uv run pywrangler preview \ - --name "${{ inputs.name || 'preview' }}" \ - --message "${{ github.sha }}" \ + --name "$PREVIEW_NAME" \ + --message "$PREVIEW_MESSAGE" \ --json - name: Smoke test deployed Preview - run: scripts/smoke_deployment.py "https://${{ inputs.name || 'preview' }}-pythonbyexample.adewale-883.workers.dev" + env: + PREVIEW_NAME: ${{ inputs.name || 'preview' }} + PBE_SMOKE_BYPASS_SECRET: ${{ secrets.PBE_SMOKE_BYPASS_SECRET }} + run: scripts/smoke_deployment.py "https://${PREVIEW_NAME}-pythonbyexample.adewale-883.workers.dev" - name: Dump wrangler logs on failure if: failure() run: | diff --git a/.github/workflows/regenerate-generated-files.yml b/.github/workflows/regenerate-generated-files.yml index f79408c..64b4b6e 100644 --- a/.github/workflows/regenerate-generated-files.yml +++ b/.github/workflows/regenerate-generated-files.yml @@ -1,11 +1,12 @@ name: Regenerate generated files # Merges made through the GitHub UI bypass the local git hooks that keep -# src/asset_manifest.py and friends in sync, so a merge to main can land with -# stale generated files (and a red Verify run). This workflow regenerates them -# and pushes a fix-up commit. Pushes made with GITHUB_TOKEN do not trigger -# other workflows, so the fix-up commit is not re-verified by CI; it only ever -# contains deterministic `make build` output. +# src/asset_manifest.py, src/example_sources_data.py, and +# src/editorial_registry_data.py in sync, so a merge to main can land with +# stale generated files (and a red Verify run). This +# workflow regenerates them and pushes a fix-up commit. Pushes made with +# GITHUB_TOKEN do not trigger other workflows, so the fix-up commit is not +# re-verified by CI; it only ever contains deterministic `make build` output. on: push: @@ -24,20 +25,37 @@ jobs: 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' - - name: Regenerate embedded examples and fingerprinted assets - run: make build + # Pull before building so the regenerated output reflects the latest + # main, not the triggering SHA — building first then rebasing could + # push stale generated files when another commit lands mid-run. + # merge.ours.driver backs the merge=ours attributes on generated files + # so the rebase never hits conflict markers in them. - name: Commit and push regenerated files if they drifted run: | - git add -A src/example_sources_data.py src/asset_manifest.py public - if git diff --cached --quiet; then - echo "Generated files are current." - exit 0 - fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "Regenerate fingerprinted assets" - git pull --rebase origin main - git push origin main + git config merge.ours.driver true + for attempt in 1 2 3; do + git pull --rebase origin main + make build + git add -A src/example_sources_data.py src/editorial_registry_data.py src/asset_manifest.py public + if git diff --cached --quiet; then + echo "Generated files are current." + exit 0 + fi + git commit -m "Regenerate fingerprinted assets" + if git push origin main; then + exit 0 + fi + echo "Push raced with another commit; retrying (attempt $attempt)." + git reset --soft HEAD~1 + git restore --staged . + done + echo "Failed to push regenerated files after 3 attempts." >&2 + exit 1 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 6211545..cdadbaa 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -2,8 +2,17 @@ name: Verify on: push: + branches: + - main pull_request: +permissions: + contents: read + +concurrency: + group: verify-${{ github.ref }} + cancel-in-progress: true + jobs: verify: runs-on: ubuntu-latest @@ -42,7 +51,6 @@ jobs: CHROME_PATH: /usr/bin/google-chrome run: | make verify - scripts/check_example_migration_parity.py scripts/format_examples.py --check make verify-python-version VERSION=3.13 git diff --check diff --git a/CHANGELOG.md b/CHANGELOG.md index 767081b..0357121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,36 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0 ## Unreleased +### Added + +- Content gates: `check_program_covers_cells.py` (every executable cell anchored in the `:::program` block, with a visible `standalone_cells` opt-out), `check_prose_duplication.py` (verbatim paragraph/note copy-paste residue), and `check_inline_links.py` (prose links must resolve to real internal pages) — all wired into `make quality-checks`. +- Inline `[text](/examples/slug)` and `[text](/journeys/slug)` links render as anchors via the shared `src/textfmt.py` renderer; figure captions render backtick spans as code through the same path. +- A `while-backedge` figure for the while-loops banner that actually draws the back-edge its caption describes, alongside the corrected loop-shape comparison caption. +- Figure contracts: XML well-formedness and text-escaping (Contract 11), mechanical caption rules (Contract 5c), reverse section-figure containment, and an end-to-end check that attached figures appear in rendered pages. +- The marginalia gestalt shows one card per attachment with the production figcaption, so caption/figure disagreements are visible during review; prototype pages regenerate in `make build` and are covered by `make check-generated`. +- Behavioral test suite for Turnstile clearance signing, challenge modes, cookie attributes, and HTML cache keys. +- Security headers on Worker HTML responses and static assets (nosniff, referrer-policy, frame protections, and a full CSP); POST body size cap with a friendly 413 page. +- An end-to-end "adding a new example" workflow and a complete quality-check table in `CONTRIBUTING.md`. + +### Changed + +- The Makefile pins every Python invocation to 3.13 via `uv run`, so `make verify` matches CI regardless of the system interpreter; `make dev`/`deploy` use the workers dependency group. +- CI workflows: deduplicated PR runs with explicit permissions and concurrency; preview workflow passes inputs through env vars (no shell interpolation next to the Cloudflare token), pins wrangler, and passes the smoke-bypass secret; the regenerate workflow pulls before building and retries with a rebuild on push races. +- Quality gates got teeth: curated scores are bounded against the criterion heuristic (`--max-delta`), waivers must carry future ISO expiry dates and are flagged when stale, the journey-section average floor is enforced, confusable-pair tokens must appear inside teaching cells and cannot be satisfied inside a longer sibling token, every `:::note` block is checked, `paired_pages` requires both `see_also` discoverability and registry-named cell evidence, scope-first-pass pages must link registry-named focused neighbors, and the rubric-audit snapshot computes its findings from live registries instead of hardcoding PASS verdicts. +- Editorial metadata moved from Python literals into `docs/quality-registries.toml`: journeys, see-also edge labels, figure attachments, captions, and curated scores now load through `src/editorial_registry.py`. +- Removed the migration-era golden fixture and parity/refresh scripts after the Markdown-backed release window; ordinary content edits now rely on verifiers, content gates, generated diffs, and browser/runtime checks rather than snapshot refreshes. +- ASGI bridge: WebSocket close events reach the close handler (`onclose` was assigned to `onopen`), headers use latin-1 per the ASGI spec, and application lifespan runs once per Worker instead of per request. +- Unknown `TURNSTILE_CHALLENGE_MODE` values fail closed to requiring a challenge; the smoke-bypass header compares in constant time. +- Template substitution is single-pass, so submitted code containing `__TOKEN__` text can no longer corrupt the rendered page; embedded JSON escapes `<` so `` in example code cannot break out of the inline script. +- Worker HTML cache keys ignore ordinary query strings (routes ignore them too), so arbitrary `?utm=` or cache-busting params cannot fragment the rendered-page cache. +- FastAPI GET handlers now call renderers directly instead of delegating to the legacy pure-Python `route()` helper and reparsing URLs. +- Oversized request bodies are capped in the ASGI bridge before the FastAPI app reads them, so the Worker no longer buffers unbounded POST bodies before rejecting them. +- Deployment smoke refuses to send the Turnstile smoke-bypass secret to non-HTTPS origins. +- Worker and static responses now include HSTS alongside CSP, frame, referrer, and nosniff headers. +- Check-script boilerplate is consolidated through `scripts/_common.py`; quality gates share the canonical loader, registry reader, and frontmatter parser. +- Small accent-colored text and the primary Run button use WCAG-AA contrast colors; the editor textarea has an accessible label. +- Content corrections across 29 example pages: dict-iteration RuntimeError scoped to key insertion/removal, PEP 695 `type` statement teaching, NotImplemented-returning operator methods, visible positional-only/keyword-only TypeError demos, divergent broad-except demonstration, honest sandbox-boundary framing for subprocesses/threads/networking, cell-specific prose replacing copied intros, and four broken inline links now rendering as real anchors. + ## 2026-05-16 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3c3c5e..ec8fd20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,7 +49,6 @@ Then run: ```bash make verify -scripts/check_example_migration_parity.py scripts/format_examples.py --check make verify-python-version VERSION=3.13 git diff --check @@ -64,17 +63,38 @@ make check-generated ## Quality checks -Five scripts enforce the catalog-level rules from `docs/example-quality-rubric.md`. Run them together with `make quality-checks`. +These scripts enforce the catalog-level rules from `docs/example-quality-rubric.md`. Run them together with `make quality-checks`. | Script | What it gates | | --- | --- | -| `scripts/check_registry_integrity.py` | Every owner slug in `docs/quality-registries.toml` exists in `manifest.toml`; tokens are present. | -| `scripts/check_confusable_pairs.py` | Each confusable pair's owning page contains every token that signals the contrast. | -| `scripts/check_broad_surface_tours.py` | Each broad-title page either covers every required form or sets `scope_first_pass = true` with `see_also` links to focused neighbors. | +| `scripts/check_registry_integrity.py` | Every owner slug in `docs/quality-registries.toml` exists in `manifest.toml`; tokens are present; each `paired_pages` pair is discoverable through `see_also` and has registry-named cell tokens demonstrated inside a teaching cell. | +| `scripts/check_confusable_pairs.py` | Each confusable pair's owning page contains every token inside teaching cells; a token shadowed inside a longer sibling token (plain `def` inside `async def`) does not count. | +| `scripts/check_broad_surface_tours.py` | Each broad-title page either covers every required form or sets `scope_first_pass = true` with registry-named `focused_neighbors` linked through `see_also`. | | `scripts/check_footgun_coverage.py` | Each canonical Python footgun has a page that contains both broken-form and fixed-form tokens. | -| `scripts/check_notes_supported.py` | Every `:::note` bullet shares at least one keyword with the page body, so notes cannot assert behavior the page never demonstrates. | - -The single source of truth for the registries is `docs/quality-registries.toml`. Add a new pair, broad tour, or footgun there, then update the owning page so the tokens appear in cells or prose. +| `scripts/check_notes_supported.py` | Every `:::note` bullet (across all note blocks) shares at least one keyword with the page body outside the notes, so notes cannot assert behavior the page never demonstrates. | +| `scripts/check_program_covers_cells.py` | Every executable cell shares substantive code with the `:::program` block, so the editor reproduces what the cells teach; `standalone_cells = true` opts out visibly. | +| `scripts/check_prose_duplication.py` | No verbatim repeated paragraphs, no cell prose copied from the intro, no duplicate note bullets. | +| `scripts/check_inline_links.py` | Inline `[text](target)` links in prose resolve to real `/examples` or `/journeys` pages. | +| `scripts/score_example_criteria.py` | Heuristic criterion scores per example; fails when a curated score exceeds the heuristic by more than the delta bound, so the score registry cannot inflate without the page changing. | +| `scripts/check_quality_scores.py` | Curated scores meet the 9.0 target / 8.5 hard minimum, waivers carry future expiry dates and are dropped when stale, and the journey-section average stays above its floor. | +| `scripts/check_no_figure_rationales.py` | No-figure opt-outs carry a reason and an unexpired `review_after` date. | +| `scripts/check_journey_outcomes.py` | Every journey section declares learner outcomes. | +| `scripts/audit_example_graph.py --check` | The `see_also` graph has no broken targets, self-links, over-linked pages, or orphaned examples. | + +The single source of truth for the registries is `docs/quality-registries.toml`. Add confusable pairs, broad tours, footguns, journey metadata, figure attachments, captions, and curated scores there, then update the owning page so the tokens appear in cells or prose. + +## Adding a new example end to end + +1. Write `src/example_sources/.md` (frontmatter, intro prose, one `:::program`, cells, notes) and add the slug to `manifest.toml`'s `order`. +2. Add `see_also` links in both directions — `scripts/audit_example_graph.py --check` fails orphaned pages. +3. Score the page against `docs/example-quality-rubric.md` and add the `[[example_quality_scores]]` entry to `docs/quality-registries.toml`. +4. Attach a figure by adding `[[figure_attachments]]` and `[[example_figure_scores]]` entries to `docs/quality-registries.toml` (or add a `no_figure_rationales` entry with a review date); keep only new paint functions in `src/marginalia.py`. Review the result on `public/prototyping/marginalia-gestalt.html`, which shows the production caption under every figure. +5. Run `make build`, `make verify-examples`, `make quality-checks`, and `scripts/format_examples.py --check`. +6. Run the full `make verify` with a local Worker running, then commit — including the regenerated `src/example_sources_data.py`, `src/editorial_registry_data.py`, `src/asset_manifest.py`, and fingerprinted assets. + +## Secrets and deploy configuration + +Deployment uses repository secrets: `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` (Preview workflow), plus optional `PBE_SMOKE_BYPASS_SECRET` so deploy smoke tests can run edited-code POSTs past the Turnstile challenge. Runtime Worker secrets (`TURNSTILE_SECRET_KEY`, `TURNSTILE_CLEARANCE_SECRET`, `PBE_SMOKE_BYPASS_SECRET`) are managed with `wrangler secret put`; see `docs/turnstile-runner-protection-spec.md`. ## Style expectations diff --git a/Makefile b/Makefile index afaa442..d5244d6 100644 --- a/Makefile +++ b/Makefile @@ -1,62 +1,86 @@ -.PHONY: test embed-examples build check-generated fingerprint browser-layout-test seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy lint +# All Python tooling runs through uv pinned to 3.13 so the suite works on any +# machine regardless of the system python3 (examples use 3.12+ `type` syntax). +PY := uv run --python 3.13 + +.PHONY: test embed-examples embed-editorial-registry build check-generated fingerprint prototypes browser-layout-test seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported check-program-covers-cells check-prose-duplication check-inline-links score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes audit-example-graph quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy lint test: - uv run --python 3.13 python -m unittest discover -s tests -v + $(PY) python -m unittest discover -s tests -v embed-examples: - scripts/embed_example_sources.py + $(PY) scripts/embed_example_sources.py + +embed-editorial-registry: + $(PY) scripts/embed_editorial_registry.py -build: embed-examples fingerprint +build: embed-examples embed-editorial-registry fingerprint prototypes check-generated: build - git diff --exit-code src/example_sources_data.py src/asset_manifest.py public/_headers + git diff --exit-code src/example_sources_data.py src/editorial_registry_data.py src/asset_manifest.py public/_headers public/prototyping + test -z "$$(git status --porcelain public/prototyping public/*.css public/*.js)" -fingerprint: embed-examples - scripts/fingerprint_assets.py +fingerprint: embed-examples embed-editorial-registry + $(PY) scripts/fingerprint_assets.py + +prototypes: embed-examples + $(PY) scripts/build_prototypes.py + $(PY) scripts/build_marginalia.py browser-layout-test: scripts/check_browser_layout.mjs seo-cache-lint: - scripts/lint_seo_cache.py + $(PY) scripts/lint_seo_cache.py verify-examples: build - scripts/verify_examples.py + $(PY) scripts/verify_examples.py check-registry-integrity: - scripts/check_registry_integrity.py + $(PY) scripts/check_registry_integrity.py check-confusable-pairs: - scripts/check_confusable_pairs.py + $(PY) scripts/check_confusable_pairs.py check-broad-surface-tours: - scripts/check_broad_surface_tours.py + $(PY) scripts/check_broad_surface_tours.py check-footgun-coverage: - scripts/check_footgun_coverage.py + $(PY) scripts/check_footgun_coverage.py check-notes-supported: - scripts/check_notes_supported.py + $(PY) scripts/check_notes_supported.py + +check-program-covers-cells: + $(PY) scripts/check_program_covers_cells.py + +check-prose-duplication: + $(PY) scripts/check_prose_duplication.py + +check-inline-links: + $(PY) scripts/check_inline_links.py score-example-criteria: - scripts/score_example_criteria.py --limit 12 + $(PY) scripts/score_example_criteria.py --limit 12 check-quality-scores: - scripts/check_quality_scores.py + $(PY) scripts/check_quality_scores.py check-no-figure-rationales: - scripts/check_no_figure_rationales.py + $(PY) scripts/check_no_figure_rationales.py check-journey-outcomes: - scripts/check_journey_outcomes.py + $(PY) scripts/check_journey_outcomes.py + +audit-example-graph: + $(PY) scripts/audit_example_graph.py --check -quality-checks: check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes +quality-checks: check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported check-program-covers-cells check-prose-duplication check-inline-links score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes audit-example-graph rubric-audit: - scripts/audit_rubric_snapshot.py + $(PY) scripts/audit_rubric_snapshot.py format-examples: - scripts/format_examples.py + $(PY) scripts/format_examples.py verify-python-version: build uv run --python $(VERSION) scripts/verify_examples.py --python-version $(VERSION) @@ -67,10 +91,10 @@ lint: verify: build test seo-cache-lint verify-examples quality-checks browser-layout-test lint check-generated dev: - uv run pywrangler dev --port 9696 + uv run --group workers pywrangler dev --port 9696 smoke-deployment: - scripts/smoke_deployment.py $(URL) + $(PY) scripts/smoke_deployment.py $(URL) deploy: build - uv run pywrangler deploy + uv run --group workers pywrangler deploy diff --git a/README.md b/README.md index ce86e72..1d0ccb0 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,18 @@ Production: (`workers.dev` remains enabled as - 109 curated Python 3.13 examples in learning order - Literate source/output cells for each example walkthrough +- Curated SVG figures (marginalia) attached to teaching cells and journey sections, drawn by a locked diagram grammar with geometry contracts +- Guided learning journeys grouping examples into outcome-driven sections - Editable complete examples powered by CodeMirror - Read-only syntax highlighting powered by Shiki - Example execution in isolated Cloudflare Dynamic Python Workers +- Cloudflare Turnstile challenge protecting the edited-code runner - Official Python 3.13 documentation links per example - Workers Assets for static files - Fingerprinted CSS/JS assets with immutable cache headers - Versioned Worker Cache API keys for rendered HTML - SEO metadata and canonical URLs for home and example pages +- A quality-gate suite (registries, rubric scores, coverage and structure checks) that fails the build when content regresses ## Attribution @@ -43,7 +47,9 @@ Python documentation links point to the official [Python documentation](https:// - `src/example_sources/*.md` contains the human-editable examples. - `src/example_loader.py` parses Markdown examples into the runtime catalog. - `src/examples.py` is a compatibility shim for the loaded catalog. -- `src/example_sources_data.py` is generated embedded source data for Cloudflare Workers. +- `src/example_sources_data.py` and `src/editorial_registry_data.py` are generated embedded data for Cloudflare Workers. +- `src/marginalia.py` keeps executable figure paint functions; attachments, captions, scores, journeys, and edge labels live in `docs/quality-registries.toml` and load through `src/editorial_registry.py`. +- `src/textfmt.py` renders inline prose (code spans and internal links) for pages and figure captions. - `public/` contains static assets served by Workers Assets. - `python_modules/` is generated by the Workers tooling and intentionally ignored by Git. @@ -95,7 +101,6 @@ Then run the main checks before deploying or pushing: ```bash make verify -scripts/check_example_migration_parity.py scripts/format_examples.py --check make verify-python-version VERSION=3.13 git diff --check @@ -136,6 +141,7 @@ public/site..css public/syntax-highlight..js public/editor..js src/asset_manifest.py +src/editorial_registry_data.py ``` Rendered HTML uses only fingerprinted asset URLs. `public/_headers` gives these immutable cache headers: @@ -154,7 +160,6 @@ Run checks, then deploy: ```bash make verify -scripts/check_example_migration_parity.py scripts/format_examples.py --check make deploy ``` @@ -174,6 +179,7 @@ Each example has: - TOML frontmatter with `slug`, `title`, `section`, `summary`, and `doc_path` - exactly one `:::program` block for the full editable source - one or more `:::cell` blocks for verified prose/source/output teaching cells +- optional `:::unsupported` blocks for displayed-but-not-executed fragments (subprocesses, sockets, threads) whose prose names the sandbox boundary - optional `:::note` blocks After editing examples, run: @@ -181,12 +187,22 @@ After editing examples, run: ```bash make build make verify-examples +make quality-checks scripts/format_examples.py --check -scripts/check_example_migration_parity.py make check-generated ``` -`src/example_sources_data.py` is generated and committed so Cloudflare Workers can load examples in production. Do not edit it by hand. +The migration-era golden fixture has been removed; intentional content edits are +reviewed through the Markdown diff, generated-source diff, verifier output, and +quality gates rather than a refreshed snapshot. + +`make quality-checks` runs the content gates: registry integrity, confusable +pairs, broad-surface tours, footguns, notes grounding, program-covers-cells, +prose duplication, inline links, criterion scoring with a curated-score delta +bound, quality scores (waiver expiry, journey average), no-figure rationales, +journey outcomes, and the see_also graph audit. + +`src/example_sources_data.py` and `src/editorial_registry_data.py` are generated and committed so Cloudflare Workers can load examples and editorial registries in production. Do not edit them by hand. For a Python version migration, update `python_version` and `docs_base_url` in `src/example_sources/manifest.toml`, then run: diff --git a/docs/codebase-audit-2026-06-11.md b/docs/codebase-audit-2026-06-11.md new file mode 100644 index 0000000..bc04bf9 --- /dev/null +++ b/docs/codebase-audit-2026-06-11.md @@ -0,0 +1,523 @@ +# Codebase audit — 2026-06-11 + +Full audit of code, docs, and internal consistency, plus improvement +suggestions. Method: every source file, script, workflow, doc, and all 109 +example sources were read in full; the verify pipeline, tests (90/90 pass), +quality gates, and example verification were executed under Python 3.13; +high-severity claims were re-verified directly before inclusion. + +Severity legend: **high** = user-visible defect, broken workflow, or gate that +cannot do its job; **med** = real defect or drift with limited blast radius; +**low** = latent bug, hygiene, or polish. + +## 1. Verified-healthy baseline + +These were checked and are in good shape — worth knowing what *not* to spend +time on: + +- All 90 unit tests pass; all 109 examples execute with byte-exact outputs + under 3.13; golden parity reports 100%; `ruff` clean; generated files and + `uv.lock` fresh. +- Manifest ↔ files ↔ journeys ↔ `see_also` graph: zero broken references, + no duplicate titles/slugs, every example has `see_also`, every journey item + resolves. +- XSS surface is sound: every dynamic template substitution is + `html.escape`d; user code reaches only escaped sinks and the dynamic Worker + via `repr()`; `editor.js`/`syntax-highlight.js` use `innerHTML` only on + trusted markup. All 28 template tokens are supplied. +- The Dynamic Worker sandbox is locked down correctly (`globalOutbound: + null`, `disable_python_external_sdk`, `subRequests: 0`, `cpuMs: 1000`) and + consistent between `src/main.py` and `wrangler.jsonc`. +- `docs/observability-spec.md` and `docs/turnstile-runner-protection-spec.md` + match the implementation nearly line-for-line; CHANGELOG spot-checks + against git history hold; CI (`verify.yml`) runs exactly the documented + pre-push command set. +- All 123 marginalia figures render well-formed XML, use no `id` attributes, + and all 24 geometry contracts pass; all current figure anchors resolve. + +## 2. Bugs in code + +- **med** `tests/test_marginalia_geometry.py:470` — the figure-anchor + contract validates anchors against `ex["walkthrough"]`, but + `src/app.py:739-760` renders `ex["cells"]`. The two lists diverge on 10 of + 109 examples in both directions (loader emits one walkthrough entry per + prose paragraph and drops `unsupported` cells). A `cell-4` anchor on + `datetime` would pass the contract yet never render; a valid `cell-1` + anchor on `virtual-environments` would be falsely rejected. Latent today — + all current anchors happen to resolve against the real cell list. +- **med** `src/marginalia_grammar.py:203-210` — `Canvas._text` interpolates + figure-internal text into SVG without XML escaping (captions are escaped; + internal labels are not). A future label containing `<` or `&` ships + malformed markup silently; no test parses the SVG with an XML parser. All + 123 current figures verified well-formed, so latent. +- **med** `src/worker_asgi_bridge.py:264` — `server.onopen = onclose` + (should be `server.onclose = onclose`) in the websocket path. Unused by + the app today; latent vendored-code bug. +- **med** `scripts/check_notes_supported.py:48` — `NOTE_RE.search` examines + only the first `:::note` block, while the loader supports any number and + README/CONTRIBUTING say "blocks" plural. Bullets in a second block are + never checked and additionally count as page "body" that can ground + bullets in the first. Latent — no current page has two note blocks. +- **low** `src/app.py:430-433` + `770-791` — `_replace` applies + `str.replace` sequentially and `html.escape` preserves `__TOKEN__` text, + so user-submitted code containing a literal template token (e.g. + `__SHOWN_OUTPUT__`) gets later substitution values injected into the + echoed editor content. Verified by simulation. Not XSS (textarea is + RCDATA; injected values contain no ``), but round-tripped code + is silently corrupted. +- **low** `src/app.py:790` + `src/templates/example.html` — + `ORIGINAL_CODE_JSON` uses `json.dumps` with no `<`/`/` escaping inside an + inline `` + would break the page. Not user-controllable (the slot always carries + curated code); fix by escaping `<` as `<`. +- **low** `scripts/lint_seo_cache.py:19,43` — the hashed-asset regex and the + unfingerprinted-reference check cover `site.css` and `syntax-highlight.js` + but omit `editor.js`, one of the three fingerprinted assets the lint + exists to protect. +- **low** `src/worker_asgi_bridge.py:55,171` — header bytes are + encoded/decoded as UTF-8; the ASGI spec uses latin-1. ASCII-safe today. +- **low** `scripts/capture_browser_screenshot.mjs:8` — `CHROME_PATH` + defaults to the macOS app path unconditionally; the sibling + `check_browser_layout.mjs` correctly switches on platform. +- **low** `scripts/verify_examples.py:55` — a frontmatter `slug` that + doesn't match its filename crashes with a `FileNotFoundError` traceback + instead of a clean per-file error (the spec calls slug↔filename matching a + required check). + +## 3. Security + +- **med** `.github/workflows/preview.yml:50-55` — `${{ inputs.name }}` is + interpolated directly into `run:` scripts in steps whose env contains + `CLOUDFLARE_API_TOKEN`. A dispatch input like `x$(curl …)` executes in the + shell and can exfiltrate the token. Limited to users with workflow-dispatch + rights; standard fix is passing inputs via `env:`. +- **med** `public/_headers` + `src/main.py` — no security headers anywhere on + Worker HTML: no CSP, `X-Content-Type-Options`, `X-Frame-Options`/ + `frame-ancestors`, `Referrer-Policy`, or HSTS. Defense-in-depth gap on a + site that reflects user-submitted code; the Run button is clickjackable. +- **low** `src/main.py:134` — the smoke-bypass header is compared with `==` + rather than `hmac.compare_digest` (the clearance-cookie path uses + `compare_digest` correctly). +- **low** `src/main.py:159-165` — `_requires_turnstile` fails open: any + unrecognized `TURNSTILE_CHALLENGE_MODE` value (e.g. a typo of `session`) + silently disables the challenge. +- **low** `src/main.py:137-156` — the clearance cookie signs only the expiry + integer: an unbound, replayable bearer token for its lifetime (8 h default, + 7 d max). Consistent with "once per session" intent; noting the design. +- **low** `src/main.py:191-264` + bridge — no application-level size limit on + POSTed code, and the ASGI bridge buffers the entire request body in memory + before the app runs; bounded only by platform caps. +- **low** `src/main.py:77-88` — every distinct query string on a cacheable + GET creates a separate edge-cache entry for an identical body (the app + ignores the query). Amplification, not poisoning — the appended + `__html_v` is always the real version. +- **low** `src/templates/example.html` — `#code=` prefills the + editor from the URL hash. Not XSS (string assignment), and execution still + passes Turnstile + sandbox; limited to social-engineering a sandboxed run. +- **low** `src/main.py:428-434` — non-200 responses on the cacheable GET + path get no `Cache-Control`, allowing browser heuristic caching of error + pages. + +## 4. Toolchain, build, and CI + +- **high** `Makefile` + `README.md:66` — only `test` and + `verify-python-version` run through `uv run --python 3.13`; every other + target (`build`, `verify-examples`, `seo-cache-lint`, all quality checks) + invokes scripts via `#!/usr/bin/env python3` shebangs. On any machine whose + system python isn't 3.13, `make verify` dies on the first example using the + 3.12+ `type` statement — while README explicitly claims "a system `python3` + on another version still works." Reproduced under python 3.11. +- **med** `Makefile:70` — `make dev` runs `uv run pywrangler dev` without + `--group workers`; `pywrangler` lives in the `workers` dependency group, so + the documented target fails on a fresh checkout. README/CONTRIBUTING give + the correct `uv run --group workers …` command. +- **med** `.githooks/post-merge`, `.gitattributes:7` — the conflict-avoidance + machinery covers `src/asset_manifest.py` only; `src/example_sources_data.py` + is equally generated and conflicts on every concurrent example edit, with no + `merge=ours` driver and no hook regeneration. Hooks should run `make build` + and `.gitattributes` should cover both files. +- **med** `.github/workflows/regenerate-generated-files.yml:32-43` — builds + from the triggering SHA, then `git pull --rebase` before pushing: a + mid-run push can land stale regenerated files on main (GITHUB_TOKEN pushes + skip CI), and the next self-heal run hits a rebase conflict because the + `merge=ours` driver is never configured in CI. Rebuilding after the pull + closes the race. +- **med** `Makefile:12` vs that workflow — `check-generated` diffs only + `example_sources_data.py`, `asset_manifest.py`, and `public/_headers`; new + or stale fingerprinted copies under `public/` are invisible to it (the + regenerate workflow stages all of `public/`), so a PR can pass with + `public/` out of sync and only self-heal after merge. +- **low** `verify.yml:3-5` — `push` + `pull_request` triggers double-run + every same-repo PR push (each run boots a Worker and Chrome and executes + all 109 examples several times); no `concurrency` group, no `permissions:` + block (the other two workflows set one). +- **low** `verify.yml:24` vs `preview.yml:41` — verify pins + `wrangler@4.90.0`; preview uses unpinned `npx --yes wrangler`, so the two + workflows can exercise different Wrangler majors. +- **low** `preview.yml` + `scripts/smoke_deployment.py:109` — + `PBE_SMOKE_BYPASS_SECRET` is set nowhere in CI, so POST smoke checks will + false-fail the moment Turnstile secrets are enabled on the target worker; + the script also doesn't require https for the origin it sends the secret + to. + +## 5. Quality-gate integrity + +The editorial gate layer is partly decorative — several documented gates +cannot fail or are enforced nowhere: + +- **high** `scripts/score_example_criteria.py:105-119` — `main()` returns 0 + unconditionally (`--below`/`--limit` only filter printing), yet it is wired + into `make quality-checks` → `make verify` as if it were a gate. Verified: + `--below 99` still exits 0. +- **med** `scripts/check_quality_scores.py:64` + + `docs/quality-registries.toml:191` — waiver `expires` is only checked to be + a non-empty string; the one shipped waiver says `expires = "never"`. Same + for `review_after` in `check_no_figure_rationales.py`, whose docstring + claims it prevents stale entries. A waiver whose score recovers is never + flagged (backlog entries are). +- **med** `docs/quality-registries.toml:185` — `journey_average_min = 8.8` + is declared under `[quality_gates]` but read by no script, source file, or + test. +- **med** `docs/quality-registries.toml:172-180` — `paired_pages` documents + "at least one member of each pair must demonstrate the relationship in a + cell"; the only consumer validates slug existence. The actual rule is + unenforced. +- **med** `scripts/check_confusable_pairs.py` + registry — several token + sets are degenerate and unfailable: `"def "` is a substring of every + `"async def "`, so the sync-vs-async pair can't detect loss of sync + content; `"return"`, `"list"`, `"tuple"`, and `" is "` match ordinary + prose. +- **med** `src/marginalia.py:2331-2449` — the only fail-able content gate + keys off hand-edited numbers (`EXAMPLE_QUALITY_SCORES`), with no range + validation and no bound on curated-vs-heuristic delta; scores have + collapsed to near-constants (108×9.0 + one 7.1), so the registry no longer + discriminates. +- **low** `scripts/audit_rubric_snapshot.py:150-338` — hardcodes all 32 + dimension verdicts as PASS, the scoreboard line, the entire "audit + conclusion" paragraph, and `--date` defaults to 2026-05-12. It is a report + generator labeled as an audit; re-running it after real drift would + regenerate a document asserting facts it never checked. +- **low** `scripts/audit_example_graph.py` — the only check-style script + wired into no Makefile target and no workflow; its unique value (out-degree + cap, orphan detection) is never enforced. Its `--check` help text is also + wrong about what fails without the flag. +- **low** `scripts/check_broad_surface_tours.py:44-49` — + `scope_first_pass = true` bypasses required tokens if `see_also` is merely + non-empty; the registry declares no expected neighbors, so the documented + contract isn't what's checked. Latent (no page sets the flag). +- **low** spec-required checks that exist nowhere: line-length enforcement + for code/prose (`docs/example-source-format-spec.md` "strict tooling"), + and slug↔filename verification (crashes instead; see §2). +- **low** `public/prototyping/*.html` — 6 of 7 committed gestalt review + pages are stale relative to their generators (missing + `aria-hidden`/`focusable` attrs, old y-offsets); `check-generated` doesn't + cover them and no workflow rebuilds them, despite + `docs/example-figure-rubric.md:169-172` declaring "gestalt = production" a + release gate. + +## 6. Example content (109 files read) + +User-visible defects: + +- **high** `src/example_sources/special-methods.md:241,277` and + `async-await.md:117` — four inline markdown links (`[container-protocols] + (/data-model/container-protocols)`, `[callable-objects](…)`, + `[context-managers](…)`, `[async iteration and context] + (/iteration/async-iteration-and-context)`) are doubly broken: + `render_inline()` only converts backticks, so they render as literal + bracket text on the live pages, and the paths use section-style prefixes + that don't exist as routes (examples live at `/examples/`). +- **med** `dicts.md:89,108` — "Mutating a dictionary while iterating it + raises RuntimeError" (stated twice) is overbroad: only size-changing + mutation raises; reassigning an existing key's value during iteration is + legal (verified on 3.13). The code only demonstrates deletion. +- **med** `type-hints.md:136-158` — teaches `typing.TypeAlias` + ("Reach for `TypeAlias` when…") without noting it is deprecated since + 3.12; the sibling `type-aliases.md` correctly presents the PEP 695 `type` + statement as the modern form. The two pages contradict each other on the + current idiom. +- **med** `subprocesses.md`, `threads-and-processes.md`, `networking.md` — + each contains an `:::unsupported` fragment declaring the code "runs in + standard Python only — the … runner does not provide …" immediately + followed by an executable `:::cell` running the identical code with + verified output, and a note claiming the evidence was produced "without + spawning a process" (it was produced by spawning one at verify time). The + sandbox-vs-verifier distinction is real but the wording contradicts + itself. +- **med** `operators.md:143-161` — the short-circuit cell is entirely absent + from the `:::program` block (the page convention everywhere else is + program ⊇ cells), so the editable program can't reproduce that cell. + Same drift, smaller: `numbers.md` (program defines unused `ratio`, cell + prints it), `decorators.md` (program omits the `__doc__` print). +- **med** `unpacking.md:66-68` — the same paragraph appears verbatim twice + in a row in cell 3; all three cell prose blocks are copies of the intro. + Similar copy-paste residue: `hello-world.md` (intro ≡ cell prose ≡ note), + `truthiness.md` (cells 2/3 share prose that doesn't describe cell 3), + `sets.md` note (same guidance twice in one list). +- **med** `operator-overloading.md:27-65` — the note says "Return + `NotImplemented` when an operand type is unsupported," but the + demonstrated `__eq__`/`__add__` do the opposite: `Vector(1, 1) == 5` + raises `AttributeError` (verified). The page teaches the anti-pattern its + own note warns against. +- **med** `positional-only-parameters.md:54-63` — the `clamp=True` demo + outputs the same value as the unclamped call (4·2 never reaches the + clamp), and the page never demonstrates the titular restriction (no cell + shows `scale(value=4)` raising `TypeError`). +- **med** `special-methods.md:212-237` — restores `__hash__` on a mutable + `Bag` whose hash derives from mutable state, without the standard warning; + after `bag[1] = "z"` the object can't be found in a set containing it + (verified). Also claims `sorted()` needs "the rest of the order family" + (only `__lt__` is required), and the example's `__lt__` (by length) is + incoherent with its `__eq__` (by contents). +- **med** `exceptions.md:101-124` — the broad-`except` hazard cell calls + both broken and fixed versions on input `"42"`, where they behave + identically; the claimed hazard is asserted but never demonstrated. +- **med** `constants.md:6` — `doc_path` points at the class + scopes/namespaces tutorial section, which says nothing about the all-caps + convention or `Final`. +- **low** `attribute-access.md:47` — prose says "Calling `object.__setattr__` + avoids recursing through your own hook" on a cell with no `__setattr__` + hook; the technique appears two cells later. +- **low** `bound-and-unbound-methods.md` — the title and `unbound` variable + use Python 2 vocabulary the page's own body correctly refutes, without + noting the term is historical. +- **low** `regular-expressions.md:107` — overstates `re.compile`'s benefit + ("skip the parser on each call… when the same pattern runs in a loop"); + module-level `re` functions cache up to 512 compiled patterns. +- **low** `string-formatting.md:55` — "padded to one decimal place" + conflates `05` (zero-pad to width 5) with `.1f` (precision) in the page's + only explanation of the format-spec mini-language. +- **low** `comprehensions.md:64-74` — prints a bare set despite the site's + own sorted-display convention (established in `sets.md`); the set + comprehension shown is a no-op transformation. +- **low** `match-statements.md` — built on mapping patterns but never warns + they match on a key subset (extra keys are ignored), the standard footgun. +- **low** `keyword-only-arguments.md:15` — "Callers must name those + arguments explicitly" is never evidenced; all cells are happy-path. +- **low** `bytes-and-bytearray.md:4` — `section = "Basics"` while its + manifest neighbors `strings`/`string-formatting` are `"Text"`, splitting + the encoding story across sections. +- **low** `generics-and-typevar.md`, `paramspec.md` — teach only the + pre-3.12 `TypeVar("T")`/`ParamSpec("P")` spellings with no mention of + PEP 695 native syntax, while `type-aliases.md` showcases PEP 695 — an + inconsistent modernity story within the Types section. + +## 7. Figures / marginalia + +- **med** `src/marginalia.py:1932-1935,2257` — the while-loops figcaption + asserts "the back-edge returns to the test each pass" and its score + commentary says "back-edge mechanism", but the attached figure draws three + forward arrows and no back-edge (verified by rendering). Caption and score + describe a figure that doesn't exist — violating the rubric's own + caption-agreement gate. +- **med** `src/marginalia.py` (23 figcaptions, e.g. line 1556) — captions + contain markdown backticks that `render_for_anchor` only HTML-escapes, so + literal backtick characters ship on example pages while adjacent prose + renders backticks as ``. +- **low** `tests/test_marginalia_geometry.py:338-347` — the section-figure + contract checks journeys ⊆ SECTION_FIGURES but not the reverse; deleting a + journey section leaves a dead figure (and its paint function counted as + used) undetected. Currently 21↔21. +- **low** orphan-figure detection counts a figure as used if its quoted name + appears anywhere in `build_prototypes.py` source (a comment would count); + size contract checks width only; collision contracts ignore line-vs-text + overlap. +- **low** `src/marginalia_grammar.py` — dead vocabulary (`open_arrow`, + `dispatch`, `register(between=)` unused by all 123 figures); the viewBox + padding triple is a literal duplicated in the test file with a comment + pointing at module constants that don't exist; `args_kwargs` + (marginalia.py:834-842) hand-computes a divider 3 px off char-center + instead of using the purpose-built `mono_divider`. +- **low** `tests/test_marginalia_geometry.py:15` — header says "all 109 + figures"; there are 123 (109 is the attached-slug count). + +## 8. Documentation drift and internal contradictions + +- **high** `docs/example-source-format-spec.md` — still opens "describes a + *future* source format" with every Phase 0–4 checkbox unchecked, though + the migration shipped (CHANGELOG 2026-05-16; `src/examples.py` is the + 13-line shim). It never documents `:::unsupported` cells, frontmatter + `expected_output` (used by 4 examples), `see_also` (present in all 109 + files), or `scope_first_pass`; mandates "exactly one output fence" per + cell (unsupported cells have none); its parsed model and `verify` target + don't match `example_loader.py`/the Makefile. +- **med** `docs/lessons-learned.md:103-106` — states the production figure + layout backwards: "inline between prose and code is the production layout; + banners between cells is the prototyped richer grammar" and cites a + `.lp-cell.has-figure` rule. Production renders banners between cells + (`src/app.py:755-760`), and `has-figure` appears nowhere in CSS, src, or + tests; two other docs state the current reality. Also "54 tests today + cover 9 contract families" (actual: 24 tests, 11 classes). +- **med** `docs/example-quality-rubric.md:45` vs + `quality-registries.toml:183` — rubric says "every shipped example should + score at least 8.5"; the enforced bar is 9.0-or-waiver + (`check_quality_scores.py:94`). `lessons-learned.md:121` attributes the + 9.0 target to the rubric doc, which never states it. +- **med** `docs/example-figure-rubric.md:150-152` and + `docs/visual-explainer-spec.md:102` — both cite a 440 px banner ceiling; + Contract 8 and `site.css:161` enforce 640 px. The rubric's formula also + omits the 1.6 intrinsic scale. Per the doc ~15 shipped figures would fail; + per code they pass. +- **med** `docs/visual-explainer-spec.md` — multiple internal + contradictions and stale names: its CSS sample uses `width: 100%` while + its own invariant 1 says "never `width: 100%`"; journey figures described + as a ~280-320 px column beside the heading (echoed by + `journey-visualisation-rubric.md:58-61,93-94`) while production renders + them inline, centered, up to 640 px; references + `JOURNEY_SECTION_FIGURES in scripts/build_prototypes.py` (actual: + `SECTION_FIGURES` in `src/marginalia.py`) and `_render_walkthrough_cell` + (actual: `_render_cell`). +- **med** `README.md` — Features/Architecture predate the 2026-05-16 + release: no mention of journeys, the marginalia figure system, `see_also` + links, Turnstile protection, or wide-event observability. A contributor + cannot discover that journeys are Python data in `src/app.py` synced with + `journey_outcomes` in the registry. +- **med** `CONTRIBUTING.md` — says "five scripts enforce the catalog-level + rules" (`make quality-checks` runs nine), and omits the steps a new + example actually requires: a curated score in `EXAMPLE_QUALITY_SCORES` + (`check_quality_scores` fails without it), a figure or a no-figure + rationale, and `see_also` authoring rules. Deployment/secret setup + (Turnstile vars, `PBE_SMOKE_BYPASS_SECRET`) is documented only inside the + turnstile spec. +- **low** `docs/rubric-audit-2026-05-12.md` — the "green baseline" itself + has drifted: claims 24 journey sections including three from the removed + Workers journey (current: 21). +- **low** `docs/example-graph-see-also.md:45-48` — "future improvements" + lists work already shipped (graph audit script, rendered edge labels, 404 + recommendations). +- **low** `docs/turnstile-runner-protection-spec.md:92-107` — documents + modes `off`/`session` only; `main.py:163` also accepts `once` and + `once-per-session`. +- **low** `docs/observability-spec.md:502-512` — cleanup snippet predates + the destroy-only-unused-callbacks refinement in `main.py:379-387`. +- **low** `docs/rubric-saturation.md:3-5` — "109 figures in FIGURES" (now + 123), "12 figures are reused" (now 7); the figure rubric cites these as + live rationale. `example-figure-rubric.md` labels two different contracts + "Contract 5b". +- **low** `README.md:153-159` — the Deploy section omits + `make verify-python-version` and `git diff --check`, which the + Verification section and PR template require. +- **low** `src/app.py:65-70` — `build_dynamic_worker_code`'s docstring + claims "The parent Worker supplies only curated example code"; user-edited + POST code runs there too (that's what Turnstile gates). +- **low** `tests/test_quality_checks.py:1` — docstring says "the four + quality-check scripts" (nine exist). + +## 9. Test gaps + +- **med** the Turnstile clearance HMAC/cookie logic (`_sign_clearance`, + `_clearance_valid`, cookie flags, max-age clamp) and the entire POST + `/examples/{slug}` run flow (verification-required path, missing-site-key + path, verify-then-set-clearance path) have no behavioral tests. The one + related test asserts that `main.py` source *contains substrings*. +- **low** several tests are source-text change-detectors + (`test_worker_entrypoint_uses_fastapi_asgi_bridge`, + `test_dynamic_worker_execution_uses_hash_keyed_get_cache`, + `test_turnstile_verification_is_session_gated_in_worker`), and + `test_every_example_executes_without_error` ends in + `assertIsInstance(stdout.getvalue(), str)` — always true. +- **low** `html_cache_key_url`'s key scheme is never invoked by a test; no + test asserts a figure/banner actually appears in rendered example or + journey HTML (if the anchor scheme drifted, every figure would vanish with + the suite green); bridge `process_request` body/streaming/error paths are + unexercised; negative tests exist only for `check_registry_integrity` + among the quality gates; `run()`'s `env_overrides` parameter in + `test_quality_checks.py` is accepted but unused; temp files are never + removed. + +## 10. Improvement suggestions (beyond fixes) + +Tooling and architecture: + +1. **Pin the whole toolchain to 3.13 in one place.** Route every Makefile + target through `uv run --python 3.13` (or a `PY := uv run --python 3.13 + python` variable) so `make verify` works on any machine, matching the + README's promise. Add `--group workers` to `make dev`. +2. **Move editorial data out of Python literals into the existing TOML + registry pattern.** `JOURNEYS` (285 lines), `SEE_ALSO_EDGE_LABELS`, + `EXAMPLE_QUALITY_SCORES`, `SCORES`, `SECTION_FIGURE_SCORES`, and + `ATTACHMENTS` are content, not code; `quality-registries.toml` + + `tomllib` is already the established home. This alone halves + `marginalia.py` (~55% data literals) and makes journeys reviewable like + examples. +3. **Make the quality layer falsifiable.** Give `score_example_criteria` a + failure condition (e.g. max curated-vs-heuristic delta) or remove it from + `verify`; compare waiver `expires`/`review_after` against today's date; + enforce or delete `journey_average_min` and the `paired_pages` rule; + validate score ranges; replace degenerate confusable tokens with + word-boundary regexes. Add one negative test per gate proving it can + fail. +4. **Consolidate script plumbing.** A small `scripts/_common.py` for the + repeated `ROOT`/`sys.path`/loader/registry boilerplate (~12 scripts) and + one frontmatter parser instead of three (`example_loader`, + `format_examples`, `check_broad_surface_tours`) that can drift. +5. **Close the generated-file loop.** Hooks run `make build` (not just + fingerprints); `.gitattributes` covers `example_sources_data.py`; + `check-generated` covers `public/` fingerprinted copies and the + prototyping pages (or prototypes move out of `public/`); the regenerate + workflow rebuilds after `git pull --rebase`. +6. **Cache the started ASGI app** in the bridge instead of running the + lifespan per request, and consider memoizing rendered pages — the site + is fully static per deploy, so even pre-rendering all pages at build time + (and reserving the Worker for POST runs) is a plausible simplification. + Measure cold-start impact of the 105 import-time program executions under + Pyodide; if the dedicated snapshot doesn't absorb it, ship precomputed + `expected_output` in the embedded data. +7. **Collapse the double routing layer.** FastAPI handlers parse + `{slug}` then delegate to the hand-rolled `route()` which re-parses the + URL; route everything through one of the two. +8. **Schedule the planned golden-fixture cleanup.** The spec conditions + (one stable release cycle, CI coverage) are met; the 11k-line fixture now + requires regeneration on every content edit, making the parity gate + near-tautological. +9. **CI hygiene:** `concurrency` + `permissions` on verify.yml; restrict + `push` trigger to main; pin wrangler consistently; pass workflow inputs + via `env:`; set `PBE_SMOKE_BYPASS_SECRET` in the smoke steps before + enabling Turnstile in production. +10. **Add security headers** (CSP without `unsafe-inline` will require + nonce-ing the two inline scripts or externalizing them; + `X-Content-Type-Options`, `frame-ancestors`, `Referrer-Policy`) and a + POST body-size cap. + +Content (systemic, from the 109-file review): + +11. **Add a program⊇cells build check** — the program block should contain + every executable cell's code (catches the operators/numbers/decorators + drift mechanically); pages whose cells are deliberate standalone + fragments need an explicit marker. +12. **Lint prose duplication** — adjacent duplicate paragraphs and cell + prose identical to the intro (hello-world, truthiness, unpacking, sets). +13. **"Show the failure" rule** — any page whose headline feature is a + restriction (`/`, bare `*`) or whose note warns about a hazard (broad + except, `__eq__` without `NotImplemented`, mutable `__hash__`) should + demonstrate the rejected call or visible breakage, via `:::unsupported` + or try/except; sweep flag demos for no-op inputs (`clamp=True`). +14. **Unify the PEP 695 story** across the Types section: one consistent + "modern vs legacy spelling" framing for type-hints, type-aliases, + generics-and-typevar, and paramspec; retire or caveat deprecated + `TypeAlias`. +15. **Support or forbid inline cross-references**: either extend + `render_inline()` to render markdown links and lint hrefs against real + routes, or enforce that cross-references go through `see_also` only. +16. **Rewrite the runner-constraint framing** on + subprocesses/threads/networking: one honest sentence ("verified under + standard CPython at build time; the in-browser runner cannot spawn + processes/threads/sockets") instead of the current self-contradiction, + and drop the duplicated unsupported/executable cell pairs. +17. **Caption pipeline**: run figcaptions through the same inline renderer + as prose (fixing the 23 backtick captions), and fix or redraw the + while-loops figure/caption mismatch. + +Docs: + +18. **Refresh the three drifted clusters in one pass**: mark the format spec + as implemented and document the four missing constructs; correct + lessons-learned's backwards layout claim and stale numbers; align the + rubric/figure-spec numbers (8.5 vs 9.0, 440 vs 640) with the enforced + values. Cheap insurance: a `make rubric-audit` variant that greps docs + for the enforced constants. +19. **Write the missing contributor path**: a short "adding an example + end-to-end" doc covering markdown file → score entry → figure or + rationale → registries → `make verify`, plus a deployment/secrets + page; update README Features/Architecture for journeys, figures, + Turnstile, and observability. diff --git a/docs/example-figure-rubric.md b/docs/example-figure-rubric.md index 16d5a3b..4794560 100644 --- a/docs/example-figure-rubric.md +++ b/docs/example-figure-rubric.md @@ -119,6 +119,13 @@ the figure can merge. 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.* +- **Captions are complete sentences within length.** Every caption + (attachments and journey section figures) is non-empty, ends with + a period, and stays under 220 characters. Semantic figure/caption + agreement stays a curator judgement — the marginalia gestalt + renders the production caption under every figure so review can + catch a caption asserting something the figure does not draw. + *Contract 5c.* - **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 @@ -132,11 +139,15 @@ the figure can merge. "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.* + 5 — FigureGrammarContract.* - **Font discipline.** Only `FONT_SERIF`, `FONT_MONO`, `FONT_SANS` - may appear as `font-family`. *Contract 5b.* + may appear as `font-family`. *Contract 5.* - **Stroke-weight discipline.** Only `W_HAIRLINE`, `W_STROKE`, - `W_EMPHASIS`, `W_GHOST`. *Contract 5c.* + `W_EMPHASIS`, `W_GHOST`. *Contract 5.* +- **Well-formed XML, escaping enforced.** Every rendered figure must + parse with a real XML parser; the grammar escapes `<`, `>`, and `&` + in drawn text so a label cannot silently produce a browser-dropped + SVG. *Contract 11 — FigureWellFormedXMLContract.* - **Emphasis scarcity, enforced.** At most ONE accent mark (`EMPHASIS`-coloured arrowhead, caret, dot, traced path, or rect stroke) per figure. Was a soft v1 criterion; now hard. The census @@ -147,9 +158,10 @@ the figure can merge. punctuation — every pause point on a ribbon — reads as one system), and a `lanes` traced path plus its terminal dot count as one mark. A gate set plus any focal accent still fails. *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.* +- **Banner-fit, enforced.** Every figure's RENDERED width — + INTRINSIC_SCALE · (Canvas.w + 2 · PAD_X) — must fit the 640px + ceiling of `.cell-banner--1` / `.journey-section-figure` + (`clamp(280px, 65-70vw, 640px)` in site.css). *Contract 8.* - **Twin consistency.** When two figures depict parallel concepts (`kw-only-separator` ↔ `positional-only-separator`, `class-triangle` ↔ `metaclass-triangle`), their metrics must @@ -173,8 +185,8 @@ the figure can merge. ## 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 +A separate 0-1.0 score applied to slugs whose `[[figure_attachments]]` +entries (loaded as `ATTACHMENTS[slug]`) contain 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 diff --git a/docs/example-graph-see-also.md b/docs/example-graph-see-also.md index 9f7b75e..88a3d55 100644 --- a/docs/example-graph-see-also.md +++ b/docs/example-graph-see-also.md @@ -40,9 +40,12 @@ see_also = [ The loader exposes `see_also`, the example renderer displays a compact `See also` section, and tests verify that every linked slug exists and does not point to itself. +## Graph audit + +`scripts/audit_example_graph.py` reports orphan pages, high in-degree pages, and reciprocal links. Its `--check` mode runs in `make quality-checks` and fails the build on broken targets, self-links, out-degree above 4, or orphaned examples. + ## Future improvements -- Add a graph audit script that reports orphan pages, high-degree pages, and missing reciprocal links. - Show short edge labels later, e.g. `contrast`, `prerequisite`, `next depth`. - Use graph data to recommend examples on 404 pages or search results. - Keep the first version minimal until we know the links improve reading rather than distracting from the code. diff --git a/docs/example-quality-rubric.md b/docs/example-quality-rubric.md index 1c0d10a..aba63c1 100644 --- a/docs/example-quality-rubric.md +++ b/docs/example-quality-rubric.md @@ -42,7 +42,7 @@ Quality bands: - **7.0-7.9**: serviceable tutorial material, not yet reference-grade. - **below 7.0**: rewrite before publishing. -Project gate: every shipped example should score at least **8.5**, and the catalog average should exceed sampled Go By Example and Rust By Example pages on craft and understandability. The score is a guide, not a substitute for reading the page. +Project gate (enforced by `scripts/check_quality_scores.py` against `docs/quality-registries.toml`): every shipped example targets **9.0**; anything below target needs a time-boxed waiver, and anything below the **8.5** hard minimum needs an improvement-backlog entry with a concrete next action. The catalog average should exceed sampled Go By Example and Rust By Example pages on craft and understandability. The score is a guide, not a substitute for reading the page. ## Weak-example smells diff --git a/docs/example-source-format-spec.md b/docs/example-source-format-spec.md index 4df7434..0aab875 100644 --- a/docs/example-source-format-spec.md +++ b/docs/example-source-format-spec.md @@ -1,6 +1,8 @@ # Example source format spec -This spec describes a future source format for Python By Example examples. The goal is to make each example independently editable, reviewable, and verifiable while keeping the project strictly a Python language tour. +**Status: implemented.** The migration this spec planned is complete — all examples live as Markdown under `src/example_sources/`, the website serves them through `src/example_loader.py`, and the checklists below are the historical execution record. The format rules and verification model remain the contract for new examples. + +This spec describes the source format for Python By Example examples. The goal is to make each example independently editable, reviewable, and verifiable while keeping the project strictly a Python language tour. Every page should teach a Python language concept, standard syntax pattern, object model behavior, or core standard-library concept that belongs in the learning sequence. @@ -81,68 +83,68 @@ Investigation and verification tasks come first because they decide the implemen ### Phase 0: spikes and investigations -- [ ] Freeze a golden copy of the current catalog before conversion; do not rely on git history as the only golden source. -- [ ] Spike Cloudflare Worker bundling for raw Markdown files under `src/example_sources/` using `pywrangler dev`. -- [ ] Spike production bundling, or document why local Worker bundling failure is enough to require embedded source data. -- [ ] Decide whether to keep the default embedded-data approach or replace it with proven native file bundling. -- [ ] Audit current walkthrough fragments and classify each example as fully executable cells, needs larger cells, or needs rewrite. -- [ ] Rewrite or redesign `match-statements`, `recursion`, `classes`, `properties`, `special-methods`, and `type-hints` so they retain fine-grained literate cells without non-executable fragments. -- [ ] Decide the final cell policy for non-executable explanatory fragments before writing conversion tooling. -- [ ] Confirm the `:::program` plus executable restatement-cell model works for the six problematic examples before converting all examples. -- [ ] Spike parser line-number tracking for frontmatter, cells, Python fences, output fences, and notes. -- [ ] Spike formatter behavior on several hand-edited Markdown examples and confirm it preserves prose/code semantics. -- [ ] Spike `uv run --python $(VERSION)` behavior for `verify-python-version` on locally available Python versions. -- [ ] Identify which examples should be marked `version_sensitive` before future Python runtime migrations. +- [x] Freeze a golden copy of the current catalog before conversion; do not rely on git history as the only golden source. +- [x] Spike Cloudflare Worker bundling for raw Markdown files under `src/example_sources/` using `pywrangler dev`. +- [x] Spike production bundling, or document why local Worker bundling failure is enough to require embedded source data. +- [x] Decide whether to keep the default embedded-data approach or replace it with proven native file bundling. +- [x] Audit current walkthrough fragments and classify each example as fully executable cells, needs larger cells, or needs rewrite. +- [x] Rewrite or redesign `match-statements`, `recursion`, `classes`, `properties`, `special-methods`, and `type-hints` so they retain fine-grained literate cells without non-executable fragments. +- [x] Decide the final cell policy for non-executable explanatory fragments before writing conversion tooling. +- [x] Confirm the `:::program` plus executable restatement-cell model works for the six problematic examples before converting all examples. +- [x] Spike parser line-number tracking for frontmatter, cells, Python fences, output fences, and notes. +- [x] Spike formatter behavior on several hand-edited Markdown examples and confirm it preserves prose/code semantics. +- [x] Spike `uv run --python $(VERSION)` behavior for `verify-python-version` on locally available Python versions. +- [x] Identify which examples should be marked `version_sensitive` before future Python runtime migrations. ### Phase 1: tooling while the live app stays on `src/examples.py` -- [ ] Add fixture Markdown examples for a small subset: `hello-world`, `values`, one multi-cell example, and one difficult class/method example. -- [ ] Add a checked-in golden catalog fixture for parity; keep it until after the cleanup milestone. -- [ ] Add `src/example_loader.py` with TOML frontmatter parsing, explicit cell parsing, line-number metadata, and generated `doc_url`. -- [ ] Add verifier execution using `compile(..., dont_inherit=True)`. -- [ ] Add support for exactly one `:::program` block per example and make the website editor source come from that block. -- [ ] Add verifier checks for wrong output, missing fences, duplicate slugs, stale embedded data, hardcoded docs versions, incompatible `min_python`, and inherited future flags. -- [ ] Add `scripts/format_examples.py` with `--check` mode. -- [ ] Add `scripts/embed_example_sources.py` if the bundling spike keeps the embedded-data approach. -- [ ] Add `scripts/check_example_migration_parity.py` against the checked-in golden catalog fixture and the current `src/examples.py` golden source. -- [ ] Add Make targets: `build`, `embed-examples`, `check-generated`, `verify-examples`, `format-examples`, and `verify-python-version`. -- [ ] Update fingerprinting so Markdown source or embedded source data changes `HTML_CACHE_VERSION`. -- [ ] Keep the website importing the current `src/examples.py` during this phase. +- [x] Add fixture Markdown examples for a small subset: `hello-world`, `values`, one multi-cell example, and one difficult class/method example. +- [x] Add a checked-in golden catalog fixture for parity; keep it until after the cleanup milestone. +- [x] Add `src/example_loader.py` with TOML frontmatter parsing, explicit cell parsing, line-number metadata, and generated `doc_url`. +- [x] Add verifier execution using `compile(..., dont_inherit=True)`. +- [x] Add support for exactly one `:::program` block per example and make the website editor source come from that block. +- [x] Add verifier checks for wrong output, missing fences, duplicate slugs, stale embedded data, hardcoded docs versions, incompatible `min_python`, and inherited future flags. +- [x] Add `scripts/format_examples.py` with `--check` mode. +- [x] Add `scripts/embed_example_sources.py` if the bundling spike keeps the embedded-data approach. +- [x] Add `scripts/check_example_migration_parity.py` against the checked-in golden catalog fixture and the current `src/examples.py` golden source. +- [x] Add Make targets: `build`, `embed-examples`, `check-generated`, `verify-examples`, `format-examples`, and `verify-python-version`. +- [x] Update fingerprinting so Markdown source or embedded source data changes `HTML_CACHE_VERSION`. +- [x] Keep the website importing the current `src/examples.py` during this phase. ### Phase 2: mechanical conversion and parity -- [ ] Mechanically convert all current examples to Markdown without rewriting teaching content. -- [ ] Run the formatter and commit canonical Markdown shape. -- [ ] Run verifier across every Markdown example. -- [ ] Run golden parity and require the program block to match old `code` byte-for-byte. -- [ ] Run golden parity and classify walkthrough differences as identical or teaching-structure. -- [ ] Fix every semantic difference; do not allowlist semantic differences for the app switch. -- [ ] Fix every teaching-structure difference, including collapsed/lost cells. -- [ ] Fix examples whose cells are not executable according to the final cell policy. -- [ ] Verify Markdown-only edits change generated embedded data and `HTML_CACHE_VERSION`. -- [ ] Verify stale generated files fail `make check-generated`. +- [x] Mechanically convert all current examples to Markdown without rewriting teaching content. +- [x] Run the formatter and commit canonical Markdown shape. +- [x] Run verifier across every Markdown example. +- [x] Run golden parity and require the program block to match old `code` byte-for-byte. +- [x] Run golden parity and classify walkthrough differences as identical or teaching-structure. +- [x] Fix every semantic difference; do not allowlist semantic differences for the app switch. +- [x] Fix every teaching-structure difference, including collapsed/lost cells. +- [x] Fix examples whose cells are not executable according to the final cell policy. +- [x] Verify Markdown-only edits change generated embedded data and `HTML_CACHE_VERSION`. +- [x] Verify stale generated files fail `make check-generated`. ### Phase 3: app switch -- [ ] Block this phase unless golden parity reports 100% parity for metadata, code behavior, output, notes, walkthrough prose/source, rendered cell count, and cell source/output structure. -- [ ] Switch `src/examples.py` to a thin compatibility layer over the Markdown loader. -- [ ] Verify existing app tests still pass without weakening assertions. -- [ ] Run `make build`, `make verify-examples`, `make test`, `make seo-cache-lint`, `make browser-layout-test`, `make lint`, and `git diff --check`. -- [ ] Start local Worker with `pywrangler dev` and verify it does not fail on missing Markdown files. -- [ ] Verify representative GET pages render from the Markdown loader. -- [ ] Verify POST execution still runs edited code through Dynamic Workers. -- [ ] Verify browser layout for Shiki, CodeMirror, literate cells, and output panels. +- [x] Block this phase unless golden parity reports 100% parity for metadata, code behavior, output, notes, walkthrough prose/source, rendered cell count, and cell source/output structure. +- [x] Switch `src/examples.py` to a thin compatibility layer over the Markdown loader. +- [x] Verify existing app tests still pass without weakening assertions. +- [x] Run `make build`, `make verify-examples`, `make test`, `make seo-cache-lint`, `make browser-layout-test`, `make lint`, and `git diff --check`. +- [x] Start local Worker with `pywrangler dev` and verify it does not fail on missing Markdown files. +- [x] Verify representative GET pages render from the Markdown loader. +- [x] Verify POST execution still runs edited code through Dynamic Workers. +- [x] Verify browser layout for Shiki, CodeMirror, literate cells, and output panels. ### Phase 4: deploy and cleanup -- [ ] Do not deploy Markdown-backed examples unless Phase 3 has 100% parity and no collapsed/lost literate cells. -- [ ] Deploy only after local Worker startup and all verification pass. -- [ ] Smoke-test `https://www.pythonbyexample.dev`. -- [ ] Smoke-test `https://pythonbyexample.adewale-883.workers.dev`. -- [ ] Verify production asset caching and HTML cache-busting after an example-only edit. -- [ ] Keep the frozen golden fixture through at least one stable production release cycle and rollback window. -- [ ] Remove temporary golden parity scaffolding only in a dedicated cleanup PR after CI and production confidence are established. -- [ ] Update README contributor instructions to point contributors at Markdown examples, `make build`, and `make verify-examples`. +- [x] Do not deploy Markdown-backed examples unless Phase 3 has 100% parity and no collapsed/lost literate cells. +- [x] Deploy only after local Worker startup and all verification pass. +- [x] Smoke-test `https://www.pythonbyexample.dev`. +- [x] Smoke-test `https://pythonbyexample.adewale-883.workers.dev`. +- [x] Verify production asset caching and HTML cache-busting after an example-only edit. +- [x] Keep the frozen golden fixture through at least one stable production release cycle and rollback window. +- [x] Remove temporary golden parity scaffolding only in a dedicated cleanup PR after CI and production confidence are established. +- [x] Update README contributor instructions to point contributors at Markdown examples, `make build`, and `make verify-examples`. ## Implementation milestones @@ -200,7 +202,21 @@ Each example file contains: 2. Introductory prose. 3. Exactly one `:::program` block containing the full editable program. 4. One or more explicit `:::cell` blocks for the literate walkthrough. -5. Optional `:::note` blocks. +5. Optional `:::unsupported` blocks: a prose-plus-code teaching unit whose code the + runner environment cannot execute (subprocesses, sockets, threads). The fence is + displayed but not executed; the prose must say plainly that Run fails in the + sandbox and where the shown evidence comes from. +6. Optional `:::note` blocks. + +Frontmatter fields beyond the navigation basics: + +- `expected_output` overrides the generated program output for + environment-shaped pages whose real output the build environment cannot + reproduce deterministically; the verifier still executes the program. +- `standalone_cells = true` opts a page out of the program-covers-cells gate + when it deliberately demonstrates code the editable program does not + contain; the opt-out is reported on every run so it stays a visible + editorial decision. TOML is preferred over YAML so the loader can use Python's standard-library `tomllib` locally and in the Worker bundle. @@ -531,17 +547,16 @@ The command should fail if: `verify-python-version` is only meaningful when `uv` can run the requested Python version. Until Cloudflare exposes the same runtime locally, the migration also requires a Worker smoke test for representative examples and at least one POST execution through Dynamic Workers. -## Golden fixture policy +## Golden fixture cleanup policy -`tests/fixtures/golden_examples.py` is temporary but intentional migration safety infrastructure. +`tests/fixtures/golden_examples.py` began as migration safety infrastructure: a frozen comparison point while the app switched from Python dictionaries to Markdown source files. That rollback window has passed, so the fixture and its refresh/parity scripts are removed rather than refreshed forever. -Rules: +Current rules: -- Keep the golden fixture checked in while Markdown remains new. -- Do not update the golden fixture in the same change as ordinary content edits. -- If changing intended teaching structure, update Markdown first and let parity fail; then update the golden fixture in a separate explicit fixture-refresh commit after review. -- Remove the golden fixture only after at least one stable production release cycle, CI coverage for `make verify` and `make check-generated`, and an explicit cleanup PR. -- Until cleanup, `scripts/check_example_migration_parity.py` must be a required verification command. +- Do not add a new checked-in catalog snapshot for ordinary content edits. +- Review intentional teaching changes through the Markdown diff, generated-source diff, `verify_examples.py`, content gates, browser layout check, and rubric audit. +- Parser changes must be justified by focused loader/verifier tests, not by refreshing a broad snapshot until it agrees with the new behavior. +- If a future source-format migration needs parity again, add a temporary fixture and delete it in the same migration plan once a stable deployment and rollback window have passed. ## CI policy @@ -549,7 +564,6 @@ GitHub Actions must run the same checks expected locally: ```bash make verify -scripts/check_example_migration_parity.py scripts/format_examples.py --check make verify-python-version VERSION=3.13 git diff --check @@ -559,7 +573,7 @@ CI must start `pywrangler dev --port 9696` before `make verify` so `browser-layo ## Contributor documentation policy -The README must describe Markdown example editing, `:::program`, `:::cell`, generated embedded source data, `make build`, `make verify-examples`, and the parity check. Contributors should not need to edit `src/examples.py` or `src/example_sources_data.py` by hand. +The README must describe Markdown example editing, `:::program`, `:::cell`, generated embedded source data, `make build`, `make verify-examples`, and the TOML editorial registries. Contributors should not need to edit `src/examples.py` or `src/example_sources_data.py` by hand. ## Worker bundling policy @@ -582,28 +596,27 @@ The attempted migration failed at Worker startup because Markdown files were not Native Markdown bundling remains unproven and is not on the production path. The accepted solution is embedded source data generated by `scripts/embed_example_sources.py`. A future native bundling change must be isolated as a spike and prove local dev, production deploy, imports, cache fingerprinting, and failure behavior before replacing embedded data. -## Golden parity script +## Historical golden parity script -Add a dedicated migration script before switching the app: +During the app switch, add a dedicated migration script before switching the app: ```text scripts/check_example_migration_parity.py ``` -Required behavior: +Required migration-time behavior: -- Import the old `src/examples.py` catalog and a checked-in frozen golden fixture as the golden sources. -- Load the Markdown catalog through `src/example_loader.py`. +- Load the catalog through `src/example_loader.py` and the checked-in structural snapshot. - Compare example count and order. -- Compare `slug`, `title`, `section`, `summary`, generated `doc_url`, `expected_output`, notes, and walkthrough prose/source. -- Render old and new walkthrough cells and compare cell count, prose grouping, source, output, and order. -- Execute old and new full code and compare stdout. +- Compare `slug`, `title`, `section`, `summary`, `doc_path`, generated `doc_url`, `explanation`, `notes`, `see_also`, full `code`, and `expected_output`. +- Compare the full cell sequence — prose, code, output, and kind — for every example. +- On failure, say whether the fix is a fixture refresh (intentional content edit) or a loader regression (anything else). - Classify full-code differences as `identical`, `whitespace-only`, or `semantic`. - Classify walkthrough differences as `identical` or `teaching-structure`. - Fail on every semantic or teaching-structure difference. No allowlist is permitted for the app switch. - Print a short table of differences for review. -This script is temporary migration scaffolding. It can be removed only after the old catalog is deleted, one production deployment succeeds, and the rollback window has passed. +This script was temporary migration scaffolding. The old catalog was deleted, production deployment succeeded, and the rollback window passed; the script and fixture have now been removed. ## Fourteen prerequisite verification gates diff --git a/docs/journey-visualisation-rubric.md b/docs/journey-visualisation-rubric.md index df42a06..18f8258 100644 --- a/docs/journey-visualisation-rubric.md +++ b/docs/journey-visualisation-rubric.md @@ -1,6 +1,6 @@ # Journey visualisation rubric -This rubric scores the figure beside each journey section heading. +This rubric scores the figure rendered between each journey section heading and its example list. The example rubric (docs/example-quality-rubric.md) covers individual lesson pages; this one covers the conceptual figures that introduce each journey section. @@ -55,9 +55,11 @@ Score each section figure on a 10-point scale. 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. +10. **Layout fit (0-0.5)** — renders comfortably in the centered + `.journey-section-figure` block (`clamp(280px, 70vw, 640px)`). + Text inside the SVG stays readable across that range; the figure + does not overflow (rendered width ≤ 640px, enforced by + Contract 8). ## Topic gates @@ -86,12 +88,12 @@ Score each section figure on a 10-point scale. - **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. +- **Section summary aligns with caption.** The journey section summary in + `docs/quality-registries.toml` agrees with what the figure caption asserts. + Disagreement means one or the other is wrong. +- **Renders within the centered figure block.** The figure obeys the + width the layout gives it (`clamp(280px, 70vw, 640px)`); design at a + viewBox whose rendered width fits the shared 640px ceiling. - **Uses only the four palette constants.** `INK`, `INK_SOFT`, `EMPHASIS`, `SOFT_FILL`. Anything else is grounds for redesign. diff --git a/docs/lessons-learned.md b/docs/lessons-learned.md index 9bcac53..2cee8ae 100644 --- a/docs/lessons-learned.md +++ b/docs/lessons-learned.md @@ -39,7 +39,7 @@ This document records project lessons that should guide future changes to Python - Keep the complete editable program visible because it is the thing that actually runs. - When storing examples as Markdown, keep the full editable program in `:::program` and teaching fragments in separate `:::cell` blocks. Do not concatenate cells to recreate the editor source. - Fine-grained cells can restate definitions to stay executable. This is better than collapsing class, property, recursion, match, or type-hint examples into one large cell. -- Preserve a frozen golden catalog while migrating source formats. Full stdout parity is not enough; rendered teaching-cell structure must also match. +- Preserve a frozen golden catalog while migrating source formats. Full stdout parity is not enough; rendered teaching-cell structure must also match. After a stable Markdown-backed release and rollback window, remove the migration fixture instead of refreshing it forever; ongoing safety belongs in verifiers, content gates, and browser/runtime checks. - Beware broad-surface titles that quietly teach only one narrow slice. Pages named `Testing`, `Packages`, `Regular Expressions`, `Type Hints`, `Async Await`, or `Special Methods` must either cover the forms a reader reasonably expects or explicitly frame themselves as a first pass and link to focused neighbors. - Do not let an important syntax form live only in a separate page if another page title strongly implies it. An umbrella `Operators` page should at least point to and lightly show `:=`, even though `Assignment Expressions` remains the focused lesson. - Journey order should follow prerequisite thinking, not catalog order. Put booleans before truthiness and conditionals, scope before closures, bytes before networking, and environment boundaries before subprocess/thread boundaries. @@ -74,7 +74,6 @@ This document records project lessons that should guide future changes to Python ```bash make verify -scripts/check_example_migration_parity.py scripts/format_examples.py --check make verify-python-version VERSION=3.13 git diff --check @@ -99,7 +98,7 @@ git diff --check - **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 material improves when the constraint is drawn as a boundary, not a caveat.** Runtime-boundary teaching works best when it shows the standard Python contract, the site-specific runner boundary, and the portable evidence that preserves the lesson. If a constraint-shaped section cannot be reframed this way, then use the no-figure rationale registry instead of shipping a weak mechanism picture. -- **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. +- **Authoring stays on the contributor; figures stay on the curator.** Example markdown does not include figure references. `src/marginalia.py` holds executable `FIGURES` paint functions; `docs/quality-registries.toml` holds attachments, captions, scores, journeys, and edge labels. Curating figures is a registry edit plus paint code only when the figure is new. - **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. @@ -116,7 +115,7 @@ git diff --check - **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. +- **Journey pages render section figures inline.** `SECTION_FIGURES` is loaded from `docs/quality-registries.toml` (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. - **Quality debt must be tracked, not normalized away.** `docs/example-quality-rubric.md` sets a 9.0 target and `scripts/check_quality_scores.py` enforces the score registry: pages below the hard minimum need a concrete improvement backlog entry, stale backlog entries fail once a page clears the gate, and Hello World is the only standing waiver because first examples are traditionally tiny. A score below target is allowed only when the remaining work is named. - **No-figure decisions need a registry.** Some examples should not have figures, but that cannot be an invisible omission. `scripts/check_no_figure_rationales.py` validates `no_figure_rationales` so future constraint-shaped pages can opt out explicitly instead of shipping weak diagrams. diff --git a/docs/quality-registries.toml b/docs/quality-registries.toml index 3ee448a..ee80aed 100644 --- a/docs/quality-registries.toml +++ b/docs/quality-registries.toml @@ -1,8 +1,8 @@ -# Quality registries. +# Quality and editorial registries. # -# Source of truth for the rubric checks in `scripts/check_*.py`. -# Each entry pins a contrast or footgun to a single owning page, so the -# catalog has exactly one home for the lesson and verifiers can prove it. +# Source of truth for rubric checks, journey structure, edge labels, +# figure attachments/captions, and curated scores. Executable paint +# functions stay in `src/marginalia.py`; mutable editorial data lives here. [[confusable_pairs]] name = "__str__ vs __repr__" @@ -179,6 +179,12 @@ pairs = [ ["comprehensions", "comprehension-patterns"], ] +[paired_pages.cell_tokens] +"iterators|iterating-over-iterables" = ["iterable", "iterator"] +"iterators|iterator-vs-iterable" = ["iterable", "iterator", "iter("] +"generators|generator-expressions" = ["generator", "expression"] +"comprehensions|comprehension-patterns" = ["nested loops", "explicit loop"] + [quality_gates] example_target = 9.0 example_hard_min = 8.5 @@ -188,7 +194,7 @@ journey_average_min = 8.8 [quality_waivers.hello-world] accepted_min = 7.0 reason = "Traditional first example is intentionally tiny: program -> output is the goal, not a full concept tour." -expires = "never" +expires = "2026-12-01" [no_figure_rationales] # Slugs may be added here only when the page is constraint-shaped, @@ -571,3 +577,2712 @@ progressive_walkthrough = 0.75 representative_coverage = 0.75 practical_usefulness = 0.75 editorial_progression = 1.0 + +# Editorial registries loaded by the site at runtime. +# Keep prose, captions, scores, journey ordering, and edge labels here; +# keep executable paint functions in src/marginalia.py. + +[[journeys]] +slug = "runtime" +title = "Runtime" +summary = "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." + +[[journeys.sections]] +title = "Start with executable evidence." +summary = "Learners first need to see that every page is a runnable program with visible output." + +[[journeys.sections.items]] +kind = "example" +value = "hello-world" +description = "start with a complete program and its output" + +[[journeys.sections.items]] +kind = "example" +value = "values" +description = "see that Python programs manipulate runtime objects" + +[[journeys.sections.items]] +kind = "example" +value = "literals" +description = "write small values directly in source code" + +[[journeys.sections.items]] +kind = "example" +value = "variables" +description = "understand that names bind to objects rather than storing values themselves" + +[[journeys.sections.items]] +kind = "example" +value = "constants" +description = "learn the convention Python uses for values that should not change" + +[[journeys.sections]] +title = "Separate value, identity, and absence." +summary = "This section prevents early confusion about equality, object identity, missing values, and truth tests." + +[[journeys.sections.items]] +kind = "example" +value = "none" +description = "represent expected absence with a singleton object" + +[[journeys.sections.items]] +kind = "example" +value = "booleans" +description = "combine facts with boolean operators" + +[[journeys.sections.items]] +kind = "example" +value = "truthiness" +description = "predict how objects behave in boolean contexts" + +[[journeys.sections.items]] +kind = "example" +value = "equality-and-identity" +description = "distinguish value equality from object identity" + +[[journeys.sections.items]] +kind = "example" +value = "mutability" +description = "predict when operations change an object in place" + +[[journeys.sections.items]] +kind = "example" +value = "object-lifecycle" +description = "explain references, garbage collection, and why identity can outlive a single name" + +[[journeys.sections]] +title = "Read expressions as object operations." +summary = "This section connects operators, text, and formatting to Python's data model." + +[[journeys.sections.items]] +kind = "example" +value = "numbers" +description = "use numeric objects and arithmetic operators" + +[[journeys.sections.items]] +kind = "example" +value = "operators" +description = "combine, compare, and test values with expression syntax" + +[[journeys.sections.items]] +kind = "example" +value = "strings" +description = "treat text as Unicode rather than raw bytes" + +[[journeys.sections.items]] +kind = "example" +value = "string-formatting" +description = "turn objects into readable text at output boundaries" + +[[journeys.sections.items]] +kind = "example" +value = "bytes-and-bytearray" +description = "contrast text with binary data and explicit decoding" + +[[journeys]] +slug = "control-flow" +title = "Control Flow" +summary = "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." + +[[journeys.sections]] +title = "Choose between paths." +summary = "Start with ordinary branching and boolean predicates before reaching for more compact forms." + +[[journeys.sections.items]] +kind = "example" +value = "booleans" +description = "combine facts into readable conditions" + +[[journeys.sections.items]] +kind = "example" +value = "truthiness" +description = "use object truth values without hiding intent" + +[[journeys.sections.items]] +kind = "example" +value = "operators" +description = "build comparisons and boolean expressions for conditions" + +[[journeys.sections.items]] +kind = "example" +value = "conditionals" +description = "choose between branches with clear predicates" + +[[journeys.sections]] +title = "Name and shape decisions." +summary = "Some branches become clearer when the code names an intermediate value or dispatches on data shape." + +[[journeys.sections.items]] +kind = "example" +value = "assignment-expressions" +description = "name an intermediate value inside a condition when it improves clarity" + +[[journeys.sections.items]] +kind = "example" +value = "match-statements" +description = "dispatch on the shape of data rather than only on boolean tests" + +[[journeys.sections.items]] +kind = "example" +value = "advanced-match-patterns" +description = "combine destructuring, alternatives, and guards in pattern matching" + +[[journeys.sections]] +title = "Stop as soon as the answer is known." +summary = "Early exits make the successful path easier to read by moving exceptional or completed cases out of the way." + +[[journeys.sections.items]] +kind = "example" +value = "guard-clauses" +description = "show how early returns reduce nested conditional code" + +[[journeys.sections.items]] +kind = "example" +value = "assertions" +description = "state assumptions that should fail loudly while developing" + +[[journeys.sections.items]] +kind = "example" +value = "exceptions" +description = "leave the current path when ordinary return values are not enough" + +[[journeys]] +slug = "iteration" +title = "Iteration" +summary = "This journey follows repeated work from ordinary loops to the iterator protocol: consume values, stop deliberately, and produce lazy streams only when they help." + +[[journeys.sections]] +title = "Choose the right loop shape." +summary = "Loops differ by what they consume, when they stop, and whether completion itself carries meaning." + +[[journeys.sections.items]] +kind = "example" +value = "for-loops" +description = "consume values from an iterable" + +[[journeys.sections.items]] +kind = "example" +value = "while-loops" +description = "repeat while a condition must be rechecked" + +[[journeys.sections.items]] +kind = "example" +value = "break-and-continue" +description = "interrupt or skip loop work intentionally" + +[[journeys.sections.items]] +kind = "example" +value = "loop-else" +description = "attach completion logic to loops that did not break" + +[[journeys.sections.items]] +kind = "example" +value = "sentinel-iteration" +description = "show `iter(callable, sentinel)` for repeated reads until a marker appears" + +[[journeys.sections]] +title = "See the protocol behind `for`." +summary = "The important mental shift is that loops consume producers through a protocol rather than special-casing lists." + +[[journeys.sections.items]] +kind = "example" +value = "iterating-over-iterables" +description = "separate value producers from value consumers" + +[[journeys.sections.items]] +kind = "example" +value = "iterators" +description = "use `iter()` and `next()` to expose the protocol behind `for`" + +[[journeys.sections.items]] +kind = "example" +value = "generators" +description = "write functions that produce values lazily" + +[[journeys.sections]] +title = "Compose lazy value streams." +summary = "Iterator pipelines are useful when code can transform values one at a time instead of materializing every intermediate result." + +[[journeys.sections.items]] +kind = "example" +value = "generator-expressions" +description = "create lazy one-pass streams with expression syntax" + +[[journeys.sections.items]] +kind = "example" +value = "itertools" +description = "compose iterator streams without materializing every value" + +[[journeys.sections.items]] +kind = "example" +value = "yield-from" +description = "delegate part of a generator to another iterable" + +[[journeys]] +slug = "shapes" +title = "Shapes" +summary = "This journey teaches the core Python habit of choosing a data shape, transforming it directly, and making the result visible." + +[[journeys.sections]] +title = "Pick the container that matches the question." +summary = "Lists, tuples, dictionaries, and sets answer different questions about order, position, lookup, and uniqueness." + +[[journeys.sections.items]] +kind = "example" +value = "lists" +description = "store ordered mutable data" + +[[journeys.sections.items]] +kind = "example" +value = "tuples" +description = "group fixed-position values" + +[[journeys.sections.items]] +kind = "example" +value = "dicts" +description = "look up values by key" + +[[journeys.sections.items]] +kind = "example" +value = "sets" +description = "model uniqueness and membership" + +[[journeys.sections.items]] +kind = "example" +value = "collections-module" +description = "show `deque`, `Counter`, `defaultdict`, and `namedtuple` as specialized shapes" + +[[journeys.sections]] +title = "Move between shapes deliberately." +summary = "Most everyday Python code is data reshaping, so learners need the idioms for selecting, unpacking, and rebuilding values." + +[[journeys.sections.items]] +kind = "example" +value = "unpacking" +description = "bind names from structured values" + +[[journeys.sections.items]] +kind = "example" +value = "slices" +description = "select ranges from sequences" + +[[journeys.sections.items]] +kind = "example" +value = "comprehensions" +description = "build concrete collections from compact loops" + +[[journeys.sections.items]] +kind = "example" +value = "comprehension-patterns" +description = "compose filters and nested transformations" + +[[journeys.sections.items]] +kind = "example" +value = "sorting" +description = "order records with key functions" + +[[journeys.sections.items]] +kind = "example" +value = "copying-collections" +description = "contrast shallow copies, deep copies, and shared nested data" + +[[journeys.sections]] +title = "Cross text and data boundaries." +summary = "Programs often receive text and produce structured data, so parsing and serialization belong in the data journey." + +[[journeys.sections.items]] +kind = "example" +value = "number-parsing" +description = "turn text into numbers safely" + +[[journeys.sections.items]] +kind = "example" +value = "json" +description = "move structured data across a text boundary" + +[[journeys.sections.items]] +kind = "example" +value = "regular-expressions" +description = "extract structure from text patterns" + +[[journeys.sections.items]] +kind = "example" +value = "datetime" +description = "represent dates, times, and durations as typed values" + +[[journeys.sections.items]] +kind = "example" +value = "csv-data" +description = "show row-shaped text data and dictionary records" + +[[journeys]] +slug = "interfaces" +title = "Interfaces" +summary = "This journey shows how Python grows from simple functions to callable APIs, object interfaces, protocols, and metaclasses." + +[[journeys.sections]] +title = "Start with functions as named behavior." +summary = "Functions are the first abstraction boundary because they name behavior and control how callers provide information." + +[[journeys.sections.items]] +kind = "example" +value = "functions" +description = "package behavior behind a name" + +[[journeys.sections.items]] +kind = "example" +value = "keyword-only-arguments" +description = "make important call-site choices explicit" + +[[journeys.sections.items]] +kind = "example" +value = "positional-only-parameters" +description = "hide parameter names that should remain implementation details" + +[[journeys.sections.items]] +kind = "example" +value = "args-and-kwargs" +description = "accept flexible call shapes when forwarding or adapting APIs" + +[[journeys.sections.items]] +kind = "example" +value = "multiple-return-values" +description = "return multiple related values as a tuple" + +[[journeys.sections]] +title = "Use functions as values." +summary = "Python functions can capture state, be passed around, and wrap other functions." + +[[journeys.sections.items]] +kind = "example" +value = "scope-global-nonlocal" +description = "control where assignment happens" + +[[journeys.sections.items]] +kind = "example" +value = "closures" +description = "capture state in nested functions" + +[[journeys.sections.items]] +kind = "example" +value = "recursion" +description = "solve self-similar problems with a base case" + +[[journeys.sections.items]] +kind = "example" +value = "lambdas" +description = "write small unnamed functions for expression positions" + +[[journeys.sections.items]] +kind = "example" +value = "decorators" +description = "wrap behavior without changing call sites" + +[[journeys.sections.items]] +kind = "example" +value = "partial-functions" +description = "show how to pre-fill arguments with `functools.partial`" + +[[journeys.sections]] +title = "Bundle behavior with state." +summary = "Classes become useful when data and behavior need to move together behind a stable interface." + +[[journeys.sections.items]] +kind = "example" +value = "classes" +description = "bundle state and behavior into a new object type" + +[[journeys.sections.items]] +kind = "example" +value = "inheritance-and-super" +description = "reuse and extend behavior through parent classes" + +[[journeys.sections.items]] +kind = "example" +value = "dataclasses" +description = "generate common methods for data containers" + +[[journeys.sections.items]] +kind = "example" +value = "properties" +description = "keep attribute syntax while adding computation or validation" + +[[journeys.sections.items]] +kind = "example" +value = "special-methods" +description = "connect objects to Python syntax and built-ins" + +[[journeys.sections.items]] +kind = "example" +value = "truth-and-size" +description = "make objects work with truth tests and `len()`" + +[[journeys.sections.items]] +kind = "example" +value = "container-protocols" +description = "support membership, lookup, and assignment syntax" + +[[journeys.sections.items]] +kind = "example" +value = "callable-objects" +description = "make stateful instances callable like functions" + +[[journeys.sections.items]] +kind = "example" +value = "operator-overloading" +description = "define operators only when the operation is unsurprising" + +[[journeys.sections.items]] +kind = "example" +value = "attribute-access" +description = "customize fallback lookup and assignment carefully" + +[[journeys.sections.items]] +kind = "example" +value = "descriptors" +description = "explain the protocol behind methods, properties, and managed attributes" + +[[journeys.sections.items]] +kind = "example" +value = "metaclasses" +description = "customize class creation when ordinary class tools are not enough" + +[[journeys]] +slug = "types" +title = "Types" +summary = "This journey maps Python's runtime object model to optional static annotations so learners know what types can and cannot promise." + +[[journeys.sections]] +title = "Keep runtime and static analysis separate." +summary = "The first lesson is that annotations describe expectations for tools while ordinary Python objects still run the program." + +[[journeys.sections.items]] +kind = "example" +value = "type-hints" +description = "document expected types and feed type checkers" + +[[journeys.sections.items]] +kind = "example" +value = "protocols" +description = "describe required behavior by structural shape" + +[[journeys.sections.items]] +kind = "example" +value = "enums" +description = "name a fixed set of symbolic values" + +[[journeys.sections.items]] +kind = "example" +value = "runtime-type-checks" +description = "show `type()`, `isinstance()`, and `issubclass()` without turning Python into Java" + +[[journeys.sections]] +title = "Describe realistic data shapes." +summary = "Typed Python becomes useful when annotations explain optional values, unions, callables, and JSON-like records." + +[[journeys.sections.items]] +kind = "example" +value = "union-and-optional-types" +description = "show `X | Y` and `None`-aware APIs" + +[[journeys.sections.items]] +kind = "example" +value = "type-aliases" +description = "name complex types with `type` statements or aliases" + +[[journeys.sections.items]] +kind = "example" +value = "typed-dicts" +description = "type dictionary records that come from JSON" + +[[journeys.sections.items]] +kind = "example" +value = "literal-and-final" +description = "express constrained values and names that should not be rebound" + +[[journeys.sections.items]] +kind = "example" +value = "callable-types" +description = "type functions that are passed as arguments" + +[[journeys.sections]] +title = "Scale annotations for reusable libraries." +summary = "Advanced typing exists to preserve information across reusable functions, containers, and decorators." + +[[journeys.sections.items]] +kind = "example" +value = "generics-and-typevar" +description = "write reusable typed containers and functions" + +[[journeys.sections.items]] +kind = "example" +value = "paramspec" +description = "preserve callable signatures through decorators" + +[[journeys.sections.items]] +kind = "example" +value = "overloads" +description = "describe APIs whose return type depends on the input shape" + +[[journeys.sections.items]] +kind = "example" +value = "casts-and-any" +description = "show escape hatches and their tradeoffs" + +[[journeys.sections.items]] +kind = "example" +value = "newtype" +description = "create distinct static identities for runtime-compatible values" + +[[journeys]] +slug = "reliability" +title = "Reliability" +summary = "This journey follows the boundaries where programs fail, clean up, split into modules, communicate with the outside world, and run concurrent work." + +[[journeys.sections]] +title = "Make failure explicit." +summary = "Robust Python code distinguishes expected absence, broken assumptions, recoverable errors, and domain-specific failures." + +[[journeys.sections.items]] +kind = "example" +value = "exceptions" +description = "signal and recover from errors" + +[[journeys.sections.items]] +kind = "example" +value = "assertions" +description = "state internal assumptions" + +[[journeys.sections.items]] +kind = "example" +value = "exception-chaining" +description = "preserve the cause while translating an error" + +[[journeys.sections.items]] +kind = "example" +value = "exception-groups" +description = "handle multiple failures together" + +[[journeys.sections.items]] +kind = "example" +value = "custom-exceptions" +description = "name failures in the language of the problem domain" + +[[journeys.sections.items]] +kind = "example" +value = "warnings" +description = "signal soft problems and deprecations" + +[[journeys.sections]] +title = "Control resource and module boundaries." +summary = "Cleanup, deletion, imports, and modules define where responsibilities begin and end." + +[[journeys.sections.items]] +kind = "example" +value = "context-managers" +description = "pair setup with reliable cleanup" + +[[journeys.sections.items]] +kind = "example" +value = "delete-statements" +description = "remove names, attributes, and items intentionally" + +[[journeys.sections.items]] +kind = "example" +value = "modules" +description = "split code into importable files" + +[[journeys.sections.items]] +kind = "example" +value = "import-aliases" +description = "make imported names clear at use sites" + +[[journeys.sections.items]] +kind = "example" +value = "packages" +description = "show package directories, `__init__.py`, and public module boundaries" + +[[journeys.sections.items]] +kind = "example" +value = "virtual-environments" +description = "isolate dependencies for a project" + +[[journeys.sections]] +title = "Handle operations that outlive one expression." +summary = "I/O, testing, logging, subprocesses, and concurrency require different control boundaries from ordinary expressions." + +[[journeys.sections.items]] +kind = "example" +value = "async-await" +description = "await concurrent I/O-shaped work" + +[[journeys.sections.items]] +kind = "example" +value = "async-iteration-and-context" +description = "consume async streams and cleanup protocols" + +[[journeys.sections.items]] +kind = "example" +value = "logging" +description = "record operational events without using `print()`" + +[[journeys.sections.items]] +kind = "example" +value = "testing" +description = "write deterministic tests with `unittest` or `pytest`" + +[[journeys.sections.items]] +kind = "example" +value = "subprocesses" +description = "run external commands safely" + +[[journeys.sections.items]] +kind = "example" +value = "threads-and-processes" +description = "contrast concurrency choices beyond `asyncio`" + +[[journeys.sections.items]] +kind = "example" +value = "networking" +description = "make HTTP or socket boundaries explicit" + +# Explicit see_also edge labels; default labels are inferred in src/app.py. +[[see_also_edge_labels]] +source = "break-and-continue" +target = "loop-else" +label = "contrast" + +[[see_also_edge_labels]] +source = "assignment-expressions" +target = "conditionals" +label = "contrast" + +[[see_also_edge_labels]] +source = "yield-from" +target = "generators" +label = "prerequisite" + +[[see_also_edge_labels]] +source = "async-iteration-and-context" +target = "async-await" +label = "prerequisite" + +[[see_also_edge_labels]] +source = "delete-statements" +target = "mutability" +label = "shared mechanism" + +[[see_also_edge_labels]] +source = "positional-only-parameters" +target = "keyword-only-arguments" +label = "contrast" + +[[see_also_edge_labels]] +source = "assertions" +target = "exceptions" +label = "alternative" + +[[see_also_edge_labels]] +source = "exception-chaining" +target = "exceptions" +label = "builds on" + +[[see_also_edge_labels]] +source = "exception-groups" +target = "exceptions" +label = "alternative" + +[[see_also_edge_labels]] +source = "operators" +target = "numbers" +label = "related syntax" + +[[see_also_edge_labels]] +source = "operators" +target = "booleans" +label = "condition building" + +[[see_also_edge_labels]] +source = "operators" +target = "assignment-expressions" +label = "specialized expression" + +[[see_also_edge_labels]] +source = "literals" +target = "values" +label = "value surface" + +[[see_also_edge_labels]] +source = "literals" +target = "strings" +label = "text literal" + +[[see_also_edge_labels]] +source = "literals" +target = "sets" +label = "container literal" + +# Example-cell figure attachments. figure names refer to FIGURES paint functions. +[[figure_attachments]] +slug = "mutability" +anchor = "cell-0" +figure = "aliasing-mutation" +caption = "Two names share one mutable list — appending through one name changes the object visible through both." + +[[figure_attachments]] +slug = "variables" +anchor = "cell-0" +figure = "variables-bind" +caption = "A name is a label that points at an object. Assignment binds the label; the object exists independently." + +[[figure_attachments]] +slug = "lists" +anchor = "cell-0" +figure = "list-append" +caption = "Lists are mutable sequences. `.append` extends the same list object — no new list is created." + +[[figure_attachments]] +slug = "dicts" +anchor = "cell-0" +figure = "dict-buckets" +caption = "Each key is hashed to a bucket; collisions chain into the next slot. Lookup is constant-time on average." + +[[figure_attachments]] +slug = "unpacking" +anchor = "cell-0" +figure = "unpacking-bind" +caption = "Left-side names bind to right-side positions; `*rest` gathers the middle into a list." + +[[figure_attachments]] +slug = "comprehensions" +anchor = "cell-0" +figure = "comprehension-equivalence" +caption = "A comprehension is a compact spelling of the equivalent for-loop with append, made into one expression." + +[[figure_attachments]] +slug = "classes" +anchor = "cell-0" +figure = "class-triangle" +caption = "Every Python value sits on the instance → class → type triangle; the metaclass is the type of the class." + +[[figure_attachments]] +slug = "inheritance-and-super" +anchor = "cell-0" +figure = "mro-chain" +caption = "Multiple inheritance forms a graph; C3 linearisation flattens it into the MRO Python uses for attribute lookup." + +[[figure_attachments]] +slug = "dataclasses" +anchor = "cell-0" +figure = "dataclass-fields" +caption = "Field declarations become the generated __init__ signature: declaration is the constructor." + +[[figure_attachments]] +slug = "special-methods" +anchor = "cell-0" +figure = "operator-dispatch" +caption = "Operators are method calls. `a + b` dispatches to `a.__add__(b)`; the data model exposes the syntax." + +[[figure_attachments]] +slug = "decorators" +anchor = "cell-0" +figure = "decorator-rebind" +caption = "@dec rebinds the name to wrapper(f₀); the original function survives only in the wrapper's closure cell." + +[[figure_attachments]] +slug = "recursion" +anchor = "cell-1" +figure = "call-stack" +caption = "Each call pushes a new frame with the same name and a smaller argument; the base case unwinds back up the stack." + +[[figure_attachments]] +slug = "exception-chaining" +anchor = "cell-0" +figure = "exception-cause-context" +caption = "`raise X from Y` sets `__cause__` (explicit); raising during except sets `__context__` (implicit)." + +[[figure_attachments]] +slug = "hello-world" +anchor = "cell-0" +figure = "program-output" +caption = "Every Python program starts as source and produces text on standard output. The smallest mental model." + +[[figure_attachments]] +slug = "numbers" +anchor = "cell-0" +figure = "number-lines" +caption = "Ints have unbounded precision; floats use IEEE doubles whose representable values thin out near the extremes." + +[[figure_attachments]] +slug = "operators" +anchor = "cell-0" +figure = "expression-tree" +caption = "An expression like `(2 + 3) * 4` parses as a tree; operator precedence and parentheses determine its shape." + +[[figure_attachments]] +slug = "none" +anchor = "cell-0" +figure = "none-singleton" +caption = "`None` is a single object: every name that points at None points at the same object." + +[[figure_attachments]] +slug = "equality-and-identity" +anchor = "cell-0" +figure = "identity-and-equality" +caption = "Two names can share one object (`is` and `==` both true) or hold two equal-but-distinct objects (only `==` true)." + +[[figure_attachments]] +slug = "strings" +anchor = "cell-0" +figure = "codepoints-bytes" +caption = "Strings are sequences of Unicode codepoints. UTF-8 encoding turns them into bytes; `é` takes two bytes, `c` takes one." + +[[figure_attachments]] +slug = "for-loops" +anchor = "cell-1" +figure = "iterator-unroll" +caption = "Each call to next() advances the caret one cell along the iterable — the same shape behind range(), strings, and any sequence." + +[[figure_attachments]] +slug = "sorting" +anchor = "cell-1" +figure = "sort-stability" +caption = "Python's sort is stable: items with equal keys keep their original order, so chained sorts compose predictably." + +[[figure_attachments]] +slug = "keyword-only-arguments" +anchor = "cell-0" +figure = "kw-only-separator" +caption = "A bare `*` divides positional or keyword arguments from keyword-only ones; callers must pass `c` and `d` by name." + +[[figure_attachments]] +slug = "positional-only-parameters" +anchor = "cell-0" +figure = "positional-only-separator" +caption = "A bare `/` divides positional-only arguments from positional-or-keyword ones; callers cannot name `a` or `b`." + +[[figure_attachments]] +slug = "closures" +anchor = "cell-0" +figure = "closure-cell" +caption = "The inner function keeps a reference into the outer scope's cell, so the captured factor survives the outer return." + +[[figure_attachments]] +slug = "scope-global-nonlocal" +anchor = "cell-0" +figure = "scope-rings" +caption = "Name lookup walks LEGB — local, enclosing, global, built-in — outward, returning the first binding it finds." + +[[figure_attachments]] +slug = "generators" +anchor = "cell-0" +figure = "generator-ribbon" +caption = "A generator's body is a timeline cut by yield gates: each next() advances to the next gate; locals survive the pause." + +[[figure_attachments]] +slug = "type-hints" +anchor = "cell-0" +figure = "annotation-ghost" +caption = "Annotations describe expected types for tools; the runtime accepts any object regardless." + +[[figure_attachments]] +slug = "exceptions" +anchor = "cell-0" +figure = "exception-lanes" +caption = "try, except, else, and finally as parallel lanes; a single coral path traces what actually runs." + +[[figure_attachments]] +slug = "context-managers" +anchor = "cell-0" +figure = "context-bowtie" +caption = "A context manager pairs setup with reliable cleanup; the raise path still routes through __exit__." + +[[figure_attachments]] +slug = "async-await" +anchor = "cell-1" +figure = "async-swimlane" +caption = "On await, the coroutine yields to the loop; the loop runs other work and resumes when the awaitable is ready." + +[[figure_attachments]] +slug = "iterators" +anchor = "cell-0" +figure = "iter-protocol" +caption = "iter() exposes the iterator behind for; next() pulls one value at a time until exhausted." + +[[figure_attachments]] +slug = "slices" +anchor = "cell-0" +figure = "slice-ruler" +caption = "Slice indices sit between cells; [:3] and [3:] partition the sequence at index 3, never overlapping or losing an item." + +[[figure_attachments]] +slug = "operator-overloading" +anchor = "cell-0" +figure = "operator-dispatch" +caption = "Defining `__add__` on a class lets `+` dispatch into the class's own behavior." + +[[figure_attachments]] +slug = "iterator-vs-iterable" +anchor = "cell-0" +figure = "iter-protocol" +caption = "An iterable knows how to produce an iterator (via iter()); the iterator knows how to produce values (via next())." + +[[figure_attachments]] +slug = "type-aliases" +anchor = "cell-0" +figure = "type-alias-name" +caption = "A type alias names a complex annotation once so call sites read as the domain meaning, not the type composition." + +[[figure_attachments]] +slug = "typed-dicts" +anchor = "cell-0" +figure = "typed-dict-shape" +caption = "TypedDict gives each key a typed value, so `obj['x']` is checked against the declared shape." + +[[figure_attachments]] +slug = "union-and-optional-types" +anchor = "cell-0" +figure = "union-types" +caption = "`int | str | None` says one slot may hold any of three shapes — including expected absence." + +[[figure_attachments]] +slug = "generics-and-typevar" +anchor = "cell-0" +figure = "generic-preservation" +caption = "A generic preserves the input type through the call: the same T flows in and out of fn[T]." + +[[figure_attachments]] +slug = "abstract-base-classes" +anchor = "cell-0" +figure = "class-triangle" +caption = "An ABC sits on the same triangle as concrete classes; subclasses inherit the abstract methods they must implement." + +[[figure_attachments]] +slug = "copying-collections" +anchor = "cell-0" +figure = "aliasing-mutation" +caption = "Without copy() two names share the same object; mutating through one is visible through the other." + +[[figure_attachments]] +slug = "truth-and-size" +anchor = "cell-0" +figure = "truth-and-size" +caption = "bool(x) calls __bool__ first; if absent, __len__() != 0; if neither, defaults to True." + +[[figure_attachments]] +slug = "descriptors" +anchor = "cell-0" +figure = "descriptor-protocol" +caption = "Attribute access on an instance routes through the descriptor's __get__/__set__/__delete__ when the attribute is a descriptor." + +[[figure_attachments]] +slug = "bound-and-unbound-methods" +anchor = "cell-0" +figure = "bound-unbound" +caption = "Accessing a method via an instance binds self; accessing it via the class returns the underlying function." + +[[figure_attachments]] +slug = "classmethods-and-staticmethods" +anchor = "cell-0" +figure = "method-kinds" +caption = "Three method kinds, three first-argument conventions: classmethod gets the class, staticmethod gets nothing, instance gets self." + +[[figure_attachments]] +slug = "callable-objects" +anchor = "cell-0" +figure = "callable-objects" +caption = "Defining __call__ makes any object callable; functions are just one shape that satisfies this protocol." + +[[figure_attachments]] +slug = "attribute-access" +anchor = "cell-0" +figure = "attribute-lookup" +caption = "obj.x checks instance __dict__, then class __dict__, then __getattr__; the first hit wins." + +[[figure_attachments]] +slug = "guard-clauses" +anchor = "cell-0" +figure = "guard-clauses" +caption = "Early returns handle the exceptional cases first so the main work is the body of the function, not its tail." + +[[figure_attachments]] +slug = "bytes-and-bytearray" +anchor = "cell-0" +figure = "bytes-vs-bytearray" +caption = "bytes is a frozen sequence of integers; bytearray is the mutable counterpart with append/extend/etc." + +[[figure_attachments]] +slug = "sentinel-iteration" +anchor = "cell-0" +figure = "sentinel-iteration" +caption = "`iter(callable, sentinel)` calls the callable repeatedly, stopping when it returns the sentinel." + +[[figure_attachments]] +slug = "partial-functions" +anchor = "cell-0" +figure = "partial-functions" +caption = "`functools.partial(f, 1)` pre-fills `a=1`, returning a thinner callable `g(b, c)` that only needs the rest." + +[[figure_attachments]] +slug = "args-and-kwargs" +anchor = "cell-0" +figure = "args-kwargs" +caption = "*args captures the extra positionals as a tuple; **kwargs captures the extra keywords as a dict." + +[[figure_attachments]] +slug = "multiple-return-values" +anchor = "cell-0" +figure = "multiple-return" +caption = "A function returning multiple values really returns one tuple; the caller unpacks it into named bindings." + +[[figure_attachments]] +slug = "lambdas" +anchor = "cell-0" +figure = "lambda-expression" +caption = "A lambda is a function literal: parameters before the colon, a single expression after, no statement body." + +[[figure_attachments]] +slug = "properties" +anchor = "cell-0" +figure = "property-fork" +caption = "When x is a property, attribute access routes through fget/fset instead of touching __dict__." + +[[figure_attachments]] +slug = "metaclasses" +anchor = "cell-0" +figure = "metaclass-triangle" +caption = "A metaclass is the type of a class, just as a class is the type of its instances; type is the default metaclass." + +[[figure_attachments]] +slug = "modules" +anchor = "cell-0" +figure = "sys-path-resolution" +caption = "An import walks sys.path entry by entry; the first directory containing the module wins." + +[[figure_attachments]] +slug = "import-aliases" +anchor = "cell-0" +figure = "import-alias" +caption = "`import x as y` binds the name y to the same module object x would have." + +[[figure_attachments]] +slug = "protocols" +anchor = "cell-0" +figure = "protocol-check" +caption = "An object satisfies a protocol structurally — by having the required methods — not by inheriting it." + +[[figure_attachments]] +slug = "enums" +anchor = "cell-0" +figure = "enum-members" +caption = "An enum names a fixed set of symbolic values; no new members appear at runtime." + +[[figure_attachments]] +slug = "datetime" +anchor = "cell-0" +figure = "datetime-instant" +caption = "An aware datetime carries a UTC offset; one instant in time reads differently on two clocks." + +[[figure_attachments]] +slug = "json" +anchor = "cell-0" +figure = "json-python-mapping" +caption = "Six type pairs bridge the JSON text boundary; each json value maps to one Python type." + +[[figure_attachments]] +slug = "regular-expressions" +anchor = "cell-0" +figure = "regex-anchors" +caption = "^ and $ anchor the pattern; quantifiers like {2} bound how many times a token repeats." + +[[figure_attachments]] +slug = "number-parsing" +anchor = "cell-0" +figure = "number-parse" +caption = "int() turns text into a typed number; malformed input raises ValueError instead of guessing." + +[[figure_attachments]] +slug = "string-formatting" +anchor = "cell-1" +figure = "format-spec" +caption = "The format spec is a railroad of named optional fields: alignment, sign, width, precision, type." + +[[figure_attachments]] +slug = "truthiness" +anchor = "cell-0" +figure = "truthy-check" +caption = "bool(x) is True except for a small fixed set: 0, 0.0, \"\", [], {}, None, False." + +[[figure_attachments]] +slug = "booleans" +anchor = "cell-0" +figure = "boolean-truth-table" +caption = "`a and b` returns True only when both are True; otherwise it returns the first falsy value." + +[[figure_attachments]] +slug = "sets" +anchor = "cell-0" +figure = "set-buckets" +caption = "Sets are hash buckets without values; `x in s` averages O(1) regardless of size." + +[[figure_attachments]] +slug = "tuples" +anchor = "cell-0" +figure = "tuple-frozen" +caption = "Tuples are ordered, immutable sequences; positions matter, contents do not change once constructed." + +[[figure_attachments]] +slug = "values" +anchor = "cell-0" +figure = "value-types" +caption = "Every literal is an object with a type; the type carries the behaviour, not the variable name." + +[[figure_attachments]] +slug = "yield-from" +anchor = "cell-0" +figure = "yield-delegation" +caption = "`yield from inner` delegates iteration to an inner generator; its yields surface here unchanged." + +[[figure_attachments]] +slug = "itertools" +anchor = "cell-0" +figure = "itertools-chain" +caption = "chain stitches two iterables into one stream without materialising either: values arrive lazily." + +[[figure_attachments]] +slug = "assertions" +anchor = "cell-0" +figure = "assertion-check" +caption = "assert tests a condition; True passes silently, False raises AssertionError with the optional message." + +[[figure_attachments]] +slug = "custom-exceptions" +anchor = "cell-0" +figure = "custom-exception-chain" +caption = "Subclassing an existing exception gains a domain name without changing semantics." + +[[figure_attachments]] +slug = "exception-groups" +anchor = "cell-0" +figure = "exception-group-peel" +caption = "except* peels matched leaves out of an ExceptionGroup; survivors regroup and propagate." + +[[figure_attachments]] +slug = "delete-statements" +anchor = "cell-0" +figure = "delete-name-erased" +caption = "`del x` removes the name; the object survives if any other reference holds it, otherwise gets collected." + +[[figure_attachments]] +slug = "conditionals" +anchor = "cell-0" +figure = "branch-fork" +caption = "A predicate sorts a value into one of several branches; if/elif/else is the explicit spelling." + +[[figure_attachments]] +slug = "match-statements" +anchor = "cell-0" +figure = "match-dispatch-ladder" +caption = "match dispatches by pattern shape; the value flows down the patterns and the first match wins." + +[[figure_attachments]] +slug = "assignment-expressions" +anchor = "cell-0" +figure = "naming-decisions" +caption = "The walrus binds a name during the surrounding expression; one expression, two outputs." + +[[figure_attachments]] +slug = "iterating-over-iterables" +anchor = "cell-0" +figure = "iter-protocol" +caption = "`for` desugars to iter()+next(): one iter() call, then next() until StopIteration ends the loop." + +[[figure_attachments]] +slug = "generator-expressions" +anchor = "cell-1" +figure = "lazy-stream" +caption = "A generator expression composes filter and map lazily; values flow only when next() pulls them." + +[[figure_attachments]] +slug = "async-iteration-and-context" +anchor = "cell-0" +figure = "async-swimlane" +caption = "async iteration and async with both rest on the same loop-vs-coroutine handoff as await." + +[[figure_attachments]] +slug = "loop-else" +anchor = "cell-0" +figure = "loop-else-gate" +caption = "The loop's else branch runs only when the loop falls through naturally; break skips it." + +[[figure_attachments]] +slug = "break-and-continue" +anchor = "cell-0" +figure = "early-exit" +caption = "break exits the loop; continue skips to the next iteration. Both interrupt the natural fall-through." + +[[figure_attachments]] +slug = "comprehension-patterns" +anchor = "cell-0" +figure = "comprehension-equivalence" +caption = "Nested clauses compose left to right; the comprehension is still equivalent to a for-loop with append." + +[[figure_attachments]] +slug = "container-protocols" +anchor = "cell-0" +figure = "container-methods" +caption = "Container syntax routes to narrow protocol methods: assignment to __setitem__, membership to __contains__, lookup to __getitem__." + +[[figure_attachments]] +slug = "functions" +anchor = "cell-0" +figure = "function-with-body" +caption = "A function takes inputs, evaluates a body, and returns a value: `greet('Ada')` produces `'Hello, Ada'`." + +[[figure_attachments]] +slug = "constants" +anchor = "cell-0" +figure = "variables-bind" +caption = "UPPER_CASE is a naming convention, not a language constraint; the binding behaves like any other variable." + +[[figure_attachments]] +slug = "while-loops" +anchor = "cell-0" +figure = "while-backedge" +caption = "`while` repeats the body while the test stays true; the back-edge returns control to the test before each pass." + +[[figure_attachments]] +slug = "while-loops" +anchor = "cell-0" +figure = "loop-repetition" +caption = "Each loop shape has its own stopping rule: `for` ends when the iterable is exhausted, `while` when the condition turns false, sentinel iteration when the marker value appears." + +[[figure_attachments]] +slug = "advanced-match-patterns" +anchor = "cell-0" +figure = "match-pattern-variants" +caption = "Capture, alternative, guard, and class patterns each name a different way a value can match a case." + +[[figure_attachments]] +slug = "literals" +anchor = "cell-0" +figure = "literal-forms" +caption = "Each Python type has its own literal spellings; ints accept decimal, hex, and binary; strings accept either quote." + +[[figure_attachments]] +slug = "packages" +anchor = "cell-0" +figure = "package-tree" +caption = "A directory with __init__.py becomes an importable package; submodules and subpackages nest beneath it." + +[[figure_attachments]] +slug = "virtual-environments" +anchor = "cell-0" +figure = "venv-boundary" +caption = "A venv carries its own interpreter and site-packages, isolating a project's dependencies from the system." + +[[figure_attachments]] +slug = "subprocesses" +anchor = "cell-0" +figure = "subprocess-spawn" +caption = "subprocess.run spawns a child process and captures its stdout, stderr, and exit code as portable evidence." + +[[figure_attachments]] +slug = "logging" +anchor = "cell-0" +figure = "logging-levels" +caption = "Five severity levels; the logger's configured threshold drops everything below it." + +[[figure_attachments]] +slug = "testing" +anchor = "cell-0" +figure = "aaa-pattern" +caption = "arrange-act-assert: set up the state, perform the behavior under test, compare the result to expectations." + +[[figure_attachments]] +slug = "networking" +anchor = "cell-0" +figure = "socket-byte-boundary" +caption = "Text crosses the socket as bytes — `encode` marks the python → wire boundary, `decode` brings the bytes back to a Python `str`." + +[[figure_attachments]] +slug = "threads-and-processes" +anchor = "cell-0" +figure = "gil-lanes" +caption = "Threads share memory but the GIL serialises Python bytecode; processes run in parallel with isolated memory." + +[[figure_attachments]] +slug = "casts-and-any" +anchor = "cell-0" +figure = "cast-escape" +caption = "cast(T, x) tells the type checker to treat x as T; the runtime is unaffected." + +[[figure_attachments]] +slug = "newtype" +anchor = "cell-0" +figure = "newtype-phantom" +caption = "NewType creates a distinct static identity backed by the same runtime type — UserId is int with a name." + +[[figure_attachments]] +slug = "overloads" +anchor = "cell-0" +figure = "overload-signatures" +caption = "@overload declares static call signatures for double(int) and double(str); one runtime implementation handles both." + +[[figure_attachments]] +slug = "paramspec" +anchor = "cell-0" +figure = "paramspec-preserve" +caption = "ParamSpec preserves the wrapped function's signature through a decorator, parameter for parameter." + +[[figure_attachments]] +slug = "literal-and-final" +anchor = "cell-0" +figure = "literal-constrained" +caption = "Literal narrows a slot to a fixed set of constant values; Final says the binding will not change." + +[[figure_attachments]] +slug = "callable-types" +anchor = "cell-0" +figure = "callable-type" +caption = "A Callable annotation describes the callback slot: this argument list must produce this return type." + +[[figure_attachments]] +slug = "runtime-type-checks" +anchor = "cell-0" +figure = "isinstance-check" +caption = "isinstance and issubclass ask the runtime; the answer is a bool, not a static type refinement." + +[[figure_attachments]] +slug = "collections-module" +anchor = "cell-0" +figure = "collections-containers" +caption = "Four specialised containers for shapes the built-in types don't cover well: deque, Counter, defaultdict, namedtuple." + +[[figure_attachments]] +slug = "structured-data-shapes" +anchor = "cell-0" +figure = "structured-shapes" +caption = "The same record can be a mutable dataclass, an immutable NamedTuple, or a runtime dict described by TypedDict." + +[[figure_attachments]] +slug = "csv-data" +anchor = "cell-0" +figure = "csv-records" +caption = "CSV files are rows of records; each line has the same columns in the same order." + +[[figure_attachments]] +slug = "warnings" +anchor = "cell-0" +figure = "warning-signal" +caption = "A warning is a soft signal: the message is reported, but execution continues unless filters elevate it." + +[[figure_attachments]] +slug = "object-lifecycle" +anchor = "cell-0" +figure = "object-lifecycle" +caption = "Names keep the same object reachable; deleting one name matters only when it removes the last reference." + +# Journey-section figures keyed by the visible section title. +[[journey_section_figures]] +section = "Start with executable evidence." +figure = "runtime-evidence-loop" +caption = "Examples are evidence loops: source, a run step, and visible output stay together." + +[[journey_section_figures]] +section = "Separate value, identity, and absence." +figure = "runtime-object-axes" +caption = "Runtime objects answer separate questions: equal value, same identity, or the singleton that marks absence." + +[[journey_section_figures]] +section = "Read expressions as object operations." +figure = "runtime-expression-model" +caption = "Expression syntax enters the data model; object methods produce the result." + +[[journey_section_figures]] +section = "Choose between paths." +figure = "control-decision-map" +caption = "Facts flow into one decision point; exactly one branch owns the next step." + +[[journey_section_figures]] +section = "Name and shape decisions." +figure = "control-fact-shape" +caption = "Name a fact when a condition needs it; match shape when the data structure is the decision." + +[[journey_section_figures]] +section = "Stop as soon as the answer is known." +figure = "control-stop-boundary" +caption = "Early exits draw a boundary: once the answer is found, the tail stays unread." + +[[journey_section_figures]] +section = "Choose the right loop shape." +figure = "iteration-loop-selector" +caption = "Choose the loop from its stopping rule: exhaustion, condition, or sentinel marker." + +[[journey_section_figures]] +section = "See the protocol behind `for`." +figure = "iteration-protocol-map" +caption = "for is surface syntax; iter() creates an iterator and next() pulls values until StopIteration." + +[[journey_section_figures]] +section = "Compose lazy value streams." +figure = "iteration-lazy-pull" +caption = "Lazy pipelines run from the consumer's pull: next() requests one value through each stage." + +[[journey_section_figures]] +section = "Pick the container that matches the question." +figure = "container-questions" +caption = "Each container answers a different question: ordered, fixed, lookup, unique." + +[[journey_section_figures]] +section = "Move between shapes deliberately." +figure = "reshape-pipeline" +caption = "Most everyday code reshapes data: one input, one transform, one new value." + +[[journey_section_figures]] +section = "Cross text and data boundaries." +figure = "text-data-boundary" +caption = "Programs receive text and produce structured data; parsing makes the boundary explicit." + +[[journey_section_figures]] +section = "Start with functions as named behavior." +figure = "function-signature" +caption = "A function is the first abstraction boundary: arguments in, body, return value out." + +[[journey_section_figures]] +section = "Use functions as values." +figure = "function-as-value" +caption = "Functions are first-class values. A second name binds to the same function object." + +[[journey_section_figures]] +section = "Bundle behavior with state." +figure = "class-with-state" +caption = "Classes group fields and methods so data and behavior move together behind one interface." + +[[journey_section_figures]] +section = "Keep runtime and static analysis separate." +figure = "type-runtime-static-split" +caption = "Runtime values run the program; static tools inspect separate annotations and report before execution." + +[[journey_section_figures]] +section = "Describe realistic data shapes." +figure = "type-shape-catalog" +caption = "Real data contracts combine fields, variants, and expected absence instead of one scalar type." + +[[journey_section_figures]] +section = "Scale annotations for reusable libraries." +figure = "type-library-contract" +caption = "Reusable APIs carry caller contracts through the library boundary with generics, parameters, and overloads." + +[[journey_section_figures]] +section = "Make failure explicit." +figure = "reliability-signal-map" +caption = "Different failure shapes need explicit signals: assertions, recovery, chained causes, or warnings." + +[[journey_section_figures]] +section = "Control resource and module boundaries." +figure = "reliability-boundary-map" +caption = "Reliable programs name their boundaries: resources clean up, modules import, environments constrain runtime." + +[[journey_section_figures]] +section = "Handle operations that outlive one expression." +figure = "reliability-operation-boundary" +caption = "Async, threaded, test, and logging work cross an operation boundary before evidence comes back." + +# Curated scores for example-cell figures. +[[example_figure_scores]] +slug = "variables" +score = 9.5 +comment = "the canonical name → object picture" + +[[example_figure_scores]] +slug = "mutability" +score = 9.5 +comment = "three-state small multiple of aliased mutation" + +[[example_figure_scores]] +slug = "copying-collections" +score = 9.5 +comment = "same picture as mutability, perfect match" + +[[example_figure_scores]] +slug = "hello-world" +score = 9.0 +comment = "program → output, smallest mechanism" + +[[example_figure_scores]] +slug = "numbers" +score = 9.0 +comment = "int unbounded vs float thinning, both registers" + +[[example_figure_scores]] +slug = "operators" +score = 9.0 +comment = "expression tree mechanism" + +[[example_figure_scores]] +slug = "none" +score = 9.0 +comment = "three names converging on one None" + +[[example_figure_scores]] +slug = "equality-and-identity" +score = 9.0 +comment = "shared vs separate object, side-by-side" + +[[example_figure_scores]] +slug = "strings" +score = 9.0 +comment = "codepoints + bytes registers" + +[[example_figure_scores]] +slug = "for-loops" +score = 9.0 +comment = "4-row caret advance" + +[[example_figure_scores]] +slug = "sorting" +score = 9.0 +comment = "stability ribbons preserved across keys" + +[[example_figure_scores]] +slug = "keyword-only-arguments" +score = 9.0 +comment = "signature with explicit `*` separator" + +[[example_figure_scores]] +slug = "positional-only-parameters" +score = 9.0 +comment = "signature with explicit `/` separator" + +[[example_figure_scores]] +slug = "closures" +score = 9.0 +comment = "captured cell reference" + +[[example_figure_scores]] +slug = "scope-global-nonlocal" +score = 9.0 +comment = "LEGB nested rings" + +[[example_figure_scores]] +slug = "recursion" +score = 9.0 +comment = "stacked frames with same name, different argument" + +[[example_figure_scores]] +slug = "lists" +score = 9.0 +comment = "cells with append mechanism" + +[[example_figure_scores]] +slug = "dicts" +score = 9.0 +comment = "hash buckets with collision chain" + +[[example_figure_scores]] +slug = "slices" +score = 9.0 +comment = "ruler with bracket overlay" + +[[example_figure_scores]] +slug = "comprehensions" +score = 9.0 +comment = "comprehension over equivalent for-loop" + +[[example_figure_scores]] +slug = "type-hints" +score = 9.0 +comment = "ghost annotations over runtime values" + +[[example_figure_scores]] +slug = "generators" +score = 9.0 +comment = "ribbon cut by yield gates" + +[[example_figure_scores]] +slug = "exceptions" +score = 9.0 +comment = "try/except/else/finally lanes with traced path" + +[[example_figure_scores]] +slug = "context-managers" +score = 9.0 +comment = "enter / body / exit bowtie" + +[[example_figure_scores]] +slug = "async-await" +score = 9.0 +comment = "loop/coro swimlane with await handoffs" + +[[example_figure_scores]] +slug = "classes" +score = 9.0 +comment = "instance/class/type triangle" + +[[example_figure_scores]] +slug = "inheritance-and-super" +score = 9.0 +comment = "MRO chain with diamond ghost" + +[[example_figure_scores]] +slug = "dataclasses" +score = 9.0 +comment = "fields → generated __init__ signature" + +[[example_figure_scores]] +slug = "decorators" +score = 9.0 +comment = "before/after rebinding through cell" + +[[example_figure_scores]] +slug = "special-methods" +score = 9.0 +comment = "syntax → method dispatch" + +[[example_figure_scores]] +slug = "unpacking" +score = 9.0 +comment = "binding-line mechanism with *rest" + +[[example_figure_scores]] +slug = "exception-chaining" +score = 9.0 +comment = "__cause__ vs __context__ distinguished" + +[[example_figure_scores]] +slug = "iterating-over-iterables" +score = 9.0 +comment = "iter() exposes the iterator" + +[[example_figure_scores]] +slug = "iterators" +score = 9.0 +comment = "three-state machine" + +[[example_figure_scores]] +slug = "iterator-vs-iterable" +score = 9.0 +comment = "the protocol exposed" + +[[example_figure_scores]] +slug = "container-protocols" +score = 9.0 +comment = "syntax routes to __setitem__, __contains__, __getitem__" + +[[example_figure_scores]] +slug = "operator-overloading" +score = 9.0 +comment = "dispatch arrow" + +[[example_figure_scores]] +slug = "union-and-optional-types" +score = 9.0 +comment = "type fork to several shapes" + +[[example_figure_scores]] +slug = "abstract-base-classes" +score = 9.0 +comment = "same triangle as concrete classes" + +[[example_figure_scores]] +slug = "conditionals" +score = 9.0 +comment = "predicate forks value to branch" + +[[example_figure_scores]] +slug = "match-statements" +score = 9.0 +comment = "dispatch ladder; first match wins" + +[[example_figure_scores]] +slug = "advanced-match-patterns" +score = 9.0 +comment = "four pattern variants" + +[[example_figure_scores]] +slug = "loop-else" +score = 9.0 +comment = "fell-through vs broke, two outcomes" + +[[example_figure_scores]] +slug = "while-loops" +score = 9.0 +comment = "back-edge mechanism plus the three stopping rules" + +[[example_figure_scores]] +slug = "type-aliases" +score = 9.0 +comment = "complex annotation collapses to a name" + +[[example_figure_scores]] +slug = "typed-dicts" +score = 9.0 +comment = "keys with declared value types" + +[[example_figure_scores]] +slug = "comprehension-patterns" +score = 9.0 +comment = "nested clauses compose" + +[[example_figure_scores]] +slug = "lambdas" +score = 9.0 +comment = "function literal: params / expression" + +[[example_figure_scores]] +slug = "string-formatting" +score = 9.0 +comment = "format-spec railroad" + +[[example_figure_scores]] +slug = "regular-expressions" +score = 9.0 +comment = "pattern ruler with anchors" + +[[example_figure_scores]] +slug = "json" +score = 9.0 +comment = "two-column type mapping" + +[[example_figure_scores]] +slug = "metaclasses" +score = 9.0 +comment = "extended triangle to metaclass" + +[[example_figure_scores]] +slug = "datetime" +score = 9.0 +comment = "one instant, two clock offsets" + +[[example_figure_scores]] +slug = "values" +score = 9.0 +comment = "every literal is a typed object" + +[[example_figure_scores]] +slug = "literals" +score = 9.0 +comment = "literal spellings per type" + +[[example_figure_scores]] +slug = "booleans" +score = 9.0 +comment = "2×2 truth table" + +[[example_figure_scores]] +slug = "sets" +score = 9.0 +comment = "hash buckets without values" + +[[example_figure_scores]] +slug = "yield-from" +score = 9.0 +comment = "stitched ribbons; delegation" + +[[example_figure_scores]] +slug = "generator-expressions" +score = 9.0 +comment = "lazy filter→map pipeline" + +[[example_figure_scores]] +slug = "async-iteration-and-context" +score = 9.0 +comment = "loop/coro lanes with await yields" + +[[example_figure_scores]] +slug = "assignment-expressions" +score = 9.0 +comment = "walrus binds while comparing" + +[[example_figure_scores]] +slug = "break-and-continue" +score = 9.0 +comment = "early exit at first match" + +[[example_figure_scores]] +slug = "delete-statements" +score = 9.0 +comment = "name erased; object survives if referenced" + +[[example_figure_scores]] +slug = "exception-groups" +score = 9.0 +comment = "except* peels matching leaves" + +[[example_figure_scores]] +slug = "custom-exceptions" +score = 9.0 +comment = "subclass chain to a domain name" + +[[example_figure_scores]] +slug = "modules" +score = 9.0 +comment = "sys.path resolution; first hit wins" + +[[example_figure_scores]] +slug = "protocols" +score = 9.0 +comment = "structural duck check" + +[[example_figure_scores]] +slug = "enums" +score = 9.0 +comment = "closed set of symbolic values" + +[[example_figure_scores]] +slug = "functions" +score = 9.0 +comment = "specific call: greet('Ada') → 'Hello, Ada'" + +[[example_figure_scores]] +slug = "constants" +score = 9.0 +comment = "name binding; UPPER_CASE is convention" + +[[example_figure_scores]] +slug = "import-aliases" +score = 9.0 +comment = "two names bind to the same module" + +[[example_figure_scores]] +slug = "number-parsing" +score = 9.0 +comment = "int() success path vs ValueError" + +[[example_figure_scores]] +slug = "tuples" +score = 9.0 +comment = "frozen sequence with struck-through .append" + +[[example_figure_scores]] +slug = "truthiness" +score = 9.0 +comment = "bool(x) with the falsy set as a strip" + +[[example_figure_scores]] +slug = "itertools" +score = 9.0 +comment = "chain joins two iterables into one stream" + +[[example_figure_scores]] +slug = "assertions" +score = 9.0 +comment = "True passes, False raises" + +[[example_figure_scores]] +slug = "descriptors" +score = 9.0 +comment = "get/set/delete protocol routed through descriptor" + +[[example_figure_scores]] +slug = "attribute-access" +score = 9.0 +comment = "instance __dict__ → class __dict__ → __getattr__" + +[[example_figure_scores]] +slug = "bound-and-unbound-methods" +score = 9.0 +comment = "instance.method bound vs Class.method unbound" + +[[example_figure_scores]] +slug = "classmethods-and-staticmethods" +score = 9.0 +comment = "three method kinds, three first-arg conventions" + +[[example_figure_scores]] +slug = "callable-objects" +score = 9.0 +comment = "__call__ makes any object callable" + +[[example_figure_scores]] +slug = "generics-and-typevar" +score = 9.0 +comment = "the same T flows in and out" + +[[example_figure_scores]] +slug = "truth-and-size" +score = 9.0 +comment = "__bool__ → __len__ → True fallback chain" + +[[example_figure_scores]] +slug = "bytes-and-bytearray" +score = 9.0 +comment = "frozen vs mutable contrast" + +[[example_figure_scores]] +slug = "sentinel-iteration" +score = 9.0 +comment = "iter(callable, sentinel) stop condition" + +[[example_figure_scores]] +slug = "partial-functions" +score = 9.0 +comment = "f → partial(f, 1) → g" + +[[example_figure_scores]] +slug = "guard-clauses" +score = 9.0 +comment = "early returns, main body at the tail" + +[[example_figure_scores]] +slug = "packages" +score = 9.0 +comment = "__init__.py + nested submodules" + +[[example_figure_scores]] +slug = "virtual-environments" +score = 9.0 +comment = "project / venv boundary" + +[[example_figure_scores]] +slug = "subprocesses" +score = 9.0 +comment = "spawn → child → captured output" + +[[example_figure_scores]] +slug = "logging" +score = 9.0 +comment = "five thresholded levels" + +[[example_figure_scores]] +slug = "testing" +score = 9.0 +comment = "arrange-act-assert three-row pattern" + +[[example_figure_scores]] +slug = "networking" +score = 9.0 +comment = "text ↔ bytes across the socket boundary" + +[[example_figure_scores]] +slug = "casts-and-any" +score = 9.0 +comment = "Any → cast(T, x) → T, runtime unchanged" + +[[example_figure_scores]] +slug = "newtype" +score = 9.0 +comment = "same runtime, distinct static identity" + +[[example_figure_scores]] +slug = "paramspec" +score = 9.0 +comment = "P preserved through decorator" + +[[example_figure_scores]] +slug = "literal-and-final" +score = 9.0 +comment = "slot narrows to a fixed set" + +[[example_figure_scores]] +slug = "runtime-type-checks" +score = 9.0 +comment = "isinstance returns bool" + +[[example_figure_scores]] +slug = "collections-module" +score = 9.0 +comment = "deque / Counter / defaultdict / namedtuple" + +[[example_figure_scores]] +slug = "structured-data-shapes" +score = 9.0 +comment = "dataclass vs NamedTuple vs TypedDict runtime trade-offs" + +[[example_figure_scores]] +slug = "csv-data" +score = 9.0 +comment = "rows × columns; same shape per line" + +[[example_figure_scores]] +slug = "warnings" +score = 9.0 +comment = "soft signal; execution continues" + +[[example_figure_scores]] +slug = "object-lifecycle" +score = 9.0 +comment = "names keep object reachable until last reference disappears" + +[[example_figure_scores]] +slug = "args-and-kwargs" +score = 9.0 +comment = "*args tuple, **kwargs dict regions" + +[[example_figure_scores]] +slug = "multiple-return-values" +score = 9.0 +comment = "function returns tuple; caller unpacks" + +[[example_figure_scores]] +slug = "properties" +score = 9.0 +comment = "obj.x routes through fget instead of __dict__" + +[[example_figure_scores]] +slug = "overloads" +score = 9.0 +comment = "static overload signatures contrasted with one runtime body" + +[[example_figure_scores]] +slug = "callable-types" +score = 9.0 +comment = "callback slot shows argument list and return type" + +[[example_figure_scores]] +slug = "threads-and-processes" +score = 9.0 +comment = "thread GIL sharing contrasted with process isolation" + +# Curated scores for journey-section figures. +[[journey_section_figure_scores]] +section = "Start with executable evidence." +score = 9.0 +comment = "journey-native source/run/output evidence loop" + +[[journey_section_figure_scores]] +section = "Separate value, identity, and absence." +score = 9.0 +comment = "three runtime questions separated" + +[[journey_section_figure_scores]] +section = "Read expressions as object operations." +score = 9.0 +comment = "syntax enters the data-model method surface" + +[[journey_section_figure_scores]] +section = "Choose between paths." +score = 9.0 +comment = "branch map independent of one if-statement example" + +[[journey_section_figure_scores]] +section = "Name and shape decisions." +score = 9.0 +comment = "name-vs-shape decision map" + +[[journey_section_figure_scores]] +section = "Stop as soon as the answer is known." +score = 9.0 +comment = "early boundary leaves tail unread" + +[[journey_section_figure_scores]] +section = "Choose the right loop shape." +score = 9.0 +comment = "loop choice by stopping rule" + +[[journey_section_figure_scores]] +section = "See the protocol behind `for`." +score = 9.5 +comment = "for surface mapped to iter()/next()/StopIteration" + +[[journey_section_figure_scores]] +section = "Compose lazy value streams." +score = 9.0 +comment = "consumer pull drives the lazy pipeline" + +[[journey_section_figure_scores]] +section = "Pick the container that matches the question." +score = 9.0 +comment = "list/tuple/dict/set per question" + +[[journey_section_figure_scores]] +section = "Move between shapes deliberately." +score = 9.0 +comment = "input → transform → result" + +[[journey_section_figure_scores]] +section = "Cross text and data boundaries." +score = 9.0 +comment = "text in, structured value out" + +[[journey_section_figure_scores]] +section = "Start with functions as named behavior." +score = 9.0 +comment = "concrete call, named body, and return boundary" + +[[journey_section_figure_scores]] +section = "Use functions as values." +score = 9.0 +comment = "second name binds same function" + +[[journey_section_figure_scores]] +section = "Bundle behavior with state." +score = 9.0 +comment = "class groups state + methods" + +[[journey_section_figure_scores]] +section = "Keep runtime and static analysis separate." +score = 9.0 +comment = "runtime track separated from static-tool track" + +[[journey_section_figure_scores]] +section = "Describe realistic data shapes." +score = 9.0 +comment = "field, variant, and absence contracts in one map" + +[[journey_section_figure_scores]] +section = "Scale annotations for reusable libraries." +score = 9.0 +comment = "caller contracts survive library API boundary" + +[[journey_section_figure_scores]] +section = "Make failure explicit." +score = 9.0 +comment = "failure shapes mapped to explicit signals" + +[[journey_section_figure_scores]] +section = "Control resource and module boundaries." +score = 9.0 +comment = "resource/module/env boundaries shown together" + +[[journey_section_figure_scores]] +section = "Handle operations that outlive one expression." +score = 9.0 +comment = "operation boundary returns evidence later" + +# Curated example quality scores. +[[example_quality_scores]] +slug = "hello-world" +score = 7.1 +comment = "waived: traditional smallest complete program" + +[[example_quality_scores]] +slug = "values" +score = 9.0 +comment = "object/type/operation map with prerequisite graph edges" + +[[example_quality_scores]] +slug = "literals" +score = 9.0 +comment = "criterion-level pass: graph-rich, note-heavy, multi-cell" + +[[example_quality_scores]] +slug = "numbers" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "booleans" +score = 9.0 +comment = "truth operations, short-circuiting, bool-as-int footgun" + +[[example_quality_scores]] +slug = "operators" +score = 9.0 +comment = "criterion-level pass: graph-rich, note-heavy, multi-cell" + +[[example_quality_scores]] +slug = "none" +score = 9.0 +comment = "None, mapping default, and exception absence boundaries" + +[[example_quality_scores]] +slug = "variables" +score = 9.0 +comment = "binding, rebinding, augmented-assignment, lifecycle graph" + +[[example_quality_scores]] +slug = "constants" +score = 9.0 +comment = "constant convention, Final boundary, runtime rebinding evidence" + +[[example_quality_scores]] +slug = "truthiness" +score = 9.0 +comment = "truth-value categories, explicit-comparison boundary, graph-linked" + +[[example_quality_scores]] +slug = "equality-and-identity" +score = 9.0 +comment = "== versus is, singleton check, identity-cache warning" + +[[example_quality_scores]] +slug = "mutability" +score = 9.0 +comment = "in-place mutation, rebinding, alias visibility" + +[[example_quality_scores]] +slug = "object-lifecycle" +score = 9.0 +comment = "references, rebinding, and last-reference boundary" + +[[example_quality_scores]] +slug = "strings" +score = 9.0 +comment = "Unicode text, immutability, encoding boundary" + +[[example_quality_scores]] +slug = "bytes-and-bytearray" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "string-formatting" +score = 9.0 +comment = "f-string expression, format spec, debug/repr boundary" + +[[example_quality_scores]] +slug = "conditionals" +score = 9.0 +comment = "branch ordering, truthiness, and ternary boundary" + +[[example_quality_scores]] +slug = "guard-clauses" +score = 9.0 +comment = "nested contrast plus flattened guard path" + +[[example_quality_scores]] +slug = "assignment-expressions" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "for-loops" +score = 9.0 +comment = "direct iteration, range, and enumerate progression" + +[[example_quality_scores]] +slug = "break-and-continue" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "loop-else" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "iterating-over-iterables" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "iterators" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "iterator-vs-iterable" +score = 9.0 +comment = "graph-rich" + +[[example_quality_scores]] +slug = "sentinel-iteration" +score = 9.0 +comment = "call-until-marker shape contrasted with manual while" + +[[example_quality_scores]] +slug = "match-statements" +score = 9.0 +comment = "shape dispatch and guarded pattern payoff" + +[[example_quality_scores]] +slug = "advanced-match-patterns" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "while-loops" +score = 9.0 +comment = "state-driven repetition contrasted with for loops" + +[[example_quality_scores]] +slug = "lists" +score = 9.0 +comment = "ordered mutable sequence operations plus tuple/set/copy boundaries" + +[[example_quality_scores]] +slug = "tuples" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "unpacking" +score = 9.0 +comment = "sequence, starred, and mapping unpacking shapes" + +[[example_quality_scores]] +slug = "dicts" +score = 9.0 +comment = "lookup/default/update/delete dictionary boundaries" + +[[example_quality_scores]] +slug = "sets" +score = 9.0 +comment = "uniqueness, membership, set algebra, ordering boundary" + +[[example_quality_scores]] +slug = "slices" +score = 9.0 +comment = "range bounds, step, reverse, and unchanged-source evidence" + +[[example_quality_scores]] +slug = "comprehensions" +score = 9.0 +comment = "loop-equivalent collection building with filter/projection boundaries" + +[[example_quality_scores]] +slug = "comprehension-patterns" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "sorting" +score = 9.0 +comment = "key functions, reverse order, sorted/list.sort boundary" + +[[example_quality_scores]] +slug = "collections-module" +score = 9.0 +comment = "Counter, defaultdict, deque, namedtuple mapped to data shapes" + +[[example_quality_scores]] +slug = "copying-collections" +score = 9.0 +comment = "alias, shallow copy, and deepcopy boundaries" + +[[example_quality_scores]] +slug = "functions" +score = 9.0 +comment = "definition/call/return/default-boundary walkthrough" + +[[example_quality_scores]] +slug = "keyword-only-arguments" +score = 9.0 +comment = "call-site readability and boolean flag boundary" + +[[example_quality_scores]] +slug = "positional-only-parameters" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "args-and-kwargs" +score = 9.0 +comment = "flexible argument collection plus forwarding/API-boundary graph" + +[[example_quality_scores]] +slug = "multiple-return-values" +score = 9.0 +comment = "tuple return plus unpacking boundary" + +[[example_quality_scores]] +slug = "closures" +score = 9.0 +comment = "closed-over state, factory payoff, late-binding boundary" + +[[example_quality_scores]] +slug = "partial-functions" +score = 9.0 +comment = "before/after callable adaptation and introspection" + +[[example_quality_scores]] +slug = "scope-global-nonlocal" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "recursion" +score = 9.0 +comment = "base case and recursive tree-shape payoff" + +[[example_quality_scores]] +slug = "lambdas" +score = 9.0 +comment = "expression callable use contrasted with named def reuse" + +[[example_quality_scores]] +slug = "generators" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "yield-from" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "generator-expressions" +score = 9.0 +comment = "eager list contrast, lazy one-pass consumption, reducer use" + +[[example_quality_scores]] +slug = "itertools" +score = 9.0 +comment = "lazy standard-library pipeline and iterator-composition payoff" + +[[example_quality_scores]] +slug = "decorators" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "classes" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "inheritance-and-super" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "classmethods-and-staticmethods" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "dataclasses" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "properties" +score = 9.0 +comment = "managed attribute API without caller syntax change" + +[[example_quality_scores]] +slug = "special-methods" +score = 9.0 +comment = "criterion-level pass: graph-rich, note-heavy, multi-cell" + +[[example_quality_scores]] +slug = "truth-and-size" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "container-protocols" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "callable-objects" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "operator-overloading" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "attribute-access" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "bound-and-unbound-methods" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "descriptors" +score = 9.0 +comment = "descriptor lookup, __set_name__, and validation mechanics" + +[[example_quality_scores]] +slug = "metaclasses" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "context-managers" +score = 9.0 +comment = "criterion-level pass: graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "delete-statements" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "exceptions" +score = 9.0 +comment = "specific handling, else/finally cleanup, broad-catch boundary" + +[[example_quality_scores]] +slug = "assertions" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "exception-chaining" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "exception-groups" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "warnings" +score = 9.0 +comment = "capture and escalate soft failures with filters" + +[[example_quality_scores]] +slug = "modules" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "import-aliases" +score = 9.0 +comment = "criterion-level pass: graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "packages" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "virtual-environments" +score = 9.0 +comment = "standard venv workflow plus explicit Worker deployment boundary" + +[[example_quality_scores]] +slug = "type-hints" +score = 9.0 +comment = "criterion-level pass: graph-rich, note-heavy, multi-cell" + +[[example_quality_scores]] +slug = "runtime-type-checks" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "union-and-optional-types" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "type-aliases" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "typed-dicts" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "structured-data-shapes" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "literal-and-final" +score = 9.0 +comment = "Literal value set, Final rebinding promise, runtime caveat" + +[[example_quality_scores]] +slug = "callable-types" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "generics-and-typevar" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "paramspec" +score = 9.0 +comment = "Callable erasure contrasted with ParamSpec-preserving decorator" + +[[example_quality_scores]] +slug = "overloads" +score = 9.0 +comment = "static overload stubs contrasted with one runtime implementation" + +[[example_quality_scores]] +slug = "casts-and-any" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "newtype" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "protocols" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "abstract-base-classes" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "enums" +score = 9.0 +comment = "closed symbolic choice set contrasted with raw strings" + +[[example_quality_scores]] +slug = "regular-expressions" +score = 9.0 +comment = "criterion-level pass: graph-rich, note-heavy, multi-cell" + +[[example_quality_scores]] +slug = "number-parsing" +score = 9.0 +comment = "decimal, explicit base, and recoverable ValueError boundary" + +[[example_quality_scores]] +slug = "custom-exceptions" +score = 9.0 +comment = "domain error naming, raise point, recovery boundary" + +[[example_quality_scores]] +slug = "json" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "logging" +score = 9.0 +comment = "logger, handler, formatter, and threshold boundaries" + +[[example_quality_scores]] +slug = "testing" +score = 9.0 +comment = "criterion-level pass: graph-rich" + +[[example_quality_scores]] +slug = "subprocesses" +score = 9.0 +comment = "standard subprocess contract plus explicit Worker boundary" + +[[example_quality_scores]] +slug = "threads-and-processes" +score = 9.0 +comment = "thread/process executor contrast plus explicit Worker boundary" + +[[example_quality_scores]] +slug = "networking" +score = 9.0 +comment = "protocol byte boundary plus explicit socket runtime caveat" + +[[example_quality_scores]] +slug = "datetime" +score = 9.0 +comment = "date/time/delta parsing with timezone scope made explicit" + +[[example_quality_scores]] +slug = "csv-data" +score = 9.0 +comment = "DictReader, conversion, and DictWriter row boundary" + +[[example_quality_scores]] +slug = "async-await" +score = 9.0 +comment = "graph-rich, note-heavy" + +[[example_quality_scores]] +slug = "async-iteration-and-context" +score = 9.0 +comment = "criterion-level pass: graph-rich" diff --git a/docs/turnstile-runner-protection-spec.md b/docs/turnstile-runner-protection-spec.md index 1f0fdca..a900f67 100644 --- a/docs/turnstile-runner-protection-spec.md +++ b/docs/turnstile-runner-protection-spec.md @@ -94,6 +94,11 @@ If `TURNSTILE_CHALLENGE_MODE` is absent or `off`: - normal POSTs do not require Turnstile - a configured site/secret key alone does not slow every run +Any other value of `TURNSTILE_CHALLENGE_MODE` — including a typo such as +`sesion` — fails closed: with a secret key configured, the challenge is +required exactly as in `session` mode. A misconfigured mode string must +never silently disable protection. + If `TURNSTILE_CHALLENGE_MODE=session` and both site/secret keys are configured: 1. A browser without valid clearance posts edited code. diff --git a/docs/visual-explainer-spec.md b/docs/visual-explainer-spec.md index caa90a0..a5e1594 100644 --- a/docs/visual-explainer-spec.md +++ b/docs/visual-explainer-spec.md @@ -99,40 +99,33 @@ Captions are per-figure. font-style: italic; max-width: 44ch; } -.cell-banner--1 figure { max-width: 440px; } +.cell-banner--1 figure { max-width: clamp(280px, 65vw, 640px); } ``` 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 — figure between section heading and list 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. +grouped under section headings. The figure renders as a centered block +**between** the section heading and the example list — the same +single-column flow on every viewport: ```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; - } +.journey-section-figure { + margin: var(--space-4) auto; + width: 100%; + max-width: clamp(280px, 70vw, 640px); } ``` 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, -but production journeys are currently expected to have section figures. +scored against `docs/journey-visualisation-rubric.md`. Figures are +mapped by section title via `SECTION_FIGURES` in `src/marginalia.py`; +`render_for_section` returns empty for unmapped titles, but the +SectionFigureContract requires every production journey section to +have one. ### Why these two, not five @@ -202,9 +195,10 @@ 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. +Edit `docs/quality-registries.toml`. Add `[[figure_attachments]]` and +`[[example_figure_scores]]` entries. If the figure is new, add a paint +function (composed from grammar primitives) and register it in `FIGURES` in +`src/marginalia.py`. Done. ## Pipeline invariants (root-cause rules) diff --git a/public/_headers b/public/_headers index d75a1d8..6a9aba9 100644 --- a/public/_headers +++ b/public/_headers @@ -1,3 +1,10 @@ +/* + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + X-Frame-Options: DENY + Strict-Transport-Security: max-age=31536000; includeSubDomains; preload + Content-Security-Policy: default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; img-src 'self' data:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-pbe-inline-v1' https://esm.sh https://challenges.cloudflare.com 'wasm-unsafe-eval'; script-src-attr 'none'; connect-src 'self' https://esm.sh https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; worker-src 'self' + /site.*.css Cache-Control: public, max-age=31536000, immutable diff --git a/public/prototyping/journey-figures-gestalt.html b/public/prototyping/journey-figures-gestalt.html index db29316..4736f50 100644 --- a/public/prototyping/journey-figures-gestalt.html +++ b/public/prototyping/journey-figures-gestalt.html @@ -52,7 +52,7 @@

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.

PAGEsourceoutputrunevidence
Examples are evidence loops: source, a run step, and visible output stay together.

Separate value, identity, and absence.

object== valueis identityNone
Runtime objects answer separate questions: equal value, same identity, or the singleton that marks absence.

Read expressions as object operations.

syntaxDATA MODEL__add____len____format__result
Expression syntax enters the data model; object methods produce the result.

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.

facts?ifelifelse
Facts flow into one decision point; exactly one branch owns the next step.

Name and shape decisions.

inputname factmatch shape?
Name a fact when a condition needs it; match shape when the data structure is the decision.

Stop as soon as the answer is known.

abcdefirst trueanswer
Early exits draw a boundary: once the answer is found, the tail stays unread.

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.

stop ruleforexhaustedwhileconditionsentinelmarker
Choose the loop from its stopping rule: exhaustion, condition, or sentinel marker.

See the protocol behind `for`.

forITERABLExsiter()ITERATORitnext()abStopIteration
for is surface syntax; iter() creates an iterator and next() pulls values until StopIteration.

Compose lazy value streams.

sourcefiltermapvaluenext()no list
Lazy pipelines run from the consumer's pull: next() requests one value through each stage.

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.

f(2, 3)DEF Fbodyreturn 5
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.

RUNTIMEvaluerunsSTATIC TOOLSx: intchecks
Runtime values run the program; static tools inspect separate annotations and report before execution.

Describe realistic data shapes.

datafieldsTypedDictvariantUnionabsenceOptional
Real data contracts combine fields, variants, and expected absence instead of one scalar type.

Scale annotations for reusable libraries.

callerLIBRARY APITPoverloadstyped result
Reusable APIs carry caller contracts through the library boundary with generics, parameters, and overloads.

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.

problemassumeassertrecoverexceptcausechainsoftwarn
Different failure shapes need explicit signals: assertions, recovery, chained causes, or warnings.

Control resource and module boundaries.

codeBOUNDARIESresourcecleanupmoduleimportenvruntime
Reliable programs name their boundaries: resources clean up, modules import, environments constrain runtime.

Handle operations that outlive one expression.

callOPERATIONawaitthreadtestlogevidence
Async, threaded, test, and logging work cross an operation boundary before evidence comes back.
+

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.

Examples are evidence loops: source, a run step, and visible output stay together.

Separate value, identity, and absence.

Runtime objects answer separate questions: equal value, same identity, or the singleton that marks absence.

Read expressions as object operations.

Expression syntax enters the data model; object methods produce the result.

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.

Facts flow into one decision point; exactly one branch owns the next step.

Name and shape decisions.

Name a fact when a condition needs it; match shape when the data structure is the decision.

Stop as soon as the answer is known.

Early exits draw a boundary: once the answer is found, the tail stays unread.

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.

Choose the loop from its stopping rule: exhaustion, condition, or sentinel marker.

See the protocol behind `for`.

for is surface syntax; iter() creates an iterator and next() pulls values until StopIteration.

Compose lazy value streams.

Lazy pipelines run from the consumer's pull: next() requests one value through each stage.

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.

Each container answers a different question: ordered, fixed, lookup, unique.

Move between shapes deliberately.

Most everyday code reshapes data: one input, one transform, one new value.

Cross text and data boundaries.

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.

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

Use functions as values.

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

Bundle behavior with state.

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.

Runtime values run the program; static tools inspect separate annotations and report before execution.

Describe realistic data shapes.

Real data contracts combine fields, variants, and expected absence instead of one scalar type.

Scale annotations for reusable libraries.

Reusable APIs carry caller contracts through the library boundary with generics, parameters, and overloads.

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.

Different failure shapes need explicit signals: assertions, recovery, chained causes, or warnings.

Control resource and module boundaries.

Reliable programs name their boundaries: resources clean up, modules import, environments constrain runtime.

Handle operations that outlive one expression.

Async, threaded, test, and logging work cross an operation boundary before evidence comes back.
diff --git a/public/prototyping/layout-banner-pair.html b/public/prototyping/layout-banner-pair.html index 16b6436..db4b647 100644 --- a/public/prototyping/layout-banner-pair.html +++ b/public/prototyping/layout-banner-pair.html @@ -61,7 +61,7 @@

Mutability

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"
+['python', 'workers']
Two names share one mutable list — appending through one name changes the object visible through both.
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
diff --git a/public/prototyping/layout-banner-single.html b/public/prototyping/layout-banner-single.html
index d4a6eec..f1f1815 100644
--- a/public/prototyping/layout-banner-single.html
+++ b/public/prototyping/layout-banner-single.html
@@ -61,7 +61,7 @@ 

Mutability

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"
+['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
diff --git a/public/prototyping/layout-banner-trio.html b/public/prototyping/layout-banner-trio.html
index d277246..a0addd0 100644
--- a/public/prototyping/layout-banner-trio.html
+++ b/public/prototyping/layout-banner-trio.html
@@ -56,7 +56,7 @@
     

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"]
+  
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)
@@ -65,11 +65,11 @@ 

Mutability

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]
+PYTHON
Mutable: change visible through any alias.
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.
+[3, 1, 2]
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.
diff --git a/public/prototyping/marginalia-gestalt.html b/public/prototyping/marginalia-gestalt.html index ed49f89..2b3f657 100644 --- a/public/prototyping/marginalia-gestalt.html +++ b/public/prototyping/marginalia-gestalt.html @@ -38,6 +38,10 @@ } .card h3 { font-size: 15px; font-weight: 500; margin: 0; letter-spacing: -0.005em; } .card svg { margin-top: 8px; max-width: 100%; height: auto; overflow: visible; } + .card .caption { + margin: 6px 0 0; font-size: 12px; color: var(--ink); + max-width: 44ch; + } .card .note { margin: 6px 0 0; font-style: italic; font-size: 12px; color: var(--ink-soft); max-width: 38ch; @@ -63,655 +67,771 @@

Examples

Basics · 01

Hello World

- print("…")stdouthello world + +

Every Python program starts as source and produces text on standard output. The smallest mental model.

9.0 · program → output, smallest mechanism

Basics · 02

Values

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

Every literal is an object with a type; the type carries the behaviour, not the variable name.

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} + +

Each Python type has its own literal spellings; ints accept decimal, hex, and binary; strings accept either quote.

9.0 · literal spellings per type

Basics · 04

Numbers

- INT · UNBOUNDEDFLOAT · REPRESENTABLE SPACING WIDENS + +

Ints have unbounded precision; floats use IEEE doubles whose representable values thin out near the extremes.

9.0 · int unbounded vs float thinning, both registers

Basics · 05

Booleans

- A AND BTFTFTFFF + +

`a and b` returns True only when both are True; otherwise it returns the first falsy value.

9.0 · 2×2 truth table

Basics · 06

Operators

- *+423 + +

An expression like `(2 + 3) * 4` parses as a tree; operator precedence and parentheses determine its shape.

9.0 · expression tree mechanism

Basics · 07

None

- abcNONETYPENone + +

`None` is a single object: every name that points at None points at the same object.

9.0 · three names converging on one None

Basics · 08

Variables

- xINT42 + +

A name is a label that points at an object. Assignment binds the label; the object exists independently.

9.5 · the canonical name → object picture

Basics · 09

Constants

- xINT42 + +

UPPER_CASE is a naming convention, not a language constraint; the binding behaves like any other variable.

9.0 · name binding; UPPER_CASE is convention

Basics · 10

Truthiness

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

bool(x) is True except for a small fixed set: 0, 0.0, "", [], {}, None, False.

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] + +

Two names can share one object (`is` and `==` both true) or hold two equal-but-distinct objects (only `==` true).

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

Data Model · 12

Mutability

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

Two names share one mutable list — appending through one name changes the object visible through both.

9.5 · three-state small multiple of aliased mutation

Basics · 13

Object Lifecycle

- __init__live · refcount > 0__del__ -

9.0 · __init__ → live → __del__

+ +

Names keep the same object reachable; deleting one name matters only when it removes the last reference.

+

9.0 · names keep object reachable until last reference disappears

Text · 14

Strings

- CODEPOINTScaféUTF-8 BYTES636166c3a9 + +

Strings are sequences of Unicode codepoints. UTF-8 encoding turns them into bytes; `é` takes two bytes, `c` takes one.

9.0 · codepoints + bytes registers

-

Basics · 15

+

Text · 15

Bytes and Bytearray

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

bytes is a frozen sequence of integers; bytearray is the mutable counterpart with append/extend/etc.

9.0 · frozen vs mutable contrast

Text · 16

String Formatting

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

The format spec is a railroad of named optional fields: alignment, sign, width, precision, type.

9.0 · format-spec railroad

Control Flow · 17

Conditionals

- value?case Acase B + +

A predicate sorts a value into one of several branches; if/elif/else is the explicit spelling.

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 + +

Early returns handle the exceptional cases first so the main work is the body of the function, not its tail.

9.0 · early returns, main body at the tail

Control Flow · 19

Assignment Expressions

- len(xs):=nNAMEvaluen > 10 + +

The walrus binds a name during the surrounding expression; one expression, two outputs.

9.0 · walrus binds while comparing

Control Flow · 20

For Loops

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

Each call to next() advances the caret one cell along the iterable — the same shape behind range(), strings, and any sequence.

9.0 · 4-row caret advance

Control Flow · 21

Break and Continue

- abcdefound · breakfirst match + +

break exits the loop; continue skips to the next iteration. Both interrupt the natural fall-through.

9.0 · early exit at first match

Control Flow · 22

Loop Else

- loop bodyfell through · else runsbroke · else skipped + +

The loop's else branch runs only when the loop falls through naturally; break skips it.

9.0 · fell-through vs broke, two outcomes

Iteration · 23

Iterating over Iterables

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

`for` desugars to iter()+next(): one iter() call, then next() until StopIteration ends the loop.

9.0 · iter() exposes the iterator

Iteration · 24

Iterators

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

iter() exposes the iterator behind for; next() pulls one value at a time until exhausted.

9.0 · three-state machine

Iteration · 25

Iterator vs Iterable

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

An iterable knows how to produce an iterator (via iter()); the iterator knows how to produce values (via next()).

9.0 · the protocol exposed

Iteration · 26

Sentinel Iteration

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

`iter(callable, sentinel)` calls the callable repeatedly, stopping when it returns the sentinel.

9.0 · iter(callable, sentinel) stop condition

Control Flow · 27

Match Statements

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

match dispatches by pattern shape; the value flows down the patterns and the first match wins.

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=_) + +

Capture, alternative, guard, and class patterns each name a different way a value can match a case.

9.0 · four pattern variants

Control Flow · 29

While Loops

- abcdbody -

9.0 · back-edge mechanism

+ +

`while` repeats the body while the test stays true; the back-edge returns control to the test before each pass.

+

9.0 · back-edge mechanism plus the three stopping rules

+
+
+

Control Flow · 29

+

While Loops

+ +

Each loop shape has its own stopping rule: `for` ends when the iterable is exhausted, `while` when the condition turns false, sentinel iteration when the marker value appears.

+

9.0 · back-edge mechanism plus the three stopping rules

Collections · 30

Lists

- 314+1.append + +

Lists are mutable sequences. `.append` extends the same list object — no new list is created.

9.0 · cells with append mechanism

Collections · 31

Tuples

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

Tuples are ordered, immutable sequences; positions matter, contents do not change once constructed.

9.0 · frozen sequence with struck-through .append

Collections · 32

Unpacking

- 12345a*restb + +

Left-side names bind to right-side positions; `*rest` gathers the middle into a list.

9.0 · binding-line mechanism with *rest

Collections · 33

Dictionaries

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

Each key is hashed to a bucket; collisions chain into the next slot. Lookup is constant-time on average.

9.0 · hash buckets with collision chain

Collections · 34

Sets

- KEYS ONLYabcx in sO(1) + +

Sets are hash buckets without values; `x in s` averages O(1) regardless of size.

9.0 · hash buckets without values

Collections · 35

Slices

- abcdef0123456[:3][3:] + +

Slice indices sit between cells; [:3] and [3:] partition the sequence at index 3, never overlapping or losing an item.

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) + +

A comprehension is a compact spelling of the equivalent for-loop with append, made into one expression.

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) + +

Nested clauses compose left to right; the comprehension is still equivalent to a for-loop with append.

9.0 · nested clauses compose

Collections · 38

Sorting

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

Python's sort is stable: items with equal keys keep their original order, so chained sorts compose predictably.

9.0 · stability ribbons preserved across keys

Collections · 39

Collections Module

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

Four specialised containers for shapes the built-in types don't cover well: deque, Counter, defaultdict, namedtuple.

9.0 · deque / Counter / defaultdict / namedtuple

Collections · 40

Copying Collections

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

Without copy() two names share the same object; mutating through one is visible through the other.

9.5 · same picture as mutability, perfect match

Functions · 41

Functions

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

A function takes inputs, evaluates a body, and returns a value: `greet('Ada')` produces `'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 + +

A bare `*` divides positional or keyword arguments from keyword-only ones; callers must pass `c` and `d` by name.

9.0 · signature with explicit `*` separator

Functions · 43

Positional-only Parameters

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

A bare `/` divides positional-only arguments from positional-or-keyword ones; callers cannot name `a` or `b`.

9.0 · signature with explicit `/` separator

Functions · 44

Args and Kwargs

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

*args captures the extra positionals as a tuple; **kwargs captures the extra keywords as a dict.

9.0 · *args tuple, **kwargs dict regions

Functions · 45

Multiple Return Values

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

A function returning multiple values really returns one tuple; the caller unpacks it into named bindings.

9.0 · function returns tuple; caller unpacks

Functions · 46

Closures

- MAKE_MULTIPLIERCELLfactor=2MULTIPLYuses cell + +

The inner function keeps a reference into the outer scope's cell, so the captured factor survives the outer return.

9.0 · captured cell reference

Functions · 47

Partial Functions

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

`functools.partial(f, 1)` pre-fills `a=1`, returning a thinner callable `g(b, c)` that only needs the rest.

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

Functions · 48

Global and Nonlocal

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

Name lookup walks LEGB — local, enclosing, global, built-in — outward, returning the first binding it finds.

9.0 · LEGB nested rings

Functions · 49

Recursion

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

Each call pushes a new frame with the same name and a smaller argument; the base case unwinds back up the stack.

9.0 · stacked frames with same name, different argument

Functions · 50

Lambdas

- lambda x: x + 1paramsexpression + +

A lambda is a function literal: parameters before the colon, a single expression after, no statement body.

9.0 · function literal: params / expression

Iteration · 51

Generators

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

A generator's body is a timeline cut by yield gates: each next() advances to the next gate; locals survive the pause.

9.0 · ribbon cut by yield gates

Iteration · 52

Yield From

- OUTERyield from innerINNER + +

`yield from inner` delegates iteration to an inner generator; its yields surface here unchanged.

9.0 · stitched ribbons; delegation

Iteration · 53

Generator Expressions

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

A generator expression composes filter and map lazily; values flow only when next() pulls them.

9.0 · lazy filter→map pipeline

Iteration · 54

Itertools

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

chain stitches two iterables into one stream without materialising either: values arrive lazily.

9.0 · chain joins two iterables into one stream

Functions · 55

Decorators

- BEFOREfFNf₀AFTER @DECfwrapperf₀ + +

@dec rebinds the name to wrapper(f₀); the original function survives only in the wrapper's closure cell.

9.0 · before/after rebinding through cell

Classes · 56

Classes

- instanceCLASSClassTYPEtype + +

Every Python value sits on the instance → class → type triangle; the metaclass is the type of the class.

9.0 · instance/class/type triangle

Classes · 57

Inheritance and Super

- ABCDMRODBCAobject + +

Multiple inheritance forms a graph; C3 linearisation flattens it into the MRO Python uses for attribute lookup.

9.0 · MRO chain with diamond ghost

Classes · 58

Classmethods and Staticmethods

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

Three method kinds, three first-argument conventions: classmethod gets the class, staticmethod gets nothing, instance gets self.

9.0 · three method kinds, three first-arg conventions

Classes · 59

Dataclasses

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

Field declarations become the generated __init__ signature: declaration is the constructor.

9.0 · fields → generated __init__ signature

Classes · 60

Properties

- obj.xfget / fset__dict__ + +

When x is a property, attribute access routes through fget/fset instead of touching __dict__.

9.0 · obj.x routes through fget instead of __dict__

Data Model · 61

Special Methods

- a + bdispatchesa.__add__(b) + +

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

9.0 · syntax → method dispatch

Data Model · 62

Truth and Size

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

bool(x) calls __bool__ first; if absent, __len__() != 0; if neither, defaults to 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

+ +

Container syntax routes to narrow protocol methods: assignment to __setitem__, membership to __contains__, lookup to __getitem__.

+

9.0 · syntax routes to __setitem__, __contains__, __getitem__

Data Model · 64

Callable Objects

- OBJECT__call__obj(...) + +

Defining __call__ makes any object callable; functions are just one shape that satisfies this protocol.

9.0 · __call__ makes any object callable

Data Model · 65

Operator Overloading

- a + bdispatchesa.__add__(b) + +

Defining `__add__` on a class lets `+` dispatch into the class's own behavior.

9.0 · dispatch arrow

Data Model · 66

Attribute Access

- obj.xinstance __dict__class __dict____getattr__ + +

obj.x checks instance __dict__, then class __dict__, then __getattr__; the first hit wins.

9.0 · instance __dict__ → class __dict__ → __getattr__

Data Model · 67

Bound and Unbound Methods

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

Accessing a method via an instance binds self; accessing it via the class returns the underlying function.

9.0 · instance.method bound vs Class.method unbound

Data Model · 68

Descriptors

- obj.attrDESCRIPTOR__get____set____delete__ + +

Attribute access on an instance routes through the descriptor's __get__/__set__/__delete__ when the attribute is a descriptor.

9.0 · get/set/delete protocol routed through descriptor

Classes · 69

Metaclasses

- instanceCLASSClassMETACLASStype + +

A metaclass is the type of a class, just as a class is the type of its instances; type is the default metaclass.

9.0 · extended triangle to metaclass

Data Model · 70

Context Managers

- inbodyout + +

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

9.0 · enter / body / exit bowtie

Data Model · 71

Delete Statements

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

`del x` removes the name; the object survives if any other reference holds it, otherwise gets collected.

9.0 · name erased; object survives if referenced

Errors · 72

Exceptions

- TRYEXCEPTELSEFINALLY + +

try, except, else, and finally as parallel lanes; a single coral path traces what actually runs.

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

Errors · 73

Assertions

- assert condTrue · passFalse · AssertionError + +

assert tests a condition; True passes silently, False raises AssertionError with the optional message.

9.0 · True passes, False raises

Errors · 74

Exception Chaining

- ValueError__cause____context__RuntimeError + +

`raise X from Y` sets `__cause__` (explicit); raising during except sets `__context__` (implicit).

9.0 · __cause__ vs __context__ distinguished

Errors · 75

Exception Groups

- BEFOREexcept*AFTER + +

except* peels matched leaves out of an ExceptionGroup; survivors regroup and propagate.

9.0 · except* peels matching leaves

Errors · 76

Warnings

- code pathDeprecationWarningexecution continues + +

A warning is a soft signal: the message is reported, but execution continues unless filters elevate it.

9.0 · soft signal; execution continues

Modules · 77

Modules

- SYS.PATHcwdsite-packagesstdlibfirst hitmymod.py + +

An import walks sys.path entry by entry; the first directory containing the module wins.

9.0 · sys.path resolution; first hit wins

Modules · 78

Import Aliases

- import numpy as npnpnumpy module + +

`import x as y` binds the name y to the same module object x would have.

9.0 · two names bind to the same module

Modules · 79

Packages

- MYPACKAGE__init__.pya.pyb.pysub/ + +

A directory with __init__.py becomes an importable package; submodules and subpackages nest beneath it.

9.0 · __init__.py + nested submodules

Modules · 80

Virtual Environments

- PROJECTcoderequirementsVENVpythonsite-packages + +

A venv carries its own interpreter and site-packages, isolating a project's dependencies from the system.

9.0 · project / venv boundary

Types · 81

Type Hints

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

Annotations describe expected types for tools; the runtime accepts any object regardless.

9.0 · ghost annotations over runtime values

Types · 82

Runtime Type Checks

- isinstance(x, T)TrueFalse + +

isinstance and issubclass ask the runtime; the answer is a bool, not a static type refinement.

9.0 · isinstance returns bool

Types · 83

Union and Optional Types

- X: INT|STR|NONExintstrNone + +

`int | str | None` says one slot may hold any of three shapes — including expected absence.

9.0 · type fork to several shapes

Types · 84

Type Aliases

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

A type alias names a complex annotation once so call sites read as the domain meaning, not the type composition.

9.0 · complex annotation collapses to a name

Types · 85

TypedDict

- USER TYPEDDICTid: intname: stractive: bool + +

TypedDict gives each key a typed value, so `obj['x']` is checked against the declared shape.

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

+ +

The same record can be a mutable dataclass, an immutable NamedTuple, or a runtime dict described by TypedDict.

+

9.0 · dataclass vs NamedTuple vs TypedDict runtime trade-offs

Types · 87

Literal and Final

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

Literal narrows a slot to a fixed set of constant values; Final says the binding will not change.

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

+ +

A Callable annotation describes the callback slot: this argument list must produce this return type.

+

9.0 · callback slot shows argument list and return type

Types · 89

Generics and TypeVar

- TFN[T]T + +

A generic preserves the input type through the call: the same T flows in and out of fn[T].

9.0 · the same T flows in and out

Types · 90

ParamSpec

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

ParamSpec preserves the wrapped function's signature through a decorator, parameter for parameter.

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

+ +

@overload declares static call signatures for double(int) and double(str); one runtime implementation handles both.

+

9.0 · static overload signatures contrasted with one runtime body

Types · 92

Casts and Any

- Anycast(T, x)T + +

cast(T, x) tells the type checker to treat x as T; the runtime is unaffected.

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

Types · 93

NewType

- RUNTIME: INT42STATIC: USERIDUserId(42) + +

NewType creates a distinct static identity backed by the same runtime type — UserId is int with a name.

9.0 · same runtime, distinct static identity

Types · 94

Protocols

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

An object satisfies a protocol structurally — by having the required methods — not by inheriting it.

9.0 · structural duck check

Classes · 95

Abstract Base Classes

- instanceCLASSClassTYPEtype + +

An ABC sits on the same triangle as concrete classes; subclasses inherit the abstract methods they must implement.

9.0 · same triangle as concrete classes

Types · 96

Enums

- COLOR · CLOSED SETREDGREENBLUEno more + +

An enum names a fixed set of symbolic values; no new members appear at runtime.

9.0 · closed set of symbolic values

Text · 97

Regular Expressions

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

^ and $ anchor the pattern; quantifiers like {2} bound how many times a token repeats.

9.0 · pattern ruler with anchors

Standard Library · 98

Number Parsing

- "42"int()42ValueError + +

int() turns text into a typed number; malformed input raises ValueError instead of guessing.

9.0 · int() success path vs ValueError

Errors · 99

Custom Exceptions

- BaseExceptionExceptionValueErrorMyDomainError + +

Subclassing an existing exception gains a domain name without changing semantics.

9.0 · subclass chain to a domain name

Standard Library · 100

JSON

- JSONPYTHONobjectdictarrayliststringstrnumberint / floattrue / falseTrue / FalsenullNone + +

Six type pairs bridge the JSON text boundary; each json value maps to one Python type.

9.0 · two-column type mapping

Standard Library · 101

Logging

- CRITICAL50ERROR40WARNING30INFO20DEBUG10 + +

Five severity levels; the logger's configured threshold drops everything below it.

9.0 · five thresholded levels

Standard Library · 102

Testing

- arrangeset up stateactperform behaviorassertcompare result + +

arrange-act-assert: set up the state, perform the behavior under test, compare the result to expectations.

9.0 · arrange-act-assert three-row pattern

Standard Library · 103

Subprocesses

- parentspawnchild processoutput + +

subprocess.run spawns a child process and captures its stdout, stderr, and exit code as portable evidence.

9.0 · spawn → child → captured output

Standard Library · 104

Threads and Processes

- GILTHREAD ATHREAD B -

8.5 · GIL lanes; abstract concurrency model

+ +

Threads share memory but the GIL serialises Python bytecode; processes run in parallel with isolated memory.

+

9.0 · thread GIL sharing contrasted with process isolation

Standard Library · 105

Networking

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

Text crosses the socket as bytes — `encode` marks the python → wire boundary, `decode` brings the bytes back to a Python `str`.

9.0 · text ↔ bytes across the socket boundary

Standard Library · 106

Dates and Times

- one instant−5h+0h + +

An aware datetime carries a UTC offset; one instant in time reads differently on two clocks.

9.0 · one instant, two clock offsets

Standard Library · 107

CSV Data

- ROWS · RECORDSidnamescore1Ada972Bo883Cy76 + +

CSV files are rows of records; each line has the same columns in the same order.

9.0 · rows × columns; same shape per line

Async · 108

Async Await

- LOOPCOROawaitresume + +

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

9.0 · loop/coro swimlane with await handoffs

Async · 109

Async Iteration and Context

- LOOPCOROawaitresume + +

async iteration and async with both rest on the same loop-vs-coroutine handoff as await.

9.0 · loop/coro lanes with await yields

diff --git a/public/prototyping/production-figures-gestalt.html b/public/prototyping/production-figures-gestalt.html index 218644c..f1a8dd9 100644 --- a/public/prototyping/production-figures-gestalt.html +++ b/public/prototyping/production-figures-gestalt.html @@ -44,15 +44,15 @@ -
Prototype · All 123 figures currently registered in src/marginalia.py FIGURES; each card names where it renders. · all prototypes
+
Prototype · All 124 figures currently registered in src/marginalia.py FIGURES; each card names where it renders. · all prototypes
-

Production figure registry · 123 figures

+

Production figure registry · 124 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 · viewBox 232×100

v2 scores: conditionals 9.0

loop-repetition

foriterable exhaustedwhilecondition falsesentinelmarker returned
attached to /examples/while-loops · viewBox 260×100

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 · viewBox 304×70

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

program-output

print("…")stdouthello world
attached to /examples/hello-world · 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 · viewBox 304×96

v2 scores: equality-and-identity 9.0

operator-dispatch

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

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

runtime-evidence-loop

PAGEsourceoutputrunevidence
attached to a journey section · viewBox 306×96

runtime-object-axes

object== valueis identityNone
attached to a journey section · viewBox 214×104

runtime-expression-model

syntaxDATA MODEL__add____len____format__result
attached to a journey section · viewBox 344×96

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

f(2, 3)DEF Fbodyreturn 5
attached to a journey section · viewBox 310×82

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 · viewBox 220×52

v2 scores: type-hints 9.0

union-types

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

v2 scores: union-and-optional-types 9.0

generic-preservation

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

v2 scores: generics-and-typevar 9.0

type-runtime-static-split

RUNTIMEvaluerunsSTATIC TOOLSx: intchecks
attached to a journey section · viewBox 224×126

type-shape-catalog

datafieldsTypedDictvariantUnionabsenceOptional
attached to a journey section · viewBox 290×108

type-library-contract

callerLIBRARY APITPoverloadstyped result
attached to a journey section · viewBox 364×104

exception-lanes

TRYEXCEPTELSEFINALLY
attached to /examples/exceptions · viewBox 320×100

v2 scores: exceptions 9.0

context-bowtie

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

v2 scores: context-managers 9.0

async-swimlane

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

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

reliability-signal-map

problemassumeassertrecoverexceptcausechainsoftwarn
attached to a journey section · viewBox 270×128

reliability-boundary-map

codeBOUNDARIESresourcecleanupmoduleimportenvruntime
attached to a journey section · viewBox 286×110

reliability-operation-boundary

callOPERATIONawaitthreadtestlogevidence
attached to a journey section · viewBox 354×104

naming-decisions

NAME FACTlen(xs):=nn > 10DISPATCH SHAPEvaluecase intcase [x,y]case _
attached to /examples/assignment-expressions · viewBox 310×98

v2 scores: assignment-expressions 9.0

early-exit

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

v2 scores: break-and-continue 9.0

lazy-stream

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

v2 scores: generator-expressions 9.0

control-decision-map

facts?ifelifelse
attached to a journey section · viewBox 244×104

control-fact-shape

inputname factmatch shape?
attached to a journey section · viewBox 286×104

control-stop-boundary

abcdefirst trueanswer
attached to a journey section · viewBox 210×104

iteration-loop-selector

stop ruleforexhaustedwhileconditionsentinelmarker
attached to a journey section · viewBox 276×104

iteration-protocol-map

forITERABLExsiter()ITERATORitnext()abStopIteration
attached to a journey section · viewBox 306×132

iteration-lazy-pull

sourcefiltermapvaluenext()no list
attached to a journey section · viewBox 360×112

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

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

THREADS: SHARED INTERPRETERGILTHREAD ATHREAD BPROCESSES: ISOLATEDproc Aproc B
attached to /examples/threads-and-processes · viewBox 300×138

v2 scores: threads-and-processes 9.0

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

STATIC SIGNATURESdouble(int) -> intdouble(str) -> strRUNTIMEone double() body
attached to /examples/overloads · viewBox 304×64

v2 scores: overloads 9.0

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

CALLBACK SLOTCallable[[int, str], bool]int,strbool
attached to /examples/callable-types · viewBox 352×42

v2 scores: callable-types 9.0

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

container-methods

scores[k] = v__setitem__k in scores__contains__scores[k]__getitem__
attached to /examples/container-protocols · viewBox 272×82

v2 scores: container-protocols 9.0

typed-dict-shape

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

v2 scores: typed-dicts 9.0

structured-shapes

dataclassmutable attrsNamedTupletuple + namesTypedDictdict at runtime
attached to /examples/structured-data-shapes · viewBox 266×82

v2 scores: 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

boxaliasBOXlabel='draft'last ref?
attached to /examples/object-lifecycle · viewBox 366×80

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

+

aliasing-mutation

attached to /examples/mutability, copying-collections · viewBox 220×175

v2 scores: mutability 9.5 · copying-collections 9.5

tuple-no-mutation

registered, not yet attached · viewBox 220×185

iterator-unroll

attached to /examples/for-loops · viewBox 220×130

v2 scores: for-loops 9.0

scope-rings

attached to /examples/scope-global-nonlocal · viewBox 216×116

v2 scores: scope-global-nonlocal 9.0

closure-cell

attached to /examples/closures · viewBox 240×120

v2 scores: closures 9.0

slice-ruler

attached to /examples/slices · viewBox 232×120

v2 scores: slices 9.0

branch-fork

attached to /examples/conditionals · viewBox 232×100

v2 scores: conditionals 9.0

loop-repetition

attached to /examples/while-loops · viewBox 260×100

v2 scores: while-loops 9.0

while-backedge

attached to /examples/while-loops · viewBox 252×80

v2 scores: while-loops 9.0

iter-protocol

attached to /examples/iterators, iterator-vs-iterable, iterating-over-iterables · viewBox 304×70

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

program-output

attached to /examples/hello-world · viewBox 240×80

v2 scores: hello-world 9.0

identity-and-equality

attached to /examples/equality-and-identity · viewBox 304×96

v2 scores: equality-and-identity 9.0

operator-dispatch

attached to /examples/special-methods, operator-overloading · viewBox 260×70

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

runtime-evidence-loop

attached to a journey section · viewBox 306×96

runtime-object-axes

attached to a journey section · viewBox 214×104

runtime-expression-model

attached to a journey section · viewBox 344×96

container-questions

attached to a journey section · viewBox 280×88

reshape-pipeline

attached to a journey section · viewBox 204×80

text-data-boundary

attached to a journey section · viewBox 172×70

function-signature

attached to a journey section · viewBox 310×82

function-as-value

attached to a journey section · viewBox 200×66

class-with-state

attached to a journey section · viewBox 152×108

annotation-ghost

attached to /examples/type-hints · viewBox 220×52

v2 scores: type-hints 9.0

union-types

attached to /examples/union-and-optional-types · viewBox 166×80

v2 scores: union-and-optional-types 9.0

generic-preservation

attached to /examples/generics-and-typevar · viewBox 250×70

v2 scores: generics-and-typevar 9.0

type-runtime-static-split

attached to a journey section · viewBox 224×126

type-shape-catalog

attached to a journey section · viewBox 290×108

type-library-contract

attached to a journey section · viewBox 364×104

exception-lanes

attached to /examples/exceptions · viewBox 320×100

v2 scores: exceptions 9.0

context-bowtie

attached to /examples/context-managers · viewBox 244×76

v2 scores: context-managers 9.0

async-swimlane

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

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

reliability-signal-map

attached to a journey section · viewBox 270×128

reliability-boundary-map

attached to a journey section · viewBox 286×110

reliability-operation-boundary

attached to a journey section · viewBox 354×104

naming-decisions

attached to /examples/assignment-expressions · viewBox 310×98

v2 scores: assignment-expressions 9.0

early-exit

attached to /examples/break-and-continue · viewBox 144×116

v2 scores: break-and-continue 9.0

lazy-stream

attached to /examples/generator-expressions · viewBox 300×56

v2 scores: generator-expressions 9.0

control-decision-map

attached to a journey section · viewBox 244×104

control-fact-shape

attached to a journey section · viewBox 286×104

control-stop-boundary

attached to a journey section · viewBox 210×104

iteration-loop-selector

attached to a journey section · viewBox 276×104

iteration-protocol-map

attached to a journey section · viewBox 306×132

iteration-lazy-pull

attached to a journey section · viewBox 360×112

variables-bind

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

v2 scores: variables 9.5 · constants 9.0

call-stack

attached to /examples/recursion · viewBox 200×100

v2 scores: recursion 9.0

decorator-rebind

attached to /examples/decorators · viewBox 232×110

v2 scores: decorators 9.0

mro-chain

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

v2 scores: inheritance-and-super 9.0

dataclass-fields

attached to /examples/dataclasses · viewBox 312×76

v2 scores: dataclasses 9.0

class-triangle

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

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

exception-cause-context

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

v2 scores: exception-chaining 9.0

unpacking-bind

attached to /examples/unpacking · viewBox 152×80

v2 scores: unpacking 9.0

comprehension-equivalence

attached to /examples/comprehensions, comprehension-patterns · viewBox 280×76

v2 scores: comprehensions 9.0 · comprehension-patterns 9.0

list-append

attached to /examples/lists · viewBox 220×36

v2 scores: lists 9.0

dict-buckets

attached to /examples/dicts · viewBox 270×88

v2 scores: dicts 9.0

number-lines

attached to /examples/numbers · viewBox 260×78

v2 scores: numbers 9.0

expression-tree

attached to /examples/operators · viewBox 220×92

v2 scores: operators 9.0

none-singleton

attached to /examples/none · viewBox 240×84

v2 scores: none 9.0

codepoints-bytes

attached to /examples/strings · viewBox 200×84

v2 scores: strings 9.0

sort-stability

attached to /examples/sorting · viewBox 270×100

v2 scores: sorting 9.0

kw-only-separator

attached to /examples/keyword-only-arguments · viewBox 200×56

v2 scores: keyword-only-arguments 9.0

positional-only-separator

attached to /examples/positional-only-parameters · viewBox 200×56

v2 scores: positional-only-parameters 9.0

generator-ribbon

attached to /examples/generators · viewBox 260×50

v2 scores: generators 9.0

truth-and-size

attached to /examples/truth-and-size · viewBox 232×70

v2 scores: truth-and-size 9.0

descriptor-protocol

attached to /examples/descriptors · viewBox 222×76

v2 scores: descriptors 9.0

bound-unbound

attached to /examples/bound-and-unbound-methods · viewBox 296×56

v2 scores: bound-and-unbound-methods 9.0

method-kinds

attached to /examples/classmethods-and-staticmethods · viewBox 272×70

v2 scores: classmethods-and-staticmethods 9.0

callable-objects

attached to /examples/callable-objects · viewBox 220×44

v2 scores: callable-objects 9.0

attribute-lookup

attached to /examples/attribute-access · viewBox 242×70

v2 scores: attribute-access 9.0

guard-clauses

attached to /examples/guard-clauses · viewBox 264×104

v2 scores: guard-clauses 9.0

bytes-vs-bytearray

attached to /examples/bytes-and-bytearray · viewBox 308×86

v2 scores: bytes-and-bytearray 9.0

sentinel-iteration

attached to /examples/sentinel-iteration · viewBox 320×92

v2 scores: sentinel-iteration 9.0

partial-functions

attached to /examples/partial-functions · viewBox 334×36

v2 scores: partial-functions 9.0

args-kwargs

attached to /examples/args-and-kwargs · viewBox 280×68

v2 scores: args-and-kwargs 9.0

multiple-return

attached to /examples/multiple-return-values · viewBox 180×110

v2 scores: multiple-return-values 9.0

lambda-expression

attached to /examples/lambdas · viewBox 170×76

v2 scores: lambdas 9.0

property-fork

attached to /examples/properties · viewBox 232×72

v2 scores: properties 9.0

metaclass-triangle

attached to /examples/metaclasses · viewBox 300×60

v2 scores: metaclasses 9.0

sys-path-resolution

attached to /examples/modules · viewBox 258×100

v2 scores: modules 9.0

import-alias

attached to /examples/import-aliases · viewBox 212×56

v2 scores: import-aliases 9.0

protocol-check

attached to /examples/protocols · viewBox 220×78

v2 scores: protocols 9.0

enum-members

attached to /examples/enums · viewBox 280×60

v2 scores: enums 9.0

datetime-instant

attached to /examples/datetime · viewBox 280×88

v2 scores: datetime 9.0

json-python-mapping

attached to /examples/json · viewBox 220×116

v2 scores: json 9.0

regex-anchors

attached to /examples/regular-expressions · viewBox 200×92

v2 scores: regular-expressions 9.0

number-parse

attached to /examples/number-parsing · viewBox 204×64

v2 scores: number-parsing 9.0

format-spec

attached to /examples/string-formatting · viewBox 220×64

v2 scores: string-formatting 9.0

truthy-check

attached to /examples/truthiness · viewBox 240×70

v2 scores: truthiness 9.0

boolean-truth-table

attached to /examples/booleans · viewBox 132×64

v2 scores: booleans 9.0

set-buckets

attached to /examples/sets · viewBox 156×90

v2 scores: sets 9.0

tuple-frozen

attached to /examples/tuples · viewBox 280×48

v2 scores: tuples 9.0

value-types

attached to /examples/values · viewBox 160×116

v2 scores: values 9.0

yield-delegation

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

v2 scores: yield-from 9.0

itertools-chain

attached to /examples/itertools · viewBox 246×82

v2 scores: itertools 9.0

assertion-check

attached to /examples/assertions · viewBox 304×76

v2 scores: assertions 9.0

custom-exception-chain

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

v2 scores: custom-exceptions 9.0

exception-group-peel

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

v2 scores: exception-groups 9.0

delete-name-erased

attached to /examples/delete-statements · viewBox 200×84

v2 scores: delete-statements 9.0

package-tree

attached to /examples/packages · viewBox 240×76

v2 scores: packages 9.0

venv-boundary

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

v2 scores: virtual-environments 9.0

subprocess-spawn

attached to /examples/subprocesses · viewBox 324×60

v2 scores: subprocesses 9.0

logging-levels

attached to /examples/logging · viewBox 164×124

v2 scores: logging 9.0

aaa-pattern

attached to /examples/testing · viewBox 250×80

v2 scores: testing 9.0

socket-byte-boundary

attached to /examples/networking · viewBox 364×46

v2 scores: networking 9.0

gil-lanes

attached to /examples/threads-and-processes · viewBox 300×138

v2 scores: threads-and-processes 9.0

cast-escape

attached to /examples/casts-and-any · viewBox 184×56

v2 scores: casts-and-any 9.0

newtype-phantom

attached to /examples/newtype · viewBox 96×92

v2 scores: newtype 9.0

overload-signatures

attached to /examples/overloads · viewBox 304×64

v2 scores: overloads 9.0

paramspec-preserve

attached to /examples/paramspec · viewBox 294×60

v2 scores: paramspec 9.0

literal-constrained

attached to /examples/literal-and-final · viewBox 144×76

v2 scores: literal-and-final 9.0

callable-type

attached to /examples/callable-types · viewBox 352×42

v2 scores: callable-types 9.0

isinstance-check

attached to /examples/runtime-type-checks · viewBox 232×76

v2 scores: runtime-type-checks 9.0

collections-containers

attached to /examples/collections-module · viewBox 284×92

v2 scores: collections-module 9.0

container-methods

attached to /examples/container-protocols · viewBox 272×82

v2 scores: container-protocols 9.0

typed-dict-shape

attached to /examples/typed-dicts · viewBox 200×92

v2 scores: typed-dicts 9.0

structured-shapes

attached to /examples/structured-data-shapes · viewBox 266×82

v2 scores: structured-data-shapes 9.0

csv-records

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

v2 scores: csv-data 9.0

warning-signal

attached to /examples/warnings · viewBox 292×80

v2 scores: warnings 9.0

object-lifecycle

attached to /examples/object-lifecycle · viewBox 366×80

v2 scores: object-lifecycle 9.0

type-alias-name

attached to /examples/type-aliases · viewBox 240×104

v2 scores: type-aliases 9.0

match-dispatch-ladder

attached to /examples/match-statements · viewBox 260×130

v2 scores: match-statements 9.0

match-pattern-variants

attached to /examples/advanced-match-patterns · viewBox 272×96

v2 scores: advanced-match-patterns 9.0

loop-else-gate

attached to /examples/loop-else · viewBox 312×76

v2 scores: loop-else 9.0

literal-forms

attached to /examples/literals · viewBox 252×132

v2 scores: literals 9.0

function-with-body

attached to /examples/functions · viewBox 334×68

v2 scores: functions 9.0

diff --git a/public/site.e87d4baf77e6.css b/public/site.0c2adaa2c94a.css similarity index 91% rename from public/site.e87d4baf77e6.css rename to public/site.0c2adaa2c94a.css index 1e24c4b..385536c 100644 --- a/public/site.e87d4baf77e6.css +++ b/public/site.0c2adaa2c94a.css @@ -1,11 +1,11 @@ -: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; } +:root { color-scheme: light; --accent: #FF4801; --accent-text: #C83800; --accent-action: #C83800; --accent-action-hover: #A22C00; --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); } header { position: sticky; top: 0; z-index: 2; margin: 0 calc(-1 * var(--space-4)) var(--space-5); padding: var(--space-2) var(--space-4); backdrop-filter: blur(16px); background: rgba(245, 241, 235, 0.82); } nav { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } nav a { color: inherit; text-decoration: underline; text-decoration-color: var(--hairline); text-underline-offset: .22em; transition-property: color, text-decoration-color; transition-duration: 160ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); } - nav a:hover { color: var(--accent); text-decoration-color: var(--accent); } + nav a:hover { color: var(--accent-text); 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, 4.5vw, 3.75rem); margin: 0 0 1rem; } @@ -65,12 +65,12 @@ .card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); } .card h2 { text-decoration: underline; text-decoration-color: var(--hairline); text-underline-offset: .18em; } .card:hover h2 { text-decoration-color: var(--accent); } - .button { min-height: 40px; border: 1px solid var(--accent); border-radius: 9999px; padding: .72rem 1rem; background: var(--accent); color: white; cursor: pointer; font-weight: 650; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, opacity, border-style; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94); } - .button:hover { opacity: .95; border-color: var(--accent-hover); } + .button { min-height: 40px; border: 1px solid var(--accent-action); border-radius: 9999px; padding: .72rem 1rem; background: var(--accent-action); color: white; cursor: pointer; font-weight: 650; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94); } + .button:hover { background: var(--accent-action-hover); border-color: var(--accent-action-hover); } .meta { color: var(--muted); font-variant-numeric: tabular-nums; } - .eyebrow { margin: 0 0 var(--space-2); color: var(--accent); font-size: .78rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } + .eyebrow { margin: 0 0 var(--space-2); color: var(--accent-text); font-size: .78rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } .text-link { color: var(--text); text-decoration: underline; text-decoration-color: var(--accent); text-underline-offset: .22em; } - .text-link:hover { color: var(--accent); } + .text-link:hover { color: var(--accent-text); } .example-shell { padding: clamp(var(--space-3), 3vw, var(--space-5)) 0; } .example-intro { max-width: 68ch; margin-bottom: var(--space-5); } .literate-program { margin: var(--space-5) 0; border-block: 1px solid var(--hairline); } @@ -91,7 +91,7 @@ .lp-cells { border-block: 1px solid var(--hairline); } .lp-cell { display: grid; grid-template-columns: minmax(17rem, .85fr) minmax(0, 1fr); gap: var(--space-4); align-items: start; padding: var(--space-4) 0; border-top: 1px dashed var(--hairline-soft); } .lp-cell:first-child { border-top: 0; } - .cell-label { margin: 0 0 var(--space-2); color: var(--accent); font-size: .74rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } + .cell-label { margin: 0 0 var(--space-2); color: var(--accent-text); font-size: .74rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } .lp-prose p { margin: 0 0 var(--space-2); color: var(--muted); } .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); } diff --git a/public/site.css b/public/site.css index 1e24c4b..385536c 100644 --- a/public/site.css +++ b/public/site.css @@ -1,11 +1,11 @@ -: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; } +:root { color-scheme: light; --accent: #FF4801; --accent-text: #C83800; --accent-action: #C83800; --accent-action-hover: #A22C00; --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); } header { position: sticky; top: 0; z-index: 2; margin: 0 calc(-1 * var(--space-4)) var(--space-5); padding: var(--space-2) var(--space-4); backdrop-filter: blur(16px); background: rgba(245, 241, 235, 0.82); } nav { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } nav a { color: inherit; text-decoration: underline; text-decoration-color: var(--hairline); text-underline-offset: .22em; transition-property: color, text-decoration-color; transition-duration: 160ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); } - nav a:hover { color: var(--accent); text-decoration-color: var(--accent); } + nav a:hover { color: var(--accent-text); 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, 4.5vw, 3.75rem); margin: 0 0 1rem; } @@ -65,12 +65,12 @@ .card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); } .card h2 { text-decoration: underline; text-decoration-color: var(--hairline); text-underline-offset: .18em; } .card:hover h2 { text-decoration-color: var(--accent); } - .button { min-height: 40px; border: 1px solid var(--accent); border-radius: 9999px; padding: .72rem 1rem; background: var(--accent); color: white; cursor: pointer; font-weight: 650; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, opacity, border-style; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94); } - .button:hover { opacity: .95; border-color: var(--accent-hover); } + .button { min-height: 40px; border: 1px solid var(--accent-action); border-radius: 9999px; padding: .72rem 1rem; background: var(--accent-action); color: white; cursor: pointer; font-weight: 650; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94); } + .button:hover { background: var(--accent-action-hover); border-color: var(--accent-action-hover); } .meta { color: var(--muted); font-variant-numeric: tabular-nums; } - .eyebrow { margin: 0 0 var(--space-2); color: var(--accent); font-size: .78rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } + .eyebrow { margin: 0 0 var(--space-2); color: var(--accent-text); font-size: .78rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } .text-link { color: var(--text); text-decoration: underline; text-decoration-color: var(--accent); text-underline-offset: .22em; } - .text-link:hover { color: var(--accent); } + .text-link:hover { color: var(--accent-text); } .example-shell { padding: clamp(var(--space-3), 3vw, var(--space-5)) 0; } .example-intro { max-width: 68ch; margin-bottom: var(--space-5); } .literate-program { margin: var(--space-5) 0; border-block: 1px solid var(--hairline); } @@ -91,7 +91,7 @@ .lp-cells { border-block: 1px solid var(--hairline); } .lp-cell { display: grid; grid-template-columns: minmax(17rem, .85fr) minmax(0, 1fr); gap: var(--space-4); align-items: start; padding: var(--space-4) 0; border-top: 1px dashed var(--hairline-soft); } .lp-cell:first-child { border-top: 0; } - .cell-label { margin: 0 0 var(--space-2); color: var(--accent); font-size: .74rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } + .cell-label { margin: 0 0 var(--space-2); color: var(--accent-text); font-size: .74rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } .lp-prose p { margin: 0 0 var(--space-2); color: var(--muted); } .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); } diff --git a/scripts/_common.py b/scripts/_common.py new file mode 100644 index 0000000..ee99da3 --- /dev/null +++ b/scripts/_common.py @@ -0,0 +1,49 @@ +"""Shared plumbing for the check scripts. + +Every script needs the repo root on sys.path, the canonical example +catalog, and often the quality registry or a page's frontmatter. Keeping +those in one module stops the three frontmatter parsers and a dozen +copies of the sys.path dance from drifting apart. +""" +from __future__ import annotations + +import sys +import tomllib +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +EXAMPLES_DIR = ROOT / "src" / "example_sources" +REGISTRY_PATH = ROOT / "docs" / "quality-registries.toml" + +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + + +def load_catalog(): + """Return (catalog, examples) through the canonical website loader.""" + from src.example_loader import load_examples + + return load_examples() + + +def load_registry() -> dict[str, Any]: + return tomllib.loads(REGISTRY_PATH.read_text()) + + +def frontmatter(path: Path) -> dict[str, Any]: + """Parse a page's TOML frontmatter with the canonical loader's splitter.""" + from src.example_loader import _split_frontmatter + + data, _, _ = _split_frontmatter(path.read_text(), path.name) + return data + + +def fail(errors: list[str], ok_message: str) -> int: + """Standard exit protocol: errors to stderr and 1, or ok line and 0.""" + if errors: + for error in errors: + print(error, file=sys.stderr) + return 1 + print(ok_message) + return 0 diff --git a/scripts/audit_example_graph.py b/scripts/audit_example_graph.py index 8ad17fc..1a8d6c5 100755 --- a/scripts/audit_example_graph.py +++ b/scripts/audit_example_graph.py @@ -5,26 +5,28 @@ import argparse import sys from collections import Counter -from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) - -from src.examples import EXAMPLES # noqa: E402 +from _common import load_catalog def main() -> int: parser = argparse.ArgumentParser() - parser.add_argument("--check", action="store_true", help="fail on invalid graph links") + parser.add_argument( + "--check", + action="store_true", + help="additionally fail on orphaned examples (invalid links, self-links, " + "and out-degree violations always fail)", + ) parser.add_argument("--max-out-degree", type=int, default=4) args = parser.parse_args() - slugs = {example["slug"] for example in EXAMPLES} + _catalog, examples = load_catalog() + slugs = {example["slug"] for example in examples} incoming: Counter[str] = Counter() outgoing: dict[str, list[str]] = {} errors: list[str] = [] - for example in EXAMPLES: + for example in examples: slug = example["slug"] links = list(example.get("see_also", [])) outgoing[slug] = links @@ -48,7 +50,7 @@ def main() -> int: reciprocal.append(tuple(sorted((source, target)))) reciprocal = sorted(set(reciprocal)) - print(f"examples={len(EXAMPLES)}") + print(f"examples={len(examples)}") print(f"linked_sources={len(linked)}") print(f"edges={sum(len(links) for links in outgoing.values())}") print(f"orphaned={len(orphaned)}") diff --git a/scripts/audit_rubric_snapshot.py b/scripts/audit_rubric_snapshot.py index 84e3388..30117f2 100755 --- a/scripts/audit_rubric_snapshot.py +++ b/scripts/audit_rubric_snapshot.py @@ -8,22 +8,16 @@ from __future__ import annotations import argparse -import sys -import tomllib +import datetime from collections import Counter from pathlib import Path from statistics import mean, median from typing import Any -ROOT = Path(__file__).resolve().parents[1] -REGISTRY_PATH = ROOT / "docs" / "quality-registries.toml" -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from scripts.score_example_criteria import criterion_scores, weighted_score # noqa: E402 -from src.app import JOURNEYS # noqa: E402 -from src.example_loader import load_examples # noqa: E402 -from src.marginalia import ( # noqa: E402 +from _common import load_catalog, load_registry +from scripts.score_example_criteria import criterion_scores, weighted_score +from src.app import JOURNEYS +from src.marginalia import ( ATTACHMENTS, EXAMPLE_QUALITY_SCORES, SCORES, @@ -147,50 +141,59 @@ def journey_rows(registry: Registry) -> list[dict[str, Any]]: def dimension_ledgers( reused_sections: list[dict[str, str]], ) -> tuple[list[tuple[str, str, str]], list[tuple[str, str, str]], list[tuple[str, str, str]]]: + """Each dimension's Result names HOW it is assured, not a bare PASS. + + GATED means a named script or test fails the build when the + dimension regresses — this report does not re-run those gates, so a + GATED row is only as current as the last `make verify`. CURATED + means the dimension rests on editorial judgement recorded in the + score registries; an audit pass must re-affirm those by review + rather than trusting this snapshot. + """ examples = [ - ("Conceptual payoff", "Curated score registry + criterion search", "PASS"), - ("Rationale", "Criterion search + notes-supported gate", "PASS"), - ("Alternatives and boundaries", "Confusable pairs, footguns, broad tours, graph edges", "PASS"), - ("Executable determinism", "`verify_examples.py` over every program and cell", "PASS"), - ("Python idiom and accuracy", "Python 3.13 verification, docs links, Ruff", "PASS"), - ("Literate fit", "Markdown parser requires prose beside every cell", "PASS"), - ("Source/result pairing", "Every runnable cell carries expected output", "PASS"), - ("Concept decomposition", "Criterion search + curated score floor", "PASS"), - ("Progressive walkthrough", "Cell sequence review + criterion search", "PASS"), - ("Representative coverage", "Broad-surface and syntax-surface gates", "PASS"), - ("Practical usefulness", "Criterion search + editorial score comments", "PASS"), - ("Editorial progression", "Quality registry + graph audit", "PASS"), + ("Conceptual payoff", "Curated score registry + criterion search", "CURATED (re-affirm by review)"), + ("Rationale", "Criterion search + notes-supported gate", "GATED by check_notes_supported"), + ("Alternatives and boundaries", "Confusable pairs, footguns, broad tours, graph edges", "GATED by quality-checks"), + ("Executable determinism", "`verify_examples.py` over every program and cell", "GATED by verify-examples"), + ("Python idiom and accuracy", "Python 3.13 verification, docs links, Ruff", "GATED by verify-examples + lint"), + ("Literate fit", "Markdown parser requires prose beside every cell", "GATED by the loader"), + ("Source/result pairing", "Every runnable cell carries expected output", "GATED by verify-examples"), + ("Concept decomposition", "Criterion search + curated score floor", "CURATED (re-affirm by review)"), + ("Progressive walkthrough", "Cell sequence review + criterion search", "CURATED (re-affirm by review)"), + ("Representative coverage", "Broad-surface and syntax-surface gates", "GATED by check_broad_surface_tours"), + ("Practical usefulness", "Criterion search + editorial score comments", "CURATED (re-affirm by review)"), + ("Editorial progression", "Quality registry + graph audit", "GATED by audit_example_graph"), ] figures = [ - ("Cell fidelity", "Attachment anchor resolves to a real teaching cell", "PASS"), - ("Earns its place", "Curated figure scores; all example figures >= 9.0", "PASS"), - ("One conceptual move", "Figure score rationale + emphasis-scarcity contract", "PASS"), - ("Mechanism over metaphor", "Canvas grammar uses bindings, arrows, cells, lanes, boundaries", "PASS"), - ("Caption quality", "Unique declarative figcaptions", "PASS"), - ("Grammar conformance", "Palette, font, and stroke contracts", "PASS"), - ("Emphasis scarcity", "At most one accent mark per figure", "PASS"), - ("Restraint", "Locked primitive grammar; no bespoke SVG", "PASS"), - ("Banner-row fit", "Intrinsic-width contract", "PASS"), - ("Pairs with surrounding cell", "Anchor/cell semantic pass + score registry", "PASS"), + ("Cell fidelity", "Attachment anchor resolves to a real rendered cell", "GATED by FigureAnchorContract"), + ("Earns its place", "Curated figure scores", "CURATED (re-affirm by review)"), + ("One conceptual move", "Figure score rationale + emphasis-scarcity contract", "CURATED + accent contract"), + ("Mechanism over metaphor", "Canvas grammar uses bindings, arrows, cells, lanes, boundaries", "CURATED (re-affirm by review)"), + ("Caption quality", "Unique, complete-sentence figcaptions", "GATED by FigureCaptionContract"), + ("Grammar conformance", "Palette, font, and stroke contracts", "GATED by geometry contracts"), + ("Emphasis scarcity", "At most one accent mark per figure", "GATED by FigureEmphasisScarcityContract"), + ("Restraint", "Locked primitive grammar; no bespoke SVG", "GATED by the grammar's surface"), + ("Banner-row fit", "Intrinsic-width contract", "GATED by FigureSizeContract"), + ("Pairs with surrounding cell", "Caption/figure agreement on the marginalia gestalt", "CURATED (re-affirm by review)"), ] independence_result = ( - "PASS" if not reused_sections else f"WATCH ({len(reused_sections)} reused paint functions)" + "computed: no reuse" if not reused_sections else f"WATCH ({len(reused_sections)} reused paint functions)" ) journeys = [ - ("Section fidelity", "Section figure keyed by journey section title", "PASS"), - ("Pedagogical scope", "Section-level captions and score rationales", "PASS"), - ("One conceptual move", "One figure per section and one emphasis mark", "PASS"), - ("Mechanism over metaphor", "Canvas primitives show protocols, boundaries, lanes, or flows", "PASS"), - ("Caption alignment", "Section figure captions unique and declarative", "PASS"), - ("Grammar conformance", "Shared geometry contracts", "PASS"), + ("Section fidelity", "Section figure keyed by journey section title", "GATED by SectionFigureContract"), + ("Pedagogical scope", "Section-level captions and score rationales", "CURATED (re-affirm by review)"), + ("One conceptual move", "One figure per section and one emphasis mark", "GATED + CURATED"), + ("Mechanism over metaphor", "Canvas primitives show protocols, boundaries, lanes, or flows", "CURATED (re-affirm by review)"), + ("Caption alignment", "Section figure captions unique and declarative", "GATED by FigureCaptionContract"), + ("Grammar conformance", "Shared geometry contracts", "GATED by geometry contracts"), ( "Independence from lesson figures", "Section figures compared with example attachments", independence_result, ), - ("Layout fit", "Journey figure dimensions within production column", "PASS"), - ("Outcome support", "`check_journey_outcomes.py` over all journey sections", "PASS"), - ("Prerequisite order", "Journey order reviewed against lesson dependencies", "PASS"), + ("Layout fit", "Journey figure dimensions within production column", "GATED by FigureSizeContract"), + ("Outcome support", "`check_journey_outcomes.py` over all journey sections", "GATED by check_journey_outcomes"), + ("Prerequisite order", "Journey order reviewed against lesson dependencies", "CURATED (re-affirm by review)"), ] return examples, figures, journeys @@ -204,8 +207,8 @@ def render_table(headers: list[str], rows: list[list[object]]) -> list[str]: def render_report(date: str) -> str: - registry = tomllib.loads(REGISTRY_PATH.read_text()) - _catalog, examples = load_examples() + registry = load_registry() + _catalog, examples = load_catalog() example_scores = [score for score, _comment in EXAMPLE_QUALITY_SCORES.values()] figure_scores = [score for score, _comment in SCORES.values()] journey_scores = [score for score, _comment in SECTION_FIGURE_SCORES.values()] @@ -215,20 +218,28 @@ def render_report(date: str) -> str: rows = example_rows(examples, registry) section_rows = journey_rows(registry) + waivers = registry.get("quality_waivers", {}) + waiver_lines = [ + f"- Accepted waiver: `{slug}` at " + f"{EXAMPLE_QUALITY_SCORES.get(slug, (0.0, ''))[0]:g} " + f"(floor {entry.get('accepted_min', '?')}, expires {entry.get('expires', '?')})." + for slug, entry in sorted(waivers.items()) + ] or ["- Accepted waivers: none."] + lines = [ f"# Rubric audit snapshot — {date}", "", - "This snapshot audits the shipped catalog against the example, example-figure, " - "and journey-visualisation rubrics. It records the green baseline and the " - "accepted exception so future passes can hunt for semantic drift instead of " - "rediscovering the same ledgers.", + "This snapshot puts the catalog's quality ledgers beside each other for an " + "editorial pass. Every number is computed from the live registries at " + "generation time; rows marked CURATED are editorial judgements that the " + "audit pass must re-affirm by review — this report does not validate them.", "", "## Scoreboard", "", f"- Examples: {score_summary(example_scores)}", f"- Example diagrams: {score_summary(figure_scores)}", f"- Journey diagrams: {score_summary(journey_scores)}", - "- Accepted waiver: `hello-world` remains intentionally tiny at 7.1.", + *waiver_lines, "- Graph health: " f"{graph['linked_sources']} linked sources, {graph['edges']} edges, " f"{graph['orphaned']} orphaned examples.", @@ -319,15 +330,39 @@ def render_report(date: str) -> str: ) else: lines.append("No journey section figures reuse production example paint functions.") + target = float(registry.get("quality_gates", {}).get("example_target", 9.0)) + below_target = sorted( + slug for slug, (score, _comment) in EXAMPLE_QUALITY_SCORES.items() + if score < target and slug not in waivers + ) + findings: list[str] = [] + if below_target: + findings.append( + f"{len(below_target)} example(s) below target {target:g} without a waiver: " + + ", ".join(f"`{slug}`" for slug in below_target) + "." + ) + if graph["orphaned"]: + findings.append(f"{graph['orphaned']} orphaned example(s) in the see_also graph.") + if reused_sections: + findings.append( + f"{len(reused_sections)} journey section(s) reuse example paint functions." + ) + weakest_diagram = min(figure_scores) if figure_scores else 0.0 + weakest_journey = min(journey_scores) if journey_scores else 0.0 lines.extend([ "", - "## Audit conclusion", + "## Computed findings", + "", + *( + [f"- {finding}" for finding in findings] + or ["- No gate-blocking conditions detected in the computed ledgers."] + ), + f"- Weakest example diagram score: {weakest_diagram:g}; " + f"weakest journey diagram score: {weakest_journey:g}.", "", - "No gate-blocking edits are required by this pass. The only below-target " - "example is the standing `hello-world` waiver; all example diagrams and " - "journey figures are at or above 9.0; every journey section has declared " - "outcomes; the example graph has no orphaned sources; and every journey " - "section now uses a journey-native figure rather than a lesson attachment.", + "CURATED dimensions above are not validated by this report; the audit " + "pass owns re-affirming them (the marginalia gestalt page shows every " + "figure with its production caption for exactly that review).", "", ]) return "\n".join(lines) @@ -335,7 +370,7 @@ def render_report(date: str) -> str: def main() -> int: parser = argparse.ArgumentParser() - parser.add_argument("--date", default="2026-05-12") + parser.add_argument("--date", default=datetime.date.today().isoformat()) parser.add_argument("--output", type=Path) args = parser.parse_args() diff --git a/scripts/build_marginalia.py b/scripts/build_marginalia.py index 00f8e63..e0a52c3 100644 --- a/scripts/build_marginalia.py +++ b/scripts/build_marginalia.py @@ -17,50 +17,44 @@ 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 +from _common import ROOT, load_catalog +from src.marginalia import ATTACHMENTS, FIGURES, SCORES +from src.marginalia_grammar import Card OUT = ROOT / "public" / "prototyping" / "marginalia-gestalt.html" def _example_cards() -> list[Card]: - """One Card per example slug that has a production attachment. + """One Card per attached production figure, in example order. - The paint function, intrinsic width, and intrinsic height all come - from FIGURES[ATTACHMENTS[slug][0][1]]. Slugs without attachments - (or without scores) are skipped. + The paint function, dimensions, and the exact production figcaption + all come from FIGURES/ATTACHMENTS, so reviewers judge the same + figure+caption pairing readers see — a caption asserting something + the figure does not draw is visible here at a glance. """ - _, examples = load_examples() + _, examples = load_catalog() 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) + for _, figure_name, caption in attachments: + paint, w, h = FIGURES[figure_name] + card = Card( + slug=slug, + title=ex["title"], + section=ex["section"], + order=i, + figure=paint, + caption=caption, + width=w, + height=h, + ) + score = SCORES.get(slug) + if score is not None: + card.score, card.score_note = score + cards.append(card) return cards @@ -110,6 +104,10 @@ def _example_cards() -> list[Card]: } .card h3 { font-size: 15px; font-weight: 500; margin: 0; letter-spacing: -0.005em; } .card svg { margin-top: 8px; max-width: 100%; height: auto; overflow: visible; } + .card .caption { + margin: 6px 0 0; font-size: 12px; color: var(--ink); + max-width: 44ch; + } .card .note { margin: 6px 0 0; font-style: italic; font-size: 12px; color: var(--ink-soft); max-width: 38ch; @@ -143,7 +141,7 @@ def render() -> str: def main() -> None: OUT.write_text(render()) - print(f"wrote {OUT.relative_to(ROOT)} — {len(EXAMPLES)} examples (from production FIGURES)") + print(f"wrote {OUT.relative_to(ROOT)} — {len(EXAMPLES)} figure cards (from production FIGURES)") if __name__ == "__main__": diff --git a/scripts/build_prototypes.py b/scripts/build_prototypes.py index 31deeba..0d49e44 100644 --- a/scripts/build_prototypes.py +++ b/scripts/build_prototypes.py @@ -10,20 +10,11 @@ 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 +from _common import ROOT +from src.app import JOURNEYS_BY_SLUG, _walkthrough_cells, get_example, render_inline +from src.marginalia import SECTION_FIGURES as JOURNEY_SECTION_FIGURES +from src.marginalia import _render_svg OUT_DIR = ROOT / "public" / "prototyping" @@ -320,7 +311,7 @@ def build_production_figures_gestalt() -> None: 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 + from src.marginalia import ATTACHMENTS, FIGURES, SCORES # Build a slug→figure_names index of attached figures so we can mark # figures that already render somewhere on a real page. diff --git a/scripts/capture_browser_screenshot.mjs b/scripts/capture_browser_screenshot.mjs index eb0eb4c..069a0a2 100755 --- a/scripts/capture_browser_screenshot.mjs +++ b/scripts/capture_browser_screenshot.mjs @@ -5,7 +5,9 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; const args = new Set(process.argv.slice(2)); -const chromePath = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; +const chromePath = process.env.CHROME_PATH || (process.platform === 'darwin' + ? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' + : '/usr/bin/google-chrome'); const target = process.env.TARGET_URL || 'http://127.0.0.1:9696/layout-options/mobile-run-first'; const output = process.env.OUTPUT || (args.has('--inject-shiki') ? '/tmp/pythonbyexample-shots/desktop-literate-shiki.png' : '/tmp/pythonbyexample-shots/desktop-literate-plain.png'); const url = `${target}${target.includes('?') ? '&' : '?'}screenshot_test=${Date.now()}`; diff --git a/scripts/check_broad_surface_tours.py b/scripts/check_broad_surface_tours.py index 11959af..a8c2918 100755 --- a/scripts/check_broad_surface_tours.py +++ b/scripts/check_broad_surface_tours.py @@ -3,33 +3,34 @@ Reads `docs/quality-registries.toml`. A page may opt out of the strict check by adding `scope_first_pass = true` to its frontmatter, in which -case it must instead carry `see_also` links pointing at focused -neighbors that the registry expects to exist. +case the registry must name `focused_neighbors` and the page must link to +all of them via `see_also`. """ from __future__ import annotations -import re import sys -import tomllib from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -EXAMPLES_DIR = ROOT / "src" / "example_sources" -REGISTRY_PATH = ROOT / "docs" / "quality-registries.toml" +from _common import EXAMPLES_DIR, REGISTRY_PATH, frontmatter, load_registry -FRONTMATTER_RE = re.compile(r"^\+\+\+\n(.*?)\n\+\+\+\n", re.DOTALL) - - -def parse_frontmatter(text: str) -> dict: - match = FRONTMATTER_RE.match(text) - if not match: - return {} - return tomllib.loads(match.group(1)) +def scope_first_pass_errors(page: Path, see_also: list[str], focused_neighbors: list[str]) -> list[str]: + errors: list[str] = [] + if not focused_neighbors: + errors.append( + f"{page}: scope_first_pass=true requires focused_neighbors in broad_surface_tours" + ) + return errors + missing = [slug for slug in focused_neighbors if slug not in see_also] + if missing: + errors.append( + f"{page}: scope_first_pass=true missing focused see_also neighbors: {missing}" + ) + return errors def main() -> int: - data = tomllib.loads(REGISTRY_PATH.read_text()) + data = load_registry() tours = data.get("broad_surface_tours", {}) errors: list[str] = [] for slug, spec in tours.items(): @@ -38,15 +39,17 @@ def main() -> int: errors.append(f"{REGISTRY_PATH}: broad-tour page missing: {slug}.md") continue text = page.read_text() - frontmatter = parse_frontmatter(text) + page_frontmatter = frontmatter(page) required = spec.get("required_tokens", []) missing = [token for token in required if token not in text] - if frontmatter.get("scope_first_pass"): - see_also = frontmatter.get("see_also") or [] - if not see_also: - errors.append( - f"{page}: scope_first_pass=true requires see_also links to focused neighbors" + if page_frontmatter.get("scope_first_pass"): + errors.extend( + scope_first_pass_errors( + page, + page_frontmatter.get("see_also") or [], + spec.get("focused_neighbors") or [], ) + ) continue if missing: errors.append( diff --git a/scripts/check_confusable_pairs.py b/scripts/check_confusable_pairs.py index 0e4e7ec..7af725c 100755 --- a/scripts/check_confusable_pairs.py +++ b/scripts/check_confusable_pairs.py @@ -1,37 +1,76 @@ #!/usr/bin/env python3 """Verify that each confusable pair appears on its owning page. -Reads `docs/quality-registries.toml` and fails if the owning page's source -text is missing any token from the pair. Tokens are matched as substrings, -so they should be specific enough to avoid false positives. +Reads `docs/quality-registries.toml` and fails if the owning page's +teaching cells are missing any token from the pair. + +Matching defends against substring shadowing: a token occurrence that +sits inside a longer sibling token's occurrence does not count, so +"def " cannot be satisfied by the "async def " that another token +already requires — the page must contain a plain function definition of +its own. Entries may also supply `patterns` (regular expressions) when +substring tokens are not precise enough. Intro prose, frontmatter, and +notes do not count; the contrast must be present in cell prose/code/output. """ from __future__ import annotations +import re import sys -import tomllib -from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -EXAMPLES_DIR = ROOT / "src" / "example_sources" -REGISTRY_PATH = ROOT / "docs" / "quality-registries.toml" +from _common import EXAMPLES_DIR, REGISTRY_PATH, load_registry +CELL_BLOCK_RE = re.compile(r":::(?:cell|unsupported)\n(.*?)\n:::", re.S) + + +def cell_text(markdown_text: str) -> str: + return "\n\n".join(match.group(1) for match in CELL_BLOCK_RE.finditer(markdown_text)) + + +def _spans(text: str, token: str) -> list[tuple[int, int]]: + spans = [] + start = 0 + while (found := text.find(token, start)) != -1: + spans.append((found, found + len(token))) + start = found + 1 + return spans + + +def missing_tokens(text: str, tokens: list[str]) -> list[str]: + missing = [] + for token in tokens: + shadow_spans = [ + span + for other in tokens + if other != token and len(other) > len(token) + for span in _spans(text, other) + ] + unshadowed = [ + (start, end) + for start, end in _spans(text, token) + if not any(start < o_end and o_start < end for o_start, o_end in shadow_spans) + ] + if not unshadowed: + missing.append(token) + return missing def main() -> int: - data = tomllib.loads(REGISTRY_PATH.read_text()) + data = load_registry() pairs = data.get("confusable_pairs", []) errors: list[str] = [] for entry in pairs: name = entry["name"] owner = entry["owner"] - tokens = entry["tokens"] page = EXAMPLES_DIR / f"{owner}.md" if not page.exists(): errors.append(f"{REGISTRY_PATH}: owner page missing for {name!r}: {owner}.md") continue - text = page.read_text() - missing = [token for token in tokens if token not in text] + text = cell_text(page.read_text()) + missing = missing_tokens(text, entry.get("tokens", [])) if missing: errors.append(f"{page}: confusable pair {name!r} missing tokens: {missing}") + for pattern in entry.get("patterns", []): + if not re.search(pattern, text): + errors.append(f"{page}: confusable pair {name!r} missing pattern: {pattern!r}") if errors: for error in errors: print(error, file=sys.stderr) diff --git a/scripts/check_example_migration_parity.py b/scripts/check_example_migration_parity.py deleted file mode 100755 index e6e9e2f..0000000 --- a/scripts/check_example_migration_parity.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -"""Verify Markdown examples against the frozen golden catalog.""" -from __future__ import annotations - -import contextlib -import importlib.util -import io -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) - -from src.example_loader import load_examples # noqa: E402 - -GOLDEN = ROOT / "tests" / "fixtures" / "golden_examples.py" - - -def run(code: str) -> str: - stdout = io.StringIO() - with contextlib.redirect_stdout(stdout): - exec(compile(code, "", "exec", dont_inherit=True), {"__name__": "__main__"}) - return stdout.getvalue() - - -def load_golden(): - spec = importlib.util.spec_from_file_location("golden_examples", GOLDEN) - if spec is None or spec.loader is None: - raise RuntimeError(f"could not load {GOLDEN}") - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module.EXAMPLES - - -def comparable_cells(example: dict) -> list[tuple[tuple[str, ...], str, str]]: - # Import lazily so this script compares against the same rendering grouping - # that the website used before the Markdown switch. - from src.app import _walkthrough_cells # noqa: PLC0415 - - return [(tuple(cell["prose"]), cell["code"], cell["output"]) for cell in _walkthrough_cells(example)] - - -def markdown_cells(example: dict) -> list[tuple[tuple[str, ...], str, str]]: - return [(tuple(cell["prose"]), cell["code"], cell["output"]) for cell in example["cells"]] - - -def main() -> int: - golden = load_golden() - _, markdown = load_examples() - errors: list[str] = [] - rows: list[str] = [] - if [e["slug"] for e in golden] != [e["slug"] for e in markdown]: - errors.append("example order differs") - for old, new in zip(golden, markdown): - slug = old["slug"] - for field in ["slug", "title", "section", "summary", "doc_url", "code", "expected_output", "notes"]: - if old.get(field) != new.get(field): - errors.append(f"{slug}: field differs: {field}") - if run(old["code"]) != run(new["code"]): - errors.append(f"{slug}: stdout differs") - old_cells = comparable_cells(old) - new_cells = markdown_cells(new) - if old_cells != new_cells: - errors.append(f"{slug}: teaching-structure difference") - rows.append(f"{slug}: cells={len(new_cells)}") - print("\n".join(rows)) - if errors: - for error in errors: - print(error, file=sys.stderr) - return 1 - print("100% golden parity") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_footgun_coverage.py b/scripts/check_footgun_coverage.py index 6856ccb..d923a59 100755 --- a/scripts/check_footgun_coverage.py +++ b/scripts/check_footgun_coverage.py @@ -9,16 +9,12 @@ from __future__ import annotations import sys -import tomllib -from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -EXAMPLES_DIR = ROOT / "src" / "example_sources" -REGISTRY_PATH = ROOT / "docs" / "quality-registries.toml" +from _common import EXAMPLES_DIR, REGISTRY_PATH, load_registry def main() -> int: - data = tomllib.loads(REGISTRY_PATH.read_text()) + data = load_registry() footguns = data.get("footguns", []) errors: list[str] = [] for entry in footguns: diff --git a/scripts/check_inline_links.py b/scripts/check_inline_links.py new file mode 100644 index 0000000..8810e1c --- /dev/null +++ b/scripts/check_inline_links.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Check inline links in example prose resolve to real pages. + +`render_inline` (src/textfmt.py) renders [text](/examples/slug) and +[text](/journeys/slug) as anchors and leaves every other [text](target) +literal. This gate fails the build for: + +- link syntax whose target is not an internal /examples or /journeys + path (it would ship as literal bracket text); +- internal links pointing at a slug that does not exist; +- a page linking to itself. +""" +from __future__ import annotations + +import re + +from _common import EXAMPLES_DIR, fail, load_catalog + +LINK_RE = re.compile(r"\[([^\[\]]+)\]\(([^()\s]+)\)") +INTERNAL_RE = re.compile(r"^/(examples|journeys)/([a-z0-9-]+)$") + + +def journey_slugs() -> set[str]: + from src.app import JOURNEYS_BY_SLUG + + return set(JOURNEYS_BY_SLUG) + + +def main() -> int: + _, examples = load_catalog() + example_slugs = {example["slug"] for example in examples} + journeys = journey_slugs() + errors: list[str] = [] + for example in examples: + slug = example["slug"] + path = EXAMPLES_DIR / f"{slug}.md" + surfaces: list[tuple[int, str]] = [(1, paragraph) for paragraph in example["explanation"]] + surfaces += [(1, note) for note in example.get("notes", [])] + for cell in example["cells"]: + surfaces += [(cell.get("line", 1), paragraph) for paragraph in cell.get("prose", [])] + for line, text in surfaces: + for match in LINK_RE.finditer(text): + label, target = match.group(1), match.group(2) + internal = INTERNAL_RE.match(target) + if not internal: + errors.append( + f"{path}:{line}: link [{label}]({target}) is not an internal " + f"/examples or /journeys path and would render as literal text" + ) + continue + kind, target_slug = internal.group(1), internal.group(2) + known = example_slugs if kind == "examples" else journeys + if target_slug not in known: + errors.append(f"{path}:{line}: link [{label}]({target}) points at an unknown {kind} slug") + if kind == "examples" and target_slug == slug: + errors.append(f"{path}:{line}: page links to itself: [{label}]({target})") + return fail(errors, f"Inline-link check OK ({len(examples)} examples).") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_journey_outcomes.py b/scripts/check_journey_outcomes.py index 047023e..5275cd2 100755 --- a/scripts/check_journey_outcomes.py +++ b/scripts/check_journey_outcomes.py @@ -8,21 +8,16 @@ from __future__ import annotations import sys -import tomllib -from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -REGISTRY_PATH = ROOT / "docs" / "quality-registries.toml" -sys.path.insert(0, str(ROOT)) - -from src.app import JOURNEYS # noqa: E402 -from src.examples import EXAMPLES # noqa: E402 +from _common import load_catalog, load_registry +from src.app import JOURNEYS def main() -> int: - registry = tomllib.loads(REGISTRY_PATH.read_text()) + registry = load_registry() outcomes = registry.get("journey_outcomes", {}) - example_slugs = {example["slug"] for example in EXAMPLES} + _catalog, examples = load_catalog() + example_slugs = {example["slug"] for example in examples} expected: dict[str, tuple[str, str, set[str]]] = {} errors: list[str] = [] diff --git a/scripts/check_no_figure_rationales.py b/scripts/check_no_figure_rationales.py index 731bbb8..8c5f56e 100755 --- a/scripts/check_no_figure_rationales.py +++ b/scripts/check_no_figure_rationales.py @@ -7,22 +7,18 @@ """ from __future__ import annotations +import datetime import sys -import tomllib -from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -REGISTRY_PATH = ROOT / "docs" / "quality-registries.toml" -sys.path.insert(0, str(ROOT)) - -from src.examples import EXAMPLES # noqa: E402 -from src.marginalia import ATTACHMENTS # noqa: E402 +from _common import load_catalog, load_registry +from src.marginalia import ATTACHMENTS def main() -> int: - registry = tomllib.loads(REGISTRY_PATH.read_text()) + registry = load_registry() rationales = registry.get("no_figure_rationales", {}) - slugs = {example["slug"] for example in EXAMPLES} + _catalog, examples = load_catalog() + slugs = {example["slug"] for example in examples} errors: list[str] = [] for slug, entry in sorted(rationales.items()): @@ -40,6 +36,17 @@ def main() -> int: errors.append(f"{slug}: missing no-figure reason") if not isinstance(review_after, str) or not review_after.strip(): errors.append(f"{slug}: missing review_after") + else: + try: + due = datetime.date.fromisoformat(review_after) + except ValueError: + errors.append(f"{slug}: review_after must be an ISO date, got {review_after!r}") + else: + if due <= datetime.date.today(): + errors.append( + f"{slug}: review_after {review_after} has passed; " + f"re-review the no-figure decision and move the date or draw the figure" + ) if errors: for error in errors: diff --git a/scripts/check_notes_supported.py b/scripts/check_notes_supported.py index e094aa7..62fac58 100755 --- a/scripts/check_notes_supported.py +++ b/scripts/check_notes_supported.py @@ -15,10 +15,8 @@ import re import sys -from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -EXAMPLES_DIR = ROOT / "src" / "example_sources" +from _common import EXAMPLES_DIR NOTE_RE = re.compile(r":::note\n(.*?)\n:::", re.DOTALL) @@ -45,23 +43,27 @@ def main() -> int: errors: list[str] = [] for path in sorted(EXAMPLES_DIR.glob("*.md")): text = path.read_text() - notes_match = NOTE_RE.search(text) - if not notes_match: + note_blocks = list(NOTE_RE.finditer(text)) + if not note_blocks: continue - body_outside_notes = text[: notes_match.start()] + text[notes_match.end():] + # Every note block is checked, and the grounding body excludes + # ALL note blocks — a bullet may not be "supported" by another + # note asserting the same undemonstrated thing. + body_outside_notes = NOTE_RE.sub("", text) body_tokens = tokens(body_outside_notes) - for raw_line in notes_match.group(1).splitlines(): - line = raw_line.strip() - if not line.startswith("- "): - continue - bullet = line[2:] - bullet_tokens = tokens(bullet) - if not bullet_tokens: - continue - if not bullet_tokens & body_tokens: - errors.append( - f"{path}: note bullet has no keyword overlap with the rest of the page: {bullet!r}" - ) + for notes_match in note_blocks: + for raw_line in notes_match.group(1).splitlines(): + line = raw_line.strip() + if not line.startswith("- "): + continue + bullet = line[2:] + bullet_tokens = tokens(bullet) + if not bullet_tokens: + continue + if not bullet_tokens & body_tokens: + errors.append( + f"{path}: note bullet has no keyword overlap with the rest of the page: {bullet!r}" + ) if errors: for error in errors: print(error, file=sys.stderr) diff --git a/scripts/check_program_covers_cells.py b/scripts/check_program_covers_cells.py new file mode 100644 index 0000000..07711c2 --- /dev/null +++ b/scripts/check_program_covers_cells.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Check that every executable cell is anchored in the program block. + +The site convention is that the editable `:::program` block contains the +code the cells walk through. Cells may restate definitions, vary inputs, +or add inspection lines (`print`, imports), but a cell whose substantive +logic is entirely absent from the program teaches code the editor cannot +reproduce — the drift this gate exists to catch. + +A page that deliberately contrasts an approach the program does not use +can opt out with `standalone_cells = true` in frontmatter; the opt-out +is reported so it stays a visible editorial decision. +""" +from __future__ import annotations + +from _common import EXAMPLES_DIR, fail, frontmatter, load_catalog + +INSPECTION_PREFIXES = ("print(", "import ", "from ", "#") + + +def substantive_lines(code: str) -> list[str]: + lines = [] + for raw in code.splitlines(): + line = raw.strip() + if not line or line.startswith(INSPECTION_PREFIXES): + continue + lines.append(line) + return lines + + +def main() -> int: + _, examples = load_catalog() + errors: list[str] = [] + opted_out: list[str] = [] + for example in examples: + slug = example["slug"] + path = EXAMPLES_DIR / f"{slug}.md" + if frontmatter(path).get("standalone_cells"): + opted_out.append(slug) + continue + program_lines = set(substantive_lines(example["code"])) + for index, cell in enumerate(example["cells"], 1): + if cell.get("kind") != "cell": + continue + cell_lines = substantive_lines(cell["code"]) + if cell_lines and not set(cell_lines) & program_lines: + errors.append( + f"{path}:{cell.get('line', 1)}: cell {index} shares no substantive line " + f"with the :::program block; add the code to the program or set " + f"standalone_cells = true with a reason in prose" + ) + if opted_out: + print("standalone_cells opt-outs:", ", ".join(sorted(opted_out))) + return fail(errors, f"Program-covers-cells check OK ({len(examples)} examples).") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_prose_duplication.py b/scripts/check_prose_duplication.py new file mode 100644 index 0000000..b378d55 --- /dev/null +++ b/scripts/check_prose_duplication.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Check for copy-paste prose residue in example pages. + +Three patterns indicate an unfinished edit rather than a writing choice, +and all three shipped at least once: + +- the same paragraph twice in a row inside one prose block; +- cell prose that is a verbatim copy of an intro paragraph (the cell + should describe its specific code, not repeat the page opening); +- the same note bullet stated twice in one page. + +Comparison is exact after whitespace normalization; paraphrases are an +editorial judgement this gate deliberately stays out of. +""" +from __future__ import annotations + +from _common import EXAMPLES_DIR, fail, load_catalog + + +def main() -> int: + _, examples = load_catalog() + errors: list[str] = [] + for example in examples: + slug = example["slug"] + path = EXAMPLES_DIR / f"{slug}.md" + intro = example.get("explanation", []) + for first, second in zip(intro, intro[1:]): + if first == second: + errors.append(f"{path}:1: intro repeats a paragraph verbatim: {first[:70]!r}") + intro_set = set(intro) + for index, cell in enumerate(example["cells"], 1): + prose = cell.get("prose", []) + line = cell.get("line", 1) + for first, second in zip(prose, prose[1:]): + if first == second: + errors.append( + f"{path}:{line}: cell {index} repeats a paragraph verbatim: {first[:70]!r}" + ) + for paragraph in prose: + if paragraph in intro_set: + errors.append( + f"{path}:{line}: cell {index} prose duplicates an intro paragraph: " + f"{paragraph[:70]!r}" + ) + seen: set[str] = set() + for note in example.get("notes", []): + if note in seen: + errors.append(f"{path}:1: duplicate note bullet: {note[:70]!r}") + seen.add(note) + return fail(errors, f"Prose-duplication check OK ({len(examples)} examples).") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_quality_scores.py b/scripts/check_quality_scores.py index 227eec3..d8a43cd 100755 --- a/scripts/check_quality_scores.py +++ b/scripts/check_quality_scores.py @@ -5,31 +5,40 @@ pretend to grade prose automatically; it makes the gate explicit: examples below the hard minimum must either have a narrow waiver or be tracked in the improvement backlog with a concrete next action. + +Waivers are time-boxed: `expires` must be an ISO date in the future, +and a waiver whose example has recovered to target is flagged as stale +so the registry only ever describes live editorial debt. """ from __future__ import annotations +import datetime import sys -import tomllib -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -REGISTRY_PATH = ROOT / "docs" / "quality-registries.toml" -sys.path.insert(0, str(ROOT)) - -from src.examples import EXAMPLES # noqa: E402 -from src.marginalia import EXAMPLE_QUALITY_SCORES, SECTION_FIGURE_SCORES # noqa: E402 - -def _load_registry() -> dict: - return tomllib.loads(REGISTRY_PATH.read_text()) +from _common import load_catalog, load_registry +from src.marginalia import EXAMPLE_QUALITY_SCORES, SECTION_FIGURE_SCORES def _entry_has_text(entry: dict, *keys: str) -> bool: return all(isinstance(entry.get(key), str) and bool(entry[key].strip()) for key in keys) +def check_expiry_date(value, *, today: datetime.date | None = None) -> str | None: + """Return an error string when `value` is not a future ISO date.""" + today = today or datetime.date.today() + if not isinstance(value, str): + return f"expires must be an ISO date string, got {value!r}" + try: + expires = datetime.date.fromisoformat(value) + except ValueError: + return f"expires must be an ISO date (YYYY-MM-DD), got {value!r}" + if expires <= today: + return f"expired on {value}; re-review and extend or fix the example" + return None + + def main() -> int: - registry = _load_registry() + registry = load_registry() gates = registry.get("quality_gates", {}) target = float(gates.get("example_target", 9.0)) hard_min = float(gates.get("example_hard_min", 8.5)) @@ -43,7 +52,8 @@ def main() -> int: print(error, file=sys.stderr) return 1 - slugs = {example["slug"] for example in EXAMPLES} + _catalog, examples = load_catalog() + slugs = {example["slug"] for example in examples} waivers = registry.get("quality_waivers", {}) backlog = registry.get("quality_improvement_backlog", {}) section_backlog = registry.get("journey_section_improvement_backlog", {}) @@ -56,13 +66,33 @@ def main() -> int: if ghost_scores: errors.append(f"quality scores for unknown examples: {sorted(ghost_scores)}") + for label, registry_scores in ( + ("example", EXAMPLE_QUALITY_SCORES), + ("journey section", SECTION_FIGURE_SCORES), + ): + for key, entry in registry_scores.items(): + if not isinstance(entry, tuple) or len(entry) != 2: + errors.append(f"{label} score {key}: not a (score, commentary) tuple") + continue + score, commentary = entry + if not isinstance(score, (int, float)) or not 0 <= score <= 10: + errors.append(f"{label} score {key}: {score!r} outside the rubric's [0, 10]") + if not isinstance(commentary, str) or not commentary.strip(): + errors.append(f"{label} score {key}: empty commentary") + for slug in sorted(waivers): if slug not in slugs: errors.append(f"quality waiver for unknown example: {slug}") continue entry = waivers[slug] - if not _entry_has_text(entry, "reason", "expires") or not isinstance(entry.get("accepted_min"), (int, float)): + if not _entry_has_text(entry, "reason") or not isinstance(entry.get("accepted_min"), (int, float)): errors.append(f"quality waiver {slug} must include accepted_min, reason, and expires") + expiry_error = check_expiry_date(entry.get("expires")) + if expiry_error: + errors.append(f"quality waiver {slug}: {expiry_error}") + waived_score = EXAMPLE_QUALITY_SCORES.get(slug, (0.0, ""))[0] + if waived_score >= target: + errors.append(f"quality waiver {slug} is stale because score is now {waived_score:.1f}") for slug in sorted(backlog): if slug not in slugs: @@ -109,6 +139,15 @@ def main() -> int: if title not in section_backlog: errors.append(f"journey section figure below {section_min:.1f}: {title}") + journey_average_min = gates.get("journey_average_min") + if journey_average_min is not None and SECTION_FIGURE_SCORES: + average = sum(score for score, _ in SECTION_FIGURE_SCORES.values()) / len(SECTION_FIGURE_SCORES) + if average < float(journey_average_min): + errors.append( + f"journey section figure average {average:.2f} below " + f"journey_average_min {float(journey_average_min):.1f}" + ) + if errors: for error in errors: print(error, file=sys.stderr) diff --git a/scripts/check_registry_integrity.py b/scripts/check_registry_integrity.py index 9dffbf9..f45c96a 100755 --- a/scripts/check_registry_integrity.py +++ b/scripts/check_registry_integrity.py @@ -7,20 +7,31 @@ """ from __future__ import annotations +import re import sys -import tomllib from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -EXAMPLES_DIR = ROOT / "src" / "example_sources" -REGISTRY_PATH = ROOT / "docs" / "quality-registries.toml" -MANIFEST_PATH = EXAMPLES_DIR / "manifest.toml" +from _common import EXAMPLES_DIR, frontmatter, load_catalog, load_registry + +CELL_BLOCK_RE = re.compile(r":::(?:cell|unsupported)\n(.*?)\n:::", re.S) + + +def _frontmatter_see_also(path: Path) -> set[str]: + return set(frontmatter(path).get("see_also", [])) + + +def _cell_text(path: Path) -> str: + return "\n\n".join(match.group(1) for match in CELL_BLOCK_RE.finditer(path.read_text())) + + +def _pair_key(first: str, second: str) -> str: + return f"{first}|{second}" def main() -> int: - registries = tomllib.loads(REGISTRY_PATH.read_text()) - manifest = tomllib.loads(MANIFEST_PATH.read_text()) - known: set[str] = set(manifest.get("order", [])) + registries = load_registry() + catalog, _examples = load_catalog() + known: set[str] = set(catalog.order) errors: list[str] = [] for entry in registries.get("confusable_pairs", []): @@ -51,10 +62,46 @@ def main() -> int: if not entry.get("fixed_tokens"): errors.append(f"footguns: {entry.get('name')!r} has no fixed_tokens") - for pair in registries.get("paired_pages", {}).get("pairs", []): + paired_pages = registries.get("paired_pages", {}) + cell_tokens_by_pair = paired_pages.get("cell_tokens", {}) + for pair in paired_pages.get("pairs", []): + if len(pair) != 2: + errors.append(f"paired_pages: expected two slugs, got {pair!r}") + continue for slug in pair: if slug not in known: errors.append(f"paired_pages: unknown slug {slug!r} in {pair}") + if all(slug in known for slug in pair): + first, second = pair + see_also = { + slug: _frontmatter_see_also(EXAMPLES_DIR / f"{slug}.md") for slug in pair + } + if second not in see_also[first] and first not in see_also[second]: + errors.append( + f"paired_pages: neither {first!r} nor {second!r} links the other " + f"via see_also, so the pair relationship is undiscoverable" + ) + tokens = cell_tokens_by_pair.get(_pair_key(first, second)) or cell_tokens_by_pair.get( + _pair_key(second, first) + ) + if not tokens: + errors.append( + f"paired_pages: {pair!r} needs cell_tokens proving the relationship " + "inside at least one teaching cell" + ) + continue + page_cell_text = { + slug: _cell_text(EXAMPLES_DIR / f"{slug}.md").lower() for slug in pair + } + lowered_tokens = [token.lower() for token in tokens] + if not any( + all(token in text for token in lowered_tokens) + for text in page_cell_text.values() + ): + errors.append( + f"paired_pages: {pair!r} cell_tokens {tokens!r} do not all appear " + "inside a cell on either paired page" + ) seen: set[tuple[str, str]] = set() for entry in registries.get("confusable_pairs", []): diff --git a/scripts/embed_editorial_registry.py b/scripts/embed_editorial_registry.py new file mode 100755 index 0000000..e24b459 --- /dev/null +++ b/scripts/embed_editorial_registry.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Embed the TOML editorial registry for Cloudflare Python Workers. + +Wrangler bundles Python modules and template files under ``src/`` but not the +``docs/`` directory. Keep the human-edited source in TOML, then generate this +small Python fallback so runtime imports have the same data in production. +""" +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "docs" / "quality-registries.toml" +TARGET = ROOT / "src" / "editorial_registry_data.py" + + +def main() -> None: + text = SOURCE.read_text() + TARGET.write_text( + "# Generated by scripts/embed_editorial_registry.py. Do not edit by hand.\n" + f"REGISTRY_TOML = {text!r}\n" + ) + print(f"Embedded editorial registry ({len(text.encode('utf-8'))} bytes).") + + +if __name__ == "__main__": + main() diff --git a/scripts/fingerprint_assets.py b/scripts/fingerprint_assets.py index 6beb53e..2e3946e 100755 --- a/scripts/fingerprint_assets.py +++ b/scripts/fingerprint_assets.py @@ -3,9 +3,10 @@ import hashlib import re -from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] +from _common import ROOT +from src.security import CONTENT_SECURITY_POLICY, STRICT_TRANSPORT_SECURITY + PUBLIC = ROOT / "public" MANIFEST = ROOT / "src" / "asset_manifest.py" @@ -37,12 +38,17 @@ def html_version(paths: dict[str, str]) -> str: for value in sorted(paths.values()): digest.update(value.encode("utf-8")) content_paths = [ + ROOT / "docs" / "quality-registries.toml", ROOT / "src" / "app.py", + ROOT / "src" / "editorial_registry.py", + ROOT / "src" / "editorial_registry_data.py", ROOT / "src" / "examples.py", ROOT / "src" / "example_loader.py", ROOT / "src" / "example_sources_data.py", + ROOT / "src" / "main.py", ROOT / "src" / "marginalia.py", ROOT / "src" / "marginalia_grammar.py", + ROOT / "src" / "security.py", ROOT / "src" / "example_sources" / "manifest.toml", *sorted((ROOT / "src" / "example_sources").glob("*.md")), *sorted((ROOT / "src" / "templates").glob("*.html")), @@ -62,6 +68,12 @@ def main() -> None: f"HTML_CACHE_VERSION = {version!r}\n" ) (PUBLIC / "_headers").write_text( + "/*\n" + " X-Content-Type-Options: nosniff\n" + " Referrer-Policy: strict-origin-when-cross-origin\n" + " X-Frame-Options: DENY\n" + f" Strict-Transport-Security: {STRICT_TRANSPORT_SECURITY}\n" + f" Content-Security-Policy: {CONTENT_SECURITY_POLICY}\n\n" "/site.*.css\n" " Cache-Control: public, max-age=31536000, immutable\n\n" "/syntax-highlight.*.js\n" diff --git a/scripts/format_examples.py b/scripts/format_examples.py index 494f236..cd62769 100755 --- a/scripts/format_examples.py +++ b/scripts/format_examples.py @@ -9,7 +9,7 @@ ROOT = Path(__file__).resolve().parents[1] SOURCE_DIR = ROOT / "src" / "example_sources" -FRONTMATTER_ORDER = ["slug", "title", "section", "summary", "doc_path", "scope_first_pass", "see_also", "min_python", "version_sensitive", "version_notes"] +FRONTMATTER_ORDER = ["slug", "title", "section", "summary", "doc_path", "scope_first_pass", "standalone_cells", "see_also", "expected_output", "min_python", "version_sensitive", "version_notes"] def toml_value(value: Any) -> str: diff --git a/scripts/lint_seo_cache.py b/scripts/lint_seo_cache.py index 0212c7c..2940faa 100755 --- a/scripts/lint_seo_cache.py +++ b/scripts/lint_seo_cache.py @@ -4,19 +4,16 @@ import hashlib import re import sys -from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) - -from scripts.fingerprint_assets import ASSETS, PUBLIC, html_version # noqa: E402 -from src.app import SITE_URL, list_examples, render_example_page, render_home # noqa: E402 -from src.asset_manifest import ASSET_PATHS, HTML_CACHE_VERSION # noqa: E402 +from _common import ROOT +from scripts.fingerprint_assets import ASSETS, PUBLIC, html_version +from src.app import SITE_URL, list_examples, render_example_page, render_home +from src.asset_manifest import ASSET_PATHS, HTML_CACHE_VERSION META_DESCRIPTION_RE = re.compile(r'') CANONICAL_RE = re.compile(r'') OG_URL_RE = re.compile(r'') -HASHED_ASSET_RE = re.compile(r'/(site|syntax-highlight)\.[0-9a-f]{12}\.(css|js)') +HASHED_ASSET_RE = re.compile(r'/(site|syntax-highlight|editor)\.[0-9a-f]{12}\.(css|js)') def fail(message: str, failures: list[str]) -> None: @@ -40,7 +37,7 @@ def assert_page_metadata(name: str, html: str, path: str, failures: list[str]) - og_url = OG_URL_RE.search(html) if not og_url or og_url.group(1) != expected_url: fail(f"{name}: og:url is {og_url.group(1) if og_url else None}, expected {expected_url}", failures) - if '/site.css' in html or '/syntax-highlight.js' in html: + if '/site.css' in html or '/syntax-highlight.js' in html or '/editor.js' in html: fail(f"{name}: references unfingerprinted CSS/JS asset", failures) if len(HASHED_ASSET_RE.findall(html)) < 2: fail(f"{name}: missing fingerprinted CSS/JS assets", failures) diff --git a/scripts/score_example_criteria.py b/scripts/score_example_criteria.py index 116eca1..d0d7e19 100755 --- a/scripts/score_example_criteria.py +++ b/scripts/score_example_criteria.py @@ -4,6 +4,11 @@ This is not the editorial source of truth. It is a search aid: it breaks the rubric into observable criteria so the next rewrite can target the weakest axis instead of arguing about one opaque number. + +It is also the one bound on the hand-curated score registry: a curated +score more than --max-delta above the heuristic fails the build, so the +registry cannot drift upward without the page itself changing in ways +the observable criteria can see. """ from __future__ import annotations @@ -11,17 +16,10 @@ import json import re import sys -import tomllib -from pathlib import Path from statistics import mean -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) - -from src.example_loader import load_examples # noqa: E402 -from src.marginalia import EXAMPLE_QUALITY_SCORES # noqa: E402 - -REGISTRY = ROOT / "docs" / "quality-registries.toml" +from _common import load_catalog, load_registry +from src.marginalia import EXAMPLE_QUALITY_SCORES GENERIC_PHRASES = [ "it exists to make a common boundary explicit", "the example is small, deterministic", @@ -82,10 +80,16 @@ def main() -> int: parser.add_argument("--json", action="store_true") parser.add_argument("--below", type=float, default=9.0) parser.add_argument("--limit", type=int, default=20) + parser.add_argument( + "--max-delta", + type=float, + default=1.5, + help="fail when a curated score exceeds the heuristic by more than this", + ) args = parser.parse_args() - weights = tomllib.loads(REGISTRY.read_text())["score_model"] - _, examples = load_examples() + weights = load_registry()["score_model"] + _, examples = load_catalog() rows = [] for example in examples: criteria = criterion_scores(example) @@ -116,6 +120,17 @@ def main() -> int: f"curated_avg={mean(row['curated'] for row in rows):.2f} " f"heuristic_avg={mean(row['heuristic'] for row in rows):.2f}" ) + + inflated = [row for row in rows if row["curated"] - row["heuristic"] > args.max_delta] + if inflated: + for row in sorted(inflated, key=lambda r: r["heuristic"] - r["curated"]): + print( + f"{row['slug']}: curated {row['curated']:.1f} exceeds heuristic " + f"{row['heuristic']:.1f} by more than {args.max_delta:.1f}; " + f"re-grade or improve the page", + file=sys.stderr, + ) + return 1 return 0 diff --git a/scripts/smoke_deployment.py b/scripts/smoke_deployment.py index 09f1be2..98b24d2 100755 --- a/scripts/smoke_deployment.py +++ b/scripts/smoke_deployment.py @@ -69,6 +69,12 @@ def has_exception_marker(body: str) -> str | None: return None +def validate_smoke_bypass_origin(base_url: str, smoke_bypass_secret: str) -> str | None: + if smoke_bypass_secret and urllib.parse.urlparse(base_url).scheme != "https": + return "PBE_SMOKE_BYPASS_SECRET may only be sent to an https:// deployment origin" + return None + + def output_panel_text(body: str) -> str: match = re.search( r']*class="[^"]*output-panel[^"]*"[^>]*>.*?
(.*?)
', @@ -86,6 +92,10 @@ def main() -> int: args = parser.parse_args() base = args.base_url.rstrip("/") + "/" + smoke_bypass_value = os.environ.get("PBE_SMOKE_BYPASS_SECRET", "") + if origin_error := validate_smoke_bypass_origin(base, smoke_bypass_value): + print(origin_error, file=sys.stderr) + return 1 paths = SMOKE_PATHS + (args.paths or []) failures: list[str] = [] @@ -106,13 +116,11 @@ def main() -> int: failures.append(f"{url}: rendered exception marker {marker!r}") print(f"GET {status} {url}") - smoke_bypass_secret = os.environ.get("PBE_SMOKE_BYPASS_SECRET", "") - if not args.skip_post: for slug, code, expected in POST_SMOKES: url = urljoin(base, f"examples/{slug}") try: - status, body = post_code(url, code, smoke_bypass_secret) + status, body = post_code(url, code, smoke_bypass_value) except urllib.error.HTTPError as exc: failures.append(f"POST {url}: HTTP {exc.code}") continue diff --git a/scripts/verify_examples.py b/scripts/verify_examples.py index f5a2cf4..73a65b8 100755 --- a/scripts/verify_examples.py +++ b/scripts/verify_examples.py @@ -5,13 +5,10 @@ import argparse import re import sys -from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) - -from src.example_loader import EXAMPLES_DIR, load_examples, verify_example_output # noqa: E402 -from src.example_sources_data import EXAMPLE_SOURCE_FILES # noqa: E402 +from _common import EXAMPLES_DIR, load_catalog +from src.example_loader import verify_example_output +from src.example_sources_data import EXAMPLE_SOURCE_FILES def version_tuple(version: str) -> tuple[int, ...]: @@ -25,7 +22,7 @@ def main() -> int: args = parser.parse_args() selected = set(args.slugs) errors: list[str] = [] - catalog, examples = load_examples() + catalog, examples = load_catalog() if args.python_version and catalog.python_version != args.python_version: errors.append(f"src/example_sources/manifest.toml:1: python_version is {catalog.python_version!r}, expected {args.python_version!r}") @@ -47,8 +44,14 @@ def main() -> int: titles: set[str] = set() version_sensitive: list[str] = [] count = 0 - for example in examples: + for order_slug, example in zip(catalog.order, examples): slug = example["slug"] + if slug != order_slug: + errors.append( + f"{EXAMPLES_DIR / f'{order_slug}.md'}:1: frontmatter slug {slug!r} " + f"does not match the filename" + ) + continue if selected and slug not in selected: continue count += 1 diff --git a/src/app.py b/src/app.py index f4669f9..9dcf818 100644 --- a/src/app.py +++ b/src/app.py @@ -5,16 +5,25 @@ import html import io import json +import re from pathlib import Path try: from .asset_manifest import ASSET_PATHS + from .editorial_registry import journeys as load_journeys + from .editorial_registry import see_also_edge_labels as load_see_also_edge_labels from .examples import EXAMPLES, EXAMPLES_BY_SLUG, PYTHON_VERSION, REFERENCE_URL from .marginalia import render_for_anchor, render_for_section + from .security import CSP_SCRIPT_NONCE + from .textfmt import render_inline except ImportError: # Cloudflare Python Workers import sibling modules from main's directory. from asset_manifest import ASSET_PATHS + from editorial_registry import journeys as load_journeys + from editorial_registry import see_also_edge_labels as load_see_also_edge_labels from examples import EXAMPLES, EXAMPLES_BY_SLUG, PYTHON_VERSION, REFERENCE_URL from marginalia import render_for_anchor, render_for_section + from security import CSP_SCRIPT_NONCE + from textfmt import render_inline class AppResponse: @@ -65,8 +74,10 @@ def _recommended_examples(slug, limit=4): def build_dynamic_worker_code(example_code: str) -> str: """Build a Python Dynamic Worker module that executes one example. - The parent Worker supplies only curated example code from this repository. The - dynamic Worker has no outbound network access when loaded by src.main. + The code may be a curated example or a visitor's edited version of one + (the POST handler in src.main passes the submitted text). Embedding via + repr() keeps it a plain string literal, and src.main loads the dynamic + Worker with no outbound network access and tight CPU/subrequest limits. """ return f'''from workers import WorkerEntrypoint, Response import contextlib @@ -93,313 +104,9 @@ async def fetch(self, request): _TEMPLATE_CACHE = {} SITE_URL = "https://www.pythonbyexample.dev" -JOURNEYS = [ - { - "slug": "runtime", - "title": "Runtime", - "summary": "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.", - "sections": [ - { - "title": "Start with executable evidence.", - "summary": "Learners first need to see that every page is a runnable program with visible output.", - "items": [ - ("example", "hello-world", "start with a complete program and its output"), - ("example", "values", "see that Python programs manipulate runtime objects"), - ("example", "literals", "write small values directly in source code"), - ("example", "variables", "understand that names bind to objects rather than storing values themselves"), - ("example", "constants", "learn the convention Python uses for values that should not change"), - ], - }, - { - "title": "Separate value, identity, and absence.", - "summary": "This section prevents early confusion about equality, object identity, missing values, and truth tests.", - "items": [ - ("example", "none", "represent expected absence with a singleton object"), - ("example", "booleans", "combine facts with boolean operators"), - ("example", "truthiness", "predict how objects behave in boolean contexts"), - ("example", "equality-and-identity", "distinguish value equality from object identity"), - ("example", "mutability", "predict when operations change an object in place"), - ("example", "object-lifecycle", "explain references, garbage collection, and why identity can outlive a single name"), - ], - }, - { - "title": "Read expressions as object operations.", - "summary": "This section connects operators, text, and formatting to Python's data model.", - "items": [ - ("example", "numbers", "use numeric objects and arithmetic operators"), - ("example", "operators", "combine, compare, and test values with expression syntax"), - ("example", "strings", "treat text as Unicode rather than raw bytes"), - ("example", "string-formatting", "turn objects into readable text at output boundaries"), - ("example", "bytes-and-bytearray", "contrast text with binary data and explicit decoding"), - ], - }, - ], - }, - { - "slug": "control-flow", - "title": "Control Flow", - "summary": "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.", - "sections": [ - { - "title": "Choose between paths.", - "summary": "Start with ordinary branching and boolean predicates before reaching for more compact forms.", - "items": [ - ("example", "booleans", "combine facts into readable conditions"), - ("example", "truthiness", "use object truth values without hiding intent"), - ("example", "operators", "build comparisons and boolean expressions for conditions"), - ("example", "conditionals", "choose between branches with clear predicates"), - ], - }, - { - "title": "Name and shape decisions.", - "summary": "Some branches become clearer when the code names an intermediate value or dispatches on data shape.", - "items": [ - ("example", "assignment-expressions", "name an intermediate value inside a condition when it improves clarity"), - ("example", "match-statements", "dispatch on the shape of data rather than only on boolean tests"), - ("example", "advanced-match-patterns", "combine destructuring, alternatives, and guards in pattern matching"), - ], - }, - { - "title": "Stop as soon as the answer is known.", - "summary": "Early exits make the successful path easier to read by moving exceptional or completed cases out of the way.", - "items": [ - ("example", "guard-clauses", "show how early returns reduce nested conditional code"), - ("example", "assertions", "state assumptions that should fail loudly while developing"), - ("example", "exceptions", "leave the current path when ordinary return values are not enough"), - ], - }, - ], - }, - { - "slug": "iteration", - "title": "Iteration", - "summary": "This journey follows repeated work from ordinary loops to the iterator protocol: consume values, stop deliberately, and produce lazy streams only when they help.", - "sections": [ - { - "title": "Choose the right loop shape.", - "summary": "Loops differ by what they consume, when they stop, and whether completion itself carries meaning.", - "items": [ - ("example", "for-loops", "consume values from an iterable"), - ("example", "while-loops", "repeat while a condition must be rechecked"), - ("example", "break-and-continue", "interrupt or skip loop work intentionally"), - ("example", "loop-else", "attach completion logic to loops that did not break"), - ("example", "sentinel-iteration", "show `iter(callable, sentinel)` for repeated reads until a marker appears"), - ], - }, - { - "title": "See the protocol behind `for`.", - "summary": "The important mental shift is that loops consume producers through a protocol rather than special-casing lists.", - "items": [ - ("example", "iterating-over-iterables", "separate value producers from value consumers"), - ("example", "iterators", "use `iter()` and `next()` to expose the protocol behind `for`"), - ("example", "generators", "write functions that produce values lazily"), - ], - }, - { - "title": "Compose lazy value streams.", - "summary": "Iterator pipelines are useful when code can transform values one at a time instead of materializing every intermediate result.", - "items": [ - ("example", "generator-expressions", "create lazy one-pass streams with expression syntax"), - ("example", "itertools", "compose iterator streams without materializing every value"), - ("example", "yield-from", "delegate part of a generator to another iterable"), - ], - }, - ], - }, - { - "slug": "shapes", - "title": "Shapes", - "summary": "This journey teaches the core Python habit of choosing a data shape, transforming it directly, and making the result visible.", - "sections": [ - { - "title": "Pick the container that matches the question.", - "summary": "Lists, tuples, dictionaries, and sets answer different questions about order, position, lookup, and uniqueness.", - "items": [ - ("example", "lists", "store ordered mutable data"), - ("example", "tuples", "group fixed-position values"), - ("example", "dicts", "look up values by key"), - ("example", "sets", "model uniqueness and membership"), - ("example", "collections-module", "show `deque`, `Counter`, `defaultdict`, and `namedtuple` as specialized shapes"), - ], - }, - { - "title": "Move between shapes deliberately.", - "summary": "Most everyday Python code is data reshaping, so learners need the idioms for selecting, unpacking, and rebuilding values.", - "items": [ - ("example", "unpacking", "bind names from structured values"), - ("example", "slices", "select ranges from sequences"), - ("example", "comprehensions", "build concrete collections from compact loops"), - ("example", "comprehension-patterns", "compose filters and nested transformations"), - ("example", "sorting", "order records with key functions"), - ("example", "copying-collections", "contrast shallow copies, deep copies, and shared nested data"), - ], - }, - { - "title": "Cross text and data boundaries.", - "summary": "Programs often receive text and produce structured data, so parsing and serialization belong in the data journey.", - "items": [ - ("example", "number-parsing", "turn text into numbers safely"), - ("example", "json", "move structured data across a text boundary"), - ("example", "regular-expressions", "extract structure from text patterns"), - ("example", "datetime", "represent dates, times, and durations as typed values"), - ("example", "csv-data", "show row-shaped text data and dictionary records"), - ], - }, - ], - }, - { - "slug": "interfaces", - "title": "Interfaces", - "summary": "This journey shows how Python grows from simple functions to callable APIs, object interfaces, protocols, and metaclasses.", - "sections": [ - { - "title": "Start with functions as named behavior.", - "summary": "Functions are the first abstraction boundary because they name behavior and control how callers provide information.", - "items": [ - ("example", "functions", "package behavior behind a name"), - ("example", "keyword-only-arguments", "make important call-site choices explicit"), - ("example", "positional-only-parameters", "hide parameter names that should remain implementation details"), - ("example", "args-and-kwargs", "accept flexible call shapes when forwarding or adapting APIs"), - ("example", "multiple-return-values", "return multiple related values as a tuple"), - ], - }, - { - "title": "Use functions as values.", - "summary": "Python functions can capture state, be passed around, and wrap other functions.", - "items": [ - ("example", "scope-global-nonlocal", "control where assignment happens"), - ("example", "closures", "capture state in nested functions"), - ("example", "recursion", "solve self-similar problems with a base case"), - ("example", "lambdas", "write small unnamed functions for expression positions"), - ("example", "decorators", "wrap behavior without changing call sites"), - ("example", "partial-functions", "show how to pre-fill arguments with `functools.partial`"), - ], - }, - { - "title": "Bundle behavior with state.", - "summary": "Classes become useful when data and behavior need to move together behind a stable interface.", - "items": [ - ("example", "classes", "bundle state and behavior into a new object type"), - ("example", "inheritance-and-super", "reuse and extend behavior through parent classes"), - ("example", "dataclasses", "generate common methods for data containers"), - ("example", "properties", "keep attribute syntax while adding computation or validation"), - ("example", "special-methods", "connect objects to Python syntax and built-ins"), - ("example", "truth-and-size", "make objects work with truth tests and `len()`"), - ("example", "container-protocols", "support membership, lookup, and assignment syntax"), - ("example", "callable-objects", "make stateful instances callable like functions"), - ("example", "operator-overloading", "define operators only when the operation is unsurprising"), - ("example", "attribute-access", "customize fallback lookup and assignment carefully"), - ("example", "descriptors", "explain the protocol behind methods, properties, and managed attributes"), - ("example", "metaclasses", "customize class creation when ordinary class tools are not enough"), - ], - }, - ], - }, - { - "slug": "types", - "title": "Types", - "summary": "This journey maps Python's runtime object model to optional static annotations so learners know what types can and cannot promise.", - "sections": [ - { - "title": "Keep runtime and static analysis separate.", - "summary": "The first lesson is that annotations describe expectations for tools while ordinary Python objects still run the program.", - "items": [ - ("example", "type-hints", "document expected types and feed type checkers"), - ("example", "protocols", "describe required behavior by structural shape"), - ("example", "enums", "name a fixed set of symbolic values"), - ("example", "runtime-type-checks", "show `type()`, `isinstance()`, and `issubclass()` without turning Python into Java"), - ], - }, - { - "title": "Describe realistic data shapes.", - "summary": "Typed Python becomes useful when annotations explain optional values, unions, callables, and JSON-like records.", - "items": [ - ("example", "union-and-optional-types", "show `X | Y` and `None`-aware APIs"), - ("example", "type-aliases", "name complex types with `type` statements or aliases"), - ("example", "typed-dicts", "type dictionary records that come from JSON"), - ("example", "literal-and-final", "express constrained values and names that should not be rebound"), - ("example", "callable-types", "type functions that are passed as arguments"), - ], - }, - { - "title": "Scale annotations for reusable libraries.", - "summary": "Advanced typing exists to preserve information across reusable functions, containers, and decorators.", - "items": [ - ("example", "generics-and-typevar", "write reusable typed containers and functions"), - ("example", "paramspec", "preserve callable signatures through decorators"), - ("example", "overloads", "describe APIs whose return type depends on the input shape"), - ("example", "casts-and-any", "show escape hatches and their tradeoffs"), - ("example", "newtype", "create distinct static identities for runtime-compatible values"), - ], - }, - ], - }, - { - "slug": "reliability", - "title": "Reliability", - "summary": "This journey follows the boundaries where programs fail, clean up, split into modules, communicate with the outside world, and run concurrent work.", - "sections": [ - { - "title": "Make failure explicit.", - "summary": "Robust Python code distinguishes expected absence, broken assumptions, recoverable errors, and domain-specific failures.", - "items": [ - ("example", "exceptions", "signal and recover from errors"), - ("example", "assertions", "state internal assumptions"), - ("example", "exception-chaining", "preserve the cause while translating an error"), - ("example", "exception-groups", "handle multiple failures together"), - ("example", "custom-exceptions", "name failures in the language of the problem domain"), - ("example", "warnings", "signal soft problems and deprecations"), - ], - }, - { - "title": "Control resource and module boundaries.", - "summary": "Cleanup, deletion, imports, and modules define where responsibilities begin and end.", - "items": [ - ("example", "context-managers", "pair setup with reliable cleanup"), - ("example", "delete-statements", "remove names, attributes, and items intentionally"), - ("example", "modules", "split code into importable files"), - ("example", "import-aliases", "make imported names clear at use sites"), - ("example", "packages", "show package directories, `__init__.py`, and public module boundaries"), - ("example", "virtual-environments", "isolate dependencies for a project"), - ], - }, - { - "title": "Handle operations that outlive one expression.", - "summary": "I/O, testing, logging, subprocesses, and concurrency require different control boundaries from ordinary expressions.", - "items": [ - ("example", "async-await", "await concurrent I/O-shaped work"), - ("example", "async-iteration-and-context", "consume async streams and cleanup protocols"), - ("example", "logging", "record operational events without using `print()`"), - ("example", "testing", "write deterministic tests with `unittest` or `pytest`"), - ("example", "subprocesses", "run external commands safely"), - ("example", "threads-and-processes", "contrast concurrency choices beyond `asyncio`"), - ("example", "networking", "make HTTP or socket boundaries explicit"), - ], - }, - ], - }, -] - +JOURNEYS = load_journeys() JOURNEYS_BY_SLUG = {journey["slug"]: journey for journey in JOURNEYS} - - -SEE_ALSO_EDGE_LABELS = { - ("break-and-continue", "loop-else"): "contrast", - ("assignment-expressions", "conditionals"): "contrast", - ("yield-from", "generators"): "prerequisite", - ("async-iteration-and-context", "async-await"): "prerequisite", - ("delete-statements", "mutability"): "shared mechanism", - ("positional-only-parameters", "keyword-only-arguments"): "contrast", - ("assertions", "exceptions"): "alternative", - ("exception-chaining", "exceptions"): "builds on", - ("exception-groups", "exceptions"): "alternative", - ("operators", "numbers"): "related syntax", - ("operators", "booleans"): "condition building", - ("operators", "assignment-expressions"): "specialized expression", - ("literals", "values"): "value surface", - ("literals", "strings"): "text literal", - ("literals", "sets"): "container literal", -} +SEE_ALSO_EDGE_LABELS = load_see_also_edge_labels() FAVICON_SVG = ''' @@ -410,27 +117,24 @@ async def fetch(self, request): -def render_inline(text: str) -> str: - parts = text.split("`") - rendered = [] - for index, part in enumerate(parts): - if index % 2: - rendered.append(f'{html.escape(part)}') - else: - rendered.append(html.escape(part)) - return "".join(rendered) - - def _template(name: str) -> str: if name not in _TEMPLATE_CACHE: _TEMPLATE_CACHE[name] = (_TEMPLATE_DIR / name).read_text() return _TEMPLATE_CACHE[name] +_PLACEHOLDER_RE = re.compile(r"__([A-Z][A-Z0-9_]*?)__") + + def _replace(template: str, values: dict[str, str]) -> str: - for key, value in values.items(): - template = template.replace(f"__{key}__", value) - return template + # Single pass over the template only: substituted values are never + # rescanned, so user-submitted code containing a literal __TOKEN__ + # cannot pick up later substitutions, and the result cannot depend + # on dict order. Unknown placeholders stay literal for the SEO lint + # to catch. + return _PLACEHOLDER_RE.sub( + lambda match: values.get(match.group(1), match.group(0)), template + ) def _meta_description(text: str) -> str: @@ -687,18 +391,20 @@ def render_cell_output_flow_option(example):

Complete woven source

-
+
{"".join(cells)}

Notes

    {notes}
- @@ -727,6 +433,27 @@ def _turnstile_required_marker(required: bool) -> str: return '' +def render_example_not_found(slug: str) -> str: + recommendations = "".join( + f'
  • {html.escape(item["title"])}
  • ' + for item in _recommended_examples(slug) + ) + body = ( + '

    Example not found

    ' + '

    Try one of these nearby examples.

    ' + f'

    Recommended examples

      {recommendations}
    ' + ) + return _layout("Not Found", body) + + +def render_journey_not_found() -> str: + return _layout("Not Found", "

    Journey not found

    ") + + +def render_not_found() -> str: + return _layout("Not Found", "

    Not found

    ") + + def render_example_page( example, output=None, @@ -787,7 +514,10 @@ def render_example_page( "OUTPUT_HEADING": html.escape(output_heading), "SHOWN_OUTPUT": html.escape(shown_output), "EXECUTION_TIME": html.escape(execution_time), - "ORIGINAL_CODE_JSON": json.dumps(example["code"]), + # <-escaping keeps a literal in example code + # from terminating the inline script block. + "ORIGINAL_CODE_JSON": json.dumps(example["code"]).replace("<", "\\u003c"), + "CSP_NONCE": html.escape(CSP_SCRIPT_NONCE), }, ) return _layout( @@ -824,20 +554,15 @@ def route(url: str, method: str = "GET", turnstile_site_key: str | None = None) slug = path.split("/", 2)[2] journey = JOURNEYS_BY_SLUG.get(slug) if journey is None: - return AppResponse(_layout("Not Found", "

    Journey not found

    "), status=404) + return AppResponse(render_journey_not_found(), status=404) return AppResponse(render_journey_page(journey), headers={"Content-Type": "text/html; charset=utf-8"}) if path.startswith("/examples/"): slug = path.split("/", 2)[2] example = get_example(slug) if example is None: - recommendations = "".join( - f'
  • {html.escape(item["title"])}
  • ' - for item in _recommended_examples(slug) - ) - body = f'

    Example not found

    Try one of these nearby examples.

    Recommended examples

      {recommendations}
    ' - return AppResponse(_layout("Not Found", body), status=404) + return AppResponse(render_example_not_found(slug), status=404) return AppResponse( render_example_page(example, turnstile_site_key=turnstile_site_key), headers={"Content-Type": "text/html; charset=utf-8"}, ) - return AppResponse(_layout("Not Found", "

    Not found

    "), status=404) + return AppResponse(render_not_found(), status=404) diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 2572020..2ed44c5 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.e87d4baf77e6.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.a4a7766e1b9b.js'} -HTML_CACHE_VERSION = 'c09a7489d1e1' +ASSET_PATHS = {'SITE_CSS': '/site.0c2adaa2c94a.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.a4a7766e1b9b.js'} +HTML_CACHE_VERSION = '9aace8e7f09e' diff --git a/src/editorial_registry.py b/src/editorial_registry.py new file mode 100644 index 0000000..b654ed7 --- /dev/null +++ b/src/editorial_registry.py @@ -0,0 +1,109 @@ +"""Runtime loaders for editorial TOML registries. + +The site keeps mutable editorial metadata — journey order, edge labels, +figure attachments, captions, and curated scores — in +``docs/quality-registries.toml``. Python modules keep only executable +behavior such as renderers and figure paint functions. +""" +from __future__ import annotations + +from functools import lru_cache +from pathlib import Path +from typing import Any +import tomllib + +ROOT = Path(__file__).resolve().parents[1] +REGISTRY_PATH = ROOT / "docs" / "quality-registries.toml" + + +def _registry_toml() -> str: + if REGISTRY_PATH.exists(): + return REGISTRY_PATH.read_text() + try: + from .editorial_registry_data import REGISTRY_TOML + except ImportError: # Cloudflare Workers import sibling modules without package prefix. + from editorial_registry_data import REGISTRY_TOML + return REGISTRY_TOML + + +@lru_cache(maxsize=1) +def load_registry() -> dict[str, Any]: + return tomllib.loads(_registry_toml()) + + +def _required_table(name: str) -> list[dict[str, Any]]: + rows = load_registry().get(name) + if not isinstance(rows, list): + raise KeyError(f"{REGISTRY_PATH}: missing [[{name}]] registry") + return rows + + +def journeys() -> list[dict[str, Any]]: + parsed: list[dict[str, Any]] = [] + for journey in _required_table("journeys"): + sections: list[dict[str, Any]] = [] + for section in journey.get("sections", []): + items = [ + (item["kind"], item["value"], item["description"]) + for item in section.get("items", []) + ] + sections.append( + { + "title": section["title"], + "summary": section["summary"], + "items": items, + } + ) + parsed.append( + { + "slug": journey["slug"], + "title": journey["title"], + "summary": journey["summary"], + "sections": sections, + } + ) + return parsed + + +def see_also_edge_labels() -> dict[tuple[str, str], str]: + return { + (row["source"], row["target"]): row["label"] + for row in _required_table("see_also_edge_labels") + } + + +def figure_attachments() -> dict[str, list[tuple[str, str, str | None]]]: + attachments: dict[str, list[tuple[str, str, str | None]]] = {} + for row in _required_table("figure_attachments"): + attachments.setdefault(row["slug"], []).append( + (row["anchor"], row["figure"], row.get("caption")) + ) + return attachments + + +def journey_section_figures() -> dict[str, tuple[str, str]]: + return { + row["section"]: (row["figure"], row["caption"]) + for row in _required_table("journey_section_figures") + } + + +def example_figure_scores() -> dict[str, tuple[float, str]]: + return { + row["slug"]: (float(row["score"]), row["comment"]) + for row in _required_table("example_figure_scores") + } + + +def journey_section_figure_scores() -> dict[str, tuple[float, str]]: + return { + row["section"]: (float(row["score"]), row["comment"]) + for row in _required_table("journey_section_figure_scores") + } + + +def example_quality_scores() -> dict[str, tuple[float, str]]: + return { + row["slug"]: (float(row["score"]), row["comment"]) + for row in _required_table("example_quality_scores") + } diff --git a/src/editorial_registry_data.py b/src/editorial_registry_data.py new file mode 100644 index 0000000..e2c7ff9 --- /dev/null +++ b/src/editorial_registry_data.py @@ -0,0 +1,2 @@ +# Generated by scripts/embed_editorial_registry.py. Do not edit by hand. +REGISTRY_TOML = '# Quality and editorial registries.\n#\n# Source of truth for rubric checks, journey structure, edge labels,\n# figure attachments/captions, and curated scores. Executable paint\n# functions stay in `src/marginalia.py`; mutable editorial data lives here.\n\n[[confusable_pairs]]\nname = "__str__ vs __repr__"\nowner = "special-methods"\ntokens = ["__str__", "__repr__"]\n\n[[confusable_pairs]]\nname = "is vs =="\nowner = "equality-and-identity"\ntokens = [" is ", "=="]\n\n[[confusable_pairs]]\nname = "list vs tuple"\nowner = "tuples"\ntokens = ["list", "tuple"]\n\n[[confusable_pairs]]\nname = "classmethod vs staticmethod vs instance method"\nowner = "classmethods-and-staticmethods"\ntokens = ["@classmethod", "@staticmethod", "self"]\n\n[[confusable_pairs]]\nname = "isinstance vs type=="\nowner = "runtime-type-checks"\ntokens = ["isinstance(", "type("]\n\n[[confusable_pairs]]\nname = "generator vs class iterator"\nowner = "generators"\ntokens = ["yield", "__next__"]\n\n[[confusable_pairs]]\nname = "iterator vs iterable"\nowner = "iterator-vs-iterable"\ntokens = ["iterable", "iterator", "iter("]\n\n[[confusable_pairs]]\nname = "mutable vs immutable class attributes"\nowner = "classes"\ntokens = ["class attribute", "__init__"]\n\n[[confusable_pairs]]\nname = "eager vs lazy production"\nowner = "generators"\ntokens = ["return", "yield"]\n\n[[confusable_pairs]]\nname = "Protocol vs ABC"\nowner = "abstract-base-classes"\ntokens = ["Protocol", "ABC"]\n\n[[confusable_pairs]]\nname = "dataclass vs NamedTuple vs TypedDict"\nowner = "structured-data-shapes"\ntokens = ["@dataclass", "NamedTuple", "TypedDict"]\n\n[[confusable_pairs]]\nname = "bound vs unbound methods"\nowner = "bound-and-unbound-methods"\ntokens = ["bound method", "Class.method"]\n\n[[confusable_pairs]]\nname = "yield vs return"\nowner = "generators"\ntokens = ["yield", "return"]\n\n[[confusable_pairs]]\nname = "shallow vs deep copy"\nowner = "copying-collections"\ntokens = ["copy(", "deepcopy("]\n\n[[confusable_pairs]]\nname = "sync vs async functions"\nowner = "async-await"\ntokens = ["async def", "def "]\n\n[broad_surface_tours]\n\n[broad_surface_tours.special-methods]\nrequired_tokens = [\n "__init__", "__repr__", "__str__", "__eq__", "__hash__", "__lt__",\n "__len__", "__iter__", "__contains__", "__getitem__", "__setitem__",\n "__call__", "__enter__", "__exit__", "__bool__",\n]\n\n[broad_surface_tours.operators]\nrequired_tokens = ["+", "==", " is ", " in ", "and", "or", "&", ":="]\n\n[broad_surface_tours.type-hints]\nrequired_tokens = ["list[", " | ", "Optional", "TypeAlias"]\n\n[broad_surface_tours.testing]\nrequired_tokens = ["TestCase", "assertEqual", "assertRaises", "setUp"]\n\n[broad_surface_tours.async-await]\nrequired_tokens = ["async def", "await", "asyncio.run", "asyncio.gather", "async for", "async with"]\n\n[broad_surface_tours.packages]\nrequired_tokens = ["__init__.py", "from .", "__all__"]\n\n[broad_surface_tours.regular-expressions]\nrequired_tokens = ["re.match", "re.search", "re.findall", "re.compile", "re.IGNORECASE", "re.sub"]\n\n[broad_surface_tours.literals]\nrequired_tokens = ["0x", "0b", "_", "f\\"", "True", "None"]\n\n[[footguns]]\nname = "Mutable default class attribute"\nowner = "classes"\nbroken_tokens = ["items = []"]\nfixed_tokens = ["self.items = []"]\n\n[[footguns]]\nname = "Mutable default function argument"\nowner = "functions"\nbroken_tokens = ["items=[]", "append_broken"]\nfixed_tokens = ["items=None", "append_fixed"]\n\n[[footguns]]\nname = "Late-binding closure in a loop"\nowner = "closures"\nbroken_tokens = ["lambda: i", "[2, 2, 2]"]\nfixed_tokens = ["lambda i=i", "[0, 1, 2]"]\n\n[[footguns]]\nname = "Integer identity caching"\nowner = "equality-and-identity"\nbroken_tokens = ["small_a is small_b", "big_a is big_b"]\nfixed_tokens = ["big_a == big_b", "small-integer cache"]\n\n[[footguns]]\nname = "Shallow vs deep copy"\nowner = "copying-collections"\nbroken_tokens = ["copy("]\nfixed_tokens = ["deepcopy("]\n\n[[footguns]]\nname = "Generator one-pass exhaustion"\nowner = "generators"\nbroken_tokens = ["yield"]\nfixed_tokens = ["list("]\n\n[[footguns]]\nname = "Dictionary mutation during iteration"\nowner = "dicts"\nbroken_tokens = ["for ", "del "]\nfixed_tokens = ["list(", ".keys("]\n\n[[footguns]]\nname = "Floating-point equality"\nowner = "numbers"\nbroken_tokens = ["0.1", "0.2"]\nfixed_tokens = ["isclose", "math"]\n\n[[footguns]]\nname = "bool as a subclass of int"\nowner = "booleans"\nbroken_tokens = ["isinstance(True, int)", "True + True"]\nfixed_tokens = ["not isinstance(value, bool)", "is_strict_int"]\n\n[[footguns]]\nname = "Bare except swallowing KeyboardInterrupt"\nowner = "exceptions"\nbroken_tokens = ["except Exception"]\nfixed_tokens = ["except ValueError"]\n\n[paired_pages]\n# Pages whose titles differ only by a suffix or modifier; at least one\n# member of each pair must demonstrate the relationship in a cell.\npairs = [\n ["iterators", "iterating-over-iterables"],\n ["iterators", "iterator-vs-iterable"],\n ["generators", "generator-expressions"],\n ["comprehensions", "comprehension-patterns"],\n]\n\n[paired_pages.cell_tokens]\n"iterators|iterating-over-iterables" = ["iterable", "iterator"]\n"iterators|iterator-vs-iterable" = ["iterable", "iterator", "iter("]\n"generators|generator-expressions" = ["generator", "expression"]\n"comprehensions|comprehension-patterns" = ["nested loops", "explicit loop"]\n\n[quality_gates]\nexample_target = 9.0\nexample_hard_min = 8.5\njourney_section_min = 8.5\njourney_average_min = 8.8\n\n[quality_waivers.hello-world]\naccepted_min = 7.0\nreason = "Traditional first example is intentionally tiny: program -> output is the goal, not a full concept tour."\nexpires = "2026-12-01"\n\n[no_figure_rationales]\n# Slugs may be added here only when the page is constraint-shaped,\n# infrastructure-shaped, or aggregator-shaped and a single mechanism\n# figure would distort the lesson. Current production attaches figures\n# to every example, so this registry is intentionally empty.\n\n[journey_outcomes]\n\n[journey_outcomes."runtime::Start with executable evidence."]\njourney = "runtime"\nsection = "Start with executable evidence."\nsupport = [\n "hello-world",\n "values",\n "literals",\n "variables",\n "constants",\n]\noutcomes = [\n "start with a complete program and its output",\n "see that Python programs manipulate runtime objects",\n "write small values directly in source code",\n "understand that names bind to objects rather than storing values themselves",\n]\n\n[journey_outcomes."runtime::Separate value, identity, and absence."]\njourney = "runtime"\nsection = "Separate value, identity, and absence."\nsupport = [\n "none",\n "booleans",\n "truthiness",\n "equality-and-identity",\n "mutability",\n "object-lifecycle",\n]\noutcomes = [\n "represent expected absence with a singleton object",\n "combine facts with boolean operators",\n "predict how objects behave in boolean contexts",\n "distinguish value equality from object identity",\n]\n\n[journey_outcomes."runtime::Read expressions as object operations."]\njourney = "runtime"\nsection = "Read expressions as object operations."\nsupport = [\n "numbers",\n "operators",\n "strings",\n "string-formatting",\n "bytes-and-bytearray",\n]\noutcomes = [\n "use numeric objects and arithmetic operators",\n "combine, compare, and test values with expression syntax",\n "treat text as Unicode rather than raw bytes",\n "turn objects into readable text at output boundaries",\n]\n\n[journey_outcomes."control-flow::Choose between paths."]\njourney = "control-flow"\nsection = "Choose between paths."\nsupport = [\n "booleans",\n "truthiness",\n "operators",\n "conditionals",\n]\noutcomes = [\n "combine facts into readable conditions",\n "use object truth values without hiding intent",\n "build comparisons and boolean expressions for conditions",\n "choose between branches with clear predicates",\n]\n\n[journey_outcomes."control-flow::Name and shape decisions."]\njourney = "control-flow"\nsection = "Name and shape decisions."\nsupport = [\n "assignment-expressions",\n "match-statements",\n "advanced-match-patterns",\n]\noutcomes = [\n "name an intermediate value inside a condition when it improves clarity",\n "dispatch on the shape of data rather than only on boolean tests",\n "combine destructuring, alternatives, and guards in pattern matching",\n]\n\n[journey_outcomes."control-flow::Stop as soon as the answer is known."]\njourney = "control-flow"\nsection = "Stop as soon as the answer is known."\nsupport = [\n "guard-clauses",\n "assertions",\n "exceptions",\n]\noutcomes = [\n "show how early returns reduce nested conditional code",\n "state assumptions that should fail loudly while developing",\n "leave the current path when ordinary return values are not enough",\n]\n\n[journey_outcomes."iteration::Choose the right loop shape."]\njourney = "iteration"\nsection = "Choose the right loop shape."\nsupport = [\n "for-loops",\n "while-loops",\n "break-and-continue",\n "loop-else",\n "sentinel-iteration",\n]\noutcomes = [\n "consume values from an iterable",\n "repeat while a condition must be rechecked",\n "interrupt or skip loop work intentionally",\n "attach completion logic to loops that did not break",\n]\n\n[journey_outcomes."iteration::See the protocol behind `for`."]\njourney = "iteration"\nsection = "See the protocol behind `for`."\nsupport = [\n "iterating-over-iterables",\n "iterators",\n "generators",\n]\noutcomes = [\n "separate value producers from value consumers",\n "use `iter()` and `next()` to expose the protocol behind `for`",\n "write functions that produce values lazily",\n]\n\n[journey_outcomes."iteration::Compose lazy value streams."]\njourney = "iteration"\nsection = "Compose lazy value streams."\nsupport = [\n "generator-expressions",\n "itertools",\n "yield-from",\n]\noutcomes = [\n "create lazy one-pass streams with expression syntax",\n "compose iterator streams without materializing every value",\n "delegate part of a generator to another iterable",\n]\n\n[journey_outcomes."shapes::Pick the container that matches the question."]\njourney = "shapes"\nsection = "Pick the container that matches the question."\nsupport = [\n "lists",\n "tuples",\n "dicts",\n "sets",\n "collections-module",\n]\noutcomes = [\n "store ordered mutable data",\n "group fixed-position values",\n "look up values by key",\n "model uniqueness and membership",\n]\n\n[journey_outcomes."shapes::Move between shapes deliberately."]\njourney = "shapes"\nsection = "Move between shapes deliberately."\nsupport = [\n "unpacking",\n "slices",\n "comprehensions",\n "comprehension-patterns",\n "sorting",\n "copying-collections",\n]\noutcomes = [\n "bind names from structured values",\n "select ranges from sequences",\n "build concrete collections from compact loops",\n "compose filters and nested transformations",\n]\n\n[journey_outcomes."shapes::Cross text and data boundaries."]\njourney = "shapes"\nsection = "Cross text and data boundaries."\nsupport = [\n "number-parsing",\n "json",\n "regular-expressions",\n "datetime",\n "csv-data",\n]\noutcomes = [\n "turn text into numbers safely",\n "move structured data across a text boundary",\n "extract structure from text patterns",\n "represent dates, times, and durations as typed values",\n]\n\n[journey_outcomes."interfaces::Start with functions as named behavior."]\njourney = "interfaces"\nsection = "Start with functions as named behavior."\nsupport = [\n "functions",\n "keyword-only-arguments",\n "positional-only-parameters",\n "args-and-kwargs",\n "multiple-return-values",\n]\noutcomes = [\n "package behavior behind a name",\n "make important call-site choices explicit",\n "hide parameter names that should remain implementation details",\n "accept flexible call shapes when forwarding or adapting APIs",\n]\n\n[journey_outcomes."interfaces::Use functions as values."]\njourney = "interfaces"\nsection = "Use functions as values."\nsupport = [\n "scope-global-nonlocal",\n "closures",\n "recursion",\n "lambdas",\n "decorators",\n "partial-functions",\n]\noutcomes = [\n "control where assignment happens",\n "capture state in nested functions",\n "solve self-similar problems with a base case",\n "write small unnamed functions for expression positions",\n]\n\n[journey_outcomes."interfaces::Bundle behavior with state."]\njourney = "interfaces"\nsection = "Bundle behavior with state."\nsupport = [\n "classes",\n "inheritance-and-super",\n "dataclasses",\n "properties",\n "special-methods",\n "truth-and-size",\n "container-protocols",\n "callable-objects",\n "operator-overloading",\n "attribute-access",\n "descriptors",\n "metaclasses",\n]\noutcomes = [\n "bundle state and behavior into a new object type",\n "reuse and extend behavior through parent classes",\n "generate common methods for data containers",\n "keep attribute syntax while adding computation or validation",\n]\n\n[journey_outcomes."types::Keep runtime and static analysis separate."]\njourney = "types"\nsection = "Keep runtime and static analysis separate."\nsupport = [\n "type-hints",\n "protocols",\n "enums",\n "runtime-type-checks",\n]\noutcomes = [\n "document expected types and feed type checkers",\n "describe required behavior by structural shape",\n "name a fixed set of symbolic values",\n "show `type()`, `isinstance()`, and `issubclass()` without turning Python into Java",\n]\n\n[journey_outcomes."types::Describe realistic data shapes."]\njourney = "types"\nsection = "Describe realistic data shapes."\nsupport = [\n "union-and-optional-types",\n "type-aliases",\n "typed-dicts",\n "literal-and-final",\n "callable-types",\n]\noutcomes = [\n "show `X | Y` and `None`-aware APIs",\n "name complex types with `type` statements or aliases",\n "type dictionary records that come from JSON",\n "express constrained values and names that should not be rebound",\n]\n\n[journey_outcomes."types::Scale annotations for reusable libraries."]\njourney = "types"\nsection = "Scale annotations for reusable libraries."\nsupport = [\n "generics-and-typevar",\n "paramspec",\n "overloads",\n "casts-and-any",\n "newtype",\n]\noutcomes = [\n "write reusable typed containers and functions",\n "preserve callable signatures through decorators",\n "describe APIs whose return type depends on the input shape",\n "show escape hatches and their tradeoffs",\n]\n\n[journey_outcomes."reliability::Make failure explicit."]\njourney = "reliability"\nsection = "Make failure explicit."\nsupport = [\n "exceptions",\n "assertions",\n "exception-chaining",\n "exception-groups",\n "custom-exceptions",\n "warnings",\n]\noutcomes = [\n "signal and recover from errors",\n "state internal assumptions",\n "preserve the cause while translating an error",\n "handle multiple failures together",\n]\n\n[journey_outcomes."reliability::Control resource and module boundaries."]\njourney = "reliability"\nsection = "Control resource and module boundaries."\nsupport = [\n "context-managers",\n "delete-statements",\n "modules",\n "import-aliases",\n "packages",\n "virtual-environments",\n]\noutcomes = [\n "pair setup with reliable cleanup",\n "remove names, attributes, and items intentionally",\n "split code into importable files",\n "make imported names clear at use sites",\n]\n\n[journey_outcomes."reliability::Handle operations that outlive one expression."]\njourney = "reliability"\nsection = "Handle operations that outlive one expression."\nsupport = [\n "async-await",\n "async-iteration-and-context",\n "logging",\n "testing",\n "subprocesses",\n "threads-and-processes",\n "networking",\n]\noutcomes = [\n "await concurrent I/O-shaped work",\n "consume async streams and cleanup protocols",\n "record operational events without using `print()`",\n "write deterministic tests with `unittest` or `pytest`",\n]\n\n[score_model]\n# Criterion weights mirror docs/example-quality-rubric.md. The registry\n# makes the scoring model inspectable even before every example is broken\n# down into per-criterion subscores.\nconceptual_payoff = 1.0\nrationale = 0.75\nalternatives_and_boundaries = 0.75\nexecutable_determinism = 1.0\npython_idiom_and_accuracy = 1.0\nliterate_fit = 0.75\nsource_result_pairing = 0.75\nconcept_decomposition = 0.75\nprogressive_walkthrough = 0.75\nrepresentative_coverage = 0.75\npractical_usefulness = 0.75\neditorial_progression = 1.0\n\n# Editorial registries loaded by the site at runtime.\n# Keep prose, captions, scores, journey ordering, and edge labels here;\n# keep executable paint functions in src/marginalia.py.\n\n[[journeys]]\nslug = "runtime"\ntitle = "Runtime"\nsummary = "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."\n\n[[journeys.sections]]\ntitle = "Start with executable evidence."\nsummary = "Learners first need to see that every page is a runnable program with visible output."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "hello-world"\ndescription = "start with a complete program and its output"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "values"\ndescription = "see that Python programs manipulate runtime objects"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "literals"\ndescription = "write small values directly in source code"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "variables"\ndescription = "understand that names bind to objects rather than storing values themselves"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "constants"\ndescription = "learn the convention Python uses for values that should not change"\n\n[[journeys.sections]]\ntitle = "Separate value, identity, and absence."\nsummary = "This section prevents early confusion about equality, object identity, missing values, and truth tests."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "none"\ndescription = "represent expected absence with a singleton object"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "booleans"\ndescription = "combine facts with boolean operators"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "truthiness"\ndescription = "predict how objects behave in boolean contexts"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "equality-and-identity"\ndescription = "distinguish value equality from object identity"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "mutability"\ndescription = "predict when operations change an object in place"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "object-lifecycle"\ndescription = "explain references, garbage collection, and why identity can outlive a single name"\n\n[[journeys.sections]]\ntitle = "Read expressions as object operations."\nsummary = "This section connects operators, text, and formatting to Python\'s data model."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "numbers"\ndescription = "use numeric objects and arithmetic operators"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "operators"\ndescription = "combine, compare, and test values with expression syntax"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "strings"\ndescription = "treat text as Unicode rather than raw bytes"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "string-formatting"\ndescription = "turn objects into readable text at output boundaries"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "bytes-and-bytearray"\ndescription = "contrast text with binary data and explicit decoding"\n\n[[journeys]]\nslug = "control-flow"\ntitle = "Control Flow"\nsummary = "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."\n\n[[journeys.sections]]\ntitle = "Choose between paths."\nsummary = "Start with ordinary branching and boolean predicates before reaching for more compact forms."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "booleans"\ndescription = "combine facts into readable conditions"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "truthiness"\ndescription = "use object truth values without hiding intent"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "operators"\ndescription = "build comparisons and boolean expressions for conditions"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "conditionals"\ndescription = "choose between branches with clear predicates"\n\n[[journeys.sections]]\ntitle = "Name and shape decisions."\nsummary = "Some branches become clearer when the code names an intermediate value or dispatches on data shape."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "assignment-expressions"\ndescription = "name an intermediate value inside a condition when it improves clarity"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "match-statements"\ndescription = "dispatch on the shape of data rather than only on boolean tests"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "advanced-match-patterns"\ndescription = "combine destructuring, alternatives, and guards in pattern matching"\n\n[[journeys.sections]]\ntitle = "Stop as soon as the answer is known."\nsummary = "Early exits make the successful path easier to read by moving exceptional or completed cases out of the way."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "guard-clauses"\ndescription = "show how early returns reduce nested conditional code"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "assertions"\ndescription = "state assumptions that should fail loudly while developing"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "exceptions"\ndescription = "leave the current path when ordinary return values are not enough"\n\n[[journeys]]\nslug = "iteration"\ntitle = "Iteration"\nsummary = "This journey follows repeated work from ordinary loops to the iterator protocol: consume values, stop deliberately, and produce lazy streams only when they help."\n\n[[journeys.sections]]\ntitle = "Choose the right loop shape."\nsummary = "Loops differ by what they consume, when they stop, and whether completion itself carries meaning."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "for-loops"\ndescription = "consume values from an iterable"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "while-loops"\ndescription = "repeat while a condition must be rechecked"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "break-and-continue"\ndescription = "interrupt or skip loop work intentionally"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "loop-else"\ndescription = "attach completion logic to loops that did not break"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "sentinel-iteration"\ndescription = "show `iter(callable, sentinel)` for repeated reads until a marker appears"\n\n[[journeys.sections]]\ntitle = "See the protocol behind `for`."\nsummary = "The important mental shift is that loops consume producers through a protocol rather than special-casing lists."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "iterating-over-iterables"\ndescription = "separate value producers from value consumers"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "iterators"\ndescription = "use `iter()` and `next()` to expose the protocol behind `for`"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "generators"\ndescription = "write functions that produce values lazily"\n\n[[journeys.sections]]\ntitle = "Compose lazy value streams."\nsummary = "Iterator pipelines are useful when code can transform values one at a time instead of materializing every intermediate result."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "generator-expressions"\ndescription = "create lazy one-pass streams with expression syntax"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "itertools"\ndescription = "compose iterator streams without materializing every value"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "yield-from"\ndescription = "delegate part of a generator to another iterable"\n\n[[journeys]]\nslug = "shapes"\ntitle = "Shapes"\nsummary = "This journey teaches the core Python habit of choosing a data shape, transforming it directly, and making the result visible."\n\n[[journeys.sections]]\ntitle = "Pick the container that matches the question."\nsummary = "Lists, tuples, dictionaries, and sets answer different questions about order, position, lookup, and uniqueness."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "lists"\ndescription = "store ordered mutable data"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "tuples"\ndescription = "group fixed-position values"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "dicts"\ndescription = "look up values by key"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "sets"\ndescription = "model uniqueness and membership"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "collections-module"\ndescription = "show `deque`, `Counter`, `defaultdict`, and `namedtuple` as specialized shapes"\n\n[[journeys.sections]]\ntitle = "Move between shapes deliberately."\nsummary = "Most everyday Python code is data reshaping, so learners need the idioms for selecting, unpacking, and rebuilding values."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "unpacking"\ndescription = "bind names from structured values"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "slices"\ndescription = "select ranges from sequences"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "comprehensions"\ndescription = "build concrete collections from compact loops"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "comprehension-patterns"\ndescription = "compose filters and nested transformations"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "sorting"\ndescription = "order records with key functions"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "copying-collections"\ndescription = "contrast shallow copies, deep copies, and shared nested data"\n\n[[journeys.sections]]\ntitle = "Cross text and data boundaries."\nsummary = "Programs often receive text and produce structured data, so parsing and serialization belong in the data journey."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "number-parsing"\ndescription = "turn text into numbers safely"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "json"\ndescription = "move structured data across a text boundary"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "regular-expressions"\ndescription = "extract structure from text patterns"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "datetime"\ndescription = "represent dates, times, and durations as typed values"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "csv-data"\ndescription = "show row-shaped text data and dictionary records"\n\n[[journeys]]\nslug = "interfaces"\ntitle = "Interfaces"\nsummary = "This journey shows how Python grows from simple functions to callable APIs, object interfaces, protocols, and metaclasses."\n\n[[journeys.sections]]\ntitle = "Start with functions as named behavior."\nsummary = "Functions are the first abstraction boundary because they name behavior and control how callers provide information."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "functions"\ndescription = "package behavior behind a name"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "keyword-only-arguments"\ndescription = "make important call-site choices explicit"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "positional-only-parameters"\ndescription = "hide parameter names that should remain implementation details"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "args-and-kwargs"\ndescription = "accept flexible call shapes when forwarding or adapting APIs"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "multiple-return-values"\ndescription = "return multiple related values as a tuple"\n\n[[journeys.sections]]\ntitle = "Use functions as values."\nsummary = "Python functions can capture state, be passed around, and wrap other functions."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "scope-global-nonlocal"\ndescription = "control where assignment happens"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "closures"\ndescription = "capture state in nested functions"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "recursion"\ndescription = "solve self-similar problems with a base case"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "lambdas"\ndescription = "write small unnamed functions for expression positions"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "decorators"\ndescription = "wrap behavior without changing call sites"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "partial-functions"\ndescription = "show how to pre-fill arguments with `functools.partial`"\n\n[[journeys.sections]]\ntitle = "Bundle behavior with state."\nsummary = "Classes become useful when data and behavior need to move together behind a stable interface."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "classes"\ndescription = "bundle state and behavior into a new object type"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "inheritance-and-super"\ndescription = "reuse and extend behavior through parent classes"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "dataclasses"\ndescription = "generate common methods for data containers"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "properties"\ndescription = "keep attribute syntax while adding computation or validation"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "special-methods"\ndescription = "connect objects to Python syntax and built-ins"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "truth-and-size"\ndescription = "make objects work with truth tests and `len()`"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "container-protocols"\ndescription = "support membership, lookup, and assignment syntax"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "callable-objects"\ndescription = "make stateful instances callable like functions"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "operator-overloading"\ndescription = "define operators only when the operation is unsurprising"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "attribute-access"\ndescription = "customize fallback lookup and assignment carefully"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "descriptors"\ndescription = "explain the protocol behind methods, properties, and managed attributes"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "metaclasses"\ndescription = "customize class creation when ordinary class tools are not enough"\n\n[[journeys]]\nslug = "types"\ntitle = "Types"\nsummary = "This journey maps Python\'s runtime object model to optional static annotations so learners know what types can and cannot promise."\n\n[[journeys.sections]]\ntitle = "Keep runtime and static analysis separate."\nsummary = "The first lesson is that annotations describe expectations for tools while ordinary Python objects still run the program."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "type-hints"\ndescription = "document expected types and feed type checkers"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "protocols"\ndescription = "describe required behavior by structural shape"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "enums"\ndescription = "name a fixed set of symbolic values"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "runtime-type-checks"\ndescription = "show `type()`, `isinstance()`, and `issubclass()` without turning Python into Java"\n\n[[journeys.sections]]\ntitle = "Describe realistic data shapes."\nsummary = "Typed Python becomes useful when annotations explain optional values, unions, callables, and JSON-like records."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "union-and-optional-types"\ndescription = "show `X | Y` and `None`-aware APIs"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "type-aliases"\ndescription = "name complex types with `type` statements or aliases"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "typed-dicts"\ndescription = "type dictionary records that come from JSON"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "literal-and-final"\ndescription = "express constrained values and names that should not be rebound"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "callable-types"\ndescription = "type functions that are passed as arguments"\n\n[[journeys.sections]]\ntitle = "Scale annotations for reusable libraries."\nsummary = "Advanced typing exists to preserve information across reusable functions, containers, and decorators."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "generics-and-typevar"\ndescription = "write reusable typed containers and functions"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "paramspec"\ndescription = "preserve callable signatures through decorators"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "overloads"\ndescription = "describe APIs whose return type depends on the input shape"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "casts-and-any"\ndescription = "show escape hatches and their tradeoffs"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "newtype"\ndescription = "create distinct static identities for runtime-compatible values"\n\n[[journeys]]\nslug = "reliability"\ntitle = "Reliability"\nsummary = "This journey follows the boundaries where programs fail, clean up, split into modules, communicate with the outside world, and run concurrent work."\n\n[[journeys.sections]]\ntitle = "Make failure explicit."\nsummary = "Robust Python code distinguishes expected absence, broken assumptions, recoverable errors, and domain-specific failures."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "exceptions"\ndescription = "signal and recover from errors"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "assertions"\ndescription = "state internal assumptions"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "exception-chaining"\ndescription = "preserve the cause while translating an error"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "exception-groups"\ndescription = "handle multiple failures together"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "custom-exceptions"\ndescription = "name failures in the language of the problem domain"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "warnings"\ndescription = "signal soft problems and deprecations"\n\n[[journeys.sections]]\ntitle = "Control resource and module boundaries."\nsummary = "Cleanup, deletion, imports, and modules define where responsibilities begin and end."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "context-managers"\ndescription = "pair setup with reliable cleanup"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "delete-statements"\ndescription = "remove names, attributes, and items intentionally"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "modules"\ndescription = "split code into importable files"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "import-aliases"\ndescription = "make imported names clear at use sites"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "packages"\ndescription = "show package directories, `__init__.py`, and public module boundaries"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "virtual-environments"\ndescription = "isolate dependencies for a project"\n\n[[journeys.sections]]\ntitle = "Handle operations that outlive one expression."\nsummary = "I/O, testing, logging, subprocesses, and concurrency require different control boundaries from ordinary expressions."\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "async-await"\ndescription = "await concurrent I/O-shaped work"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "async-iteration-and-context"\ndescription = "consume async streams and cleanup protocols"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "logging"\ndescription = "record operational events without using `print()`"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "testing"\ndescription = "write deterministic tests with `unittest` or `pytest`"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "subprocesses"\ndescription = "run external commands safely"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "threads-and-processes"\ndescription = "contrast concurrency choices beyond `asyncio`"\n\n[[journeys.sections.items]]\nkind = "example"\nvalue = "networking"\ndescription = "make HTTP or socket boundaries explicit"\n\n# Explicit see_also edge labels; default labels are inferred in src/app.py.\n[[see_also_edge_labels]]\nsource = "break-and-continue"\ntarget = "loop-else"\nlabel = "contrast"\n\n[[see_also_edge_labels]]\nsource = "assignment-expressions"\ntarget = "conditionals"\nlabel = "contrast"\n\n[[see_also_edge_labels]]\nsource = "yield-from"\ntarget = "generators"\nlabel = "prerequisite"\n\n[[see_also_edge_labels]]\nsource = "async-iteration-and-context"\ntarget = "async-await"\nlabel = "prerequisite"\n\n[[see_also_edge_labels]]\nsource = "delete-statements"\ntarget = "mutability"\nlabel = "shared mechanism"\n\n[[see_also_edge_labels]]\nsource = "positional-only-parameters"\ntarget = "keyword-only-arguments"\nlabel = "contrast"\n\n[[see_also_edge_labels]]\nsource = "assertions"\ntarget = "exceptions"\nlabel = "alternative"\n\n[[see_also_edge_labels]]\nsource = "exception-chaining"\ntarget = "exceptions"\nlabel = "builds on"\n\n[[see_also_edge_labels]]\nsource = "exception-groups"\ntarget = "exceptions"\nlabel = "alternative"\n\n[[see_also_edge_labels]]\nsource = "operators"\ntarget = "numbers"\nlabel = "related syntax"\n\n[[see_also_edge_labels]]\nsource = "operators"\ntarget = "booleans"\nlabel = "condition building"\n\n[[see_also_edge_labels]]\nsource = "operators"\ntarget = "assignment-expressions"\nlabel = "specialized expression"\n\n[[see_also_edge_labels]]\nsource = "literals"\ntarget = "values"\nlabel = "value surface"\n\n[[see_also_edge_labels]]\nsource = "literals"\ntarget = "strings"\nlabel = "text literal"\n\n[[see_also_edge_labels]]\nsource = "literals"\ntarget = "sets"\nlabel = "container literal"\n\n# Example-cell figure attachments. figure names refer to FIGURES paint functions.\n[[figure_attachments]]\nslug = "mutability"\nanchor = "cell-0"\nfigure = "aliasing-mutation"\ncaption = "Two names share one mutable list — appending through one name changes the object visible through both."\n\n[[figure_attachments]]\nslug = "variables"\nanchor = "cell-0"\nfigure = "variables-bind"\ncaption = "A name is a label that points at an object. Assignment binds the label; the object exists independently."\n\n[[figure_attachments]]\nslug = "lists"\nanchor = "cell-0"\nfigure = "list-append"\ncaption = "Lists are mutable sequences. `.append` extends the same list object — no new list is created."\n\n[[figure_attachments]]\nslug = "dicts"\nanchor = "cell-0"\nfigure = "dict-buckets"\ncaption = "Each key is hashed to a bucket; collisions chain into the next slot. Lookup is constant-time on average."\n\n[[figure_attachments]]\nslug = "unpacking"\nanchor = "cell-0"\nfigure = "unpacking-bind"\ncaption = "Left-side names bind to right-side positions; `*rest` gathers the middle into a list."\n\n[[figure_attachments]]\nslug = "comprehensions"\nanchor = "cell-0"\nfigure = "comprehension-equivalence"\ncaption = "A comprehension is a compact spelling of the equivalent for-loop with append, made into one expression."\n\n[[figure_attachments]]\nslug = "classes"\nanchor = "cell-0"\nfigure = "class-triangle"\ncaption = "Every Python value sits on the instance → class → type triangle; the metaclass is the type of the class."\n\n[[figure_attachments]]\nslug = "inheritance-and-super"\nanchor = "cell-0"\nfigure = "mro-chain"\ncaption = "Multiple inheritance forms a graph; C3 linearisation flattens it into the MRO Python uses for attribute lookup."\n\n[[figure_attachments]]\nslug = "dataclasses"\nanchor = "cell-0"\nfigure = "dataclass-fields"\ncaption = "Field declarations become the generated __init__ signature: declaration is the constructor."\n\n[[figure_attachments]]\nslug = "special-methods"\nanchor = "cell-0"\nfigure = "operator-dispatch"\ncaption = "Operators are method calls. `a + b` dispatches to `a.__add__(b)`; the data model exposes the syntax."\n\n[[figure_attachments]]\nslug = "decorators"\nanchor = "cell-0"\nfigure = "decorator-rebind"\ncaption = "@dec rebinds the name to wrapper(f₀); the original function survives only in the wrapper\'s closure cell."\n\n[[figure_attachments]]\nslug = "recursion"\nanchor = "cell-1"\nfigure = "call-stack"\ncaption = "Each call pushes a new frame with the same name and a smaller argument; the base case unwinds back up the stack."\n\n[[figure_attachments]]\nslug = "exception-chaining"\nanchor = "cell-0"\nfigure = "exception-cause-context"\ncaption = "`raise X from Y` sets `__cause__` (explicit); raising during except sets `__context__` (implicit)."\n\n[[figure_attachments]]\nslug = "hello-world"\nanchor = "cell-0"\nfigure = "program-output"\ncaption = "Every Python program starts as source and produces text on standard output. The smallest mental model."\n\n[[figure_attachments]]\nslug = "numbers"\nanchor = "cell-0"\nfigure = "number-lines"\ncaption = "Ints have unbounded precision; floats use IEEE doubles whose representable values thin out near the extremes."\n\n[[figure_attachments]]\nslug = "operators"\nanchor = "cell-0"\nfigure = "expression-tree"\ncaption = "An expression like `(2 + 3) * 4` parses as a tree; operator precedence and parentheses determine its shape."\n\n[[figure_attachments]]\nslug = "none"\nanchor = "cell-0"\nfigure = "none-singleton"\ncaption = "`None` is a single object: every name that points at None points at the same object."\n\n[[figure_attachments]]\nslug = "equality-and-identity"\nanchor = "cell-0"\nfigure = "identity-and-equality"\ncaption = "Two names can share one object (`is` and `==` both true) or hold two equal-but-distinct objects (only `==` true)."\n\n[[figure_attachments]]\nslug = "strings"\nanchor = "cell-0"\nfigure = "codepoints-bytes"\ncaption = "Strings are sequences of Unicode codepoints. UTF-8 encoding turns them into bytes; `é` takes two bytes, `c` takes one."\n\n[[figure_attachments]]\nslug = "for-loops"\nanchor = "cell-1"\nfigure = "iterator-unroll"\ncaption = "Each call to next() advances the caret one cell along the iterable — the same shape behind range(), strings, and any sequence."\n\n[[figure_attachments]]\nslug = "sorting"\nanchor = "cell-1"\nfigure = "sort-stability"\ncaption = "Python\'s sort is stable: items with equal keys keep their original order, so chained sorts compose predictably."\n\n[[figure_attachments]]\nslug = "keyword-only-arguments"\nanchor = "cell-0"\nfigure = "kw-only-separator"\ncaption = "A bare `*` divides positional or keyword arguments from keyword-only ones; callers must pass `c` and `d` by name."\n\n[[figure_attachments]]\nslug = "positional-only-parameters"\nanchor = "cell-0"\nfigure = "positional-only-separator"\ncaption = "A bare `/` divides positional-only arguments from positional-or-keyword ones; callers cannot name `a` or `b`."\n\n[[figure_attachments]]\nslug = "closures"\nanchor = "cell-0"\nfigure = "closure-cell"\ncaption = "The inner function keeps a reference into the outer scope\'s cell, so the captured factor survives the outer return."\n\n[[figure_attachments]]\nslug = "scope-global-nonlocal"\nanchor = "cell-0"\nfigure = "scope-rings"\ncaption = "Name lookup walks LEGB — local, enclosing, global, built-in — outward, returning the first binding it finds."\n\n[[figure_attachments]]\nslug = "generators"\nanchor = "cell-0"\nfigure = "generator-ribbon"\ncaption = "A generator\'s body is a timeline cut by yield gates: each next() advances to the next gate; locals survive the pause."\n\n[[figure_attachments]]\nslug = "type-hints"\nanchor = "cell-0"\nfigure = "annotation-ghost"\ncaption = "Annotations describe expected types for tools; the runtime accepts any object regardless."\n\n[[figure_attachments]]\nslug = "exceptions"\nanchor = "cell-0"\nfigure = "exception-lanes"\ncaption = "try, except, else, and finally as parallel lanes; a single coral path traces what actually runs."\n\n[[figure_attachments]]\nslug = "context-managers"\nanchor = "cell-0"\nfigure = "context-bowtie"\ncaption = "A context manager pairs setup with reliable cleanup; the raise path still routes through __exit__."\n\n[[figure_attachments]]\nslug = "async-await"\nanchor = "cell-1"\nfigure = "async-swimlane"\ncaption = "On await, the coroutine yields to the loop; the loop runs other work and resumes when the awaitable is ready."\n\n[[figure_attachments]]\nslug = "iterators"\nanchor = "cell-0"\nfigure = "iter-protocol"\ncaption = "iter() exposes the iterator behind for; next() pulls one value at a time until exhausted."\n\n[[figure_attachments]]\nslug = "slices"\nanchor = "cell-0"\nfigure = "slice-ruler"\ncaption = "Slice indices sit between cells; [:3] and [3:] partition the sequence at index 3, never overlapping or losing an item."\n\n[[figure_attachments]]\nslug = "operator-overloading"\nanchor = "cell-0"\nfigure = "operator-dispatch"\ncaption = "Defining `__add__` on a class lets `+` dispatch into the class\'s own behavior."\n\n[[figure_attachments]]\nslug = "iterator-vs-iterable"\nanchor = "cell-0"\nfigure = "iter-protocol"\ncaption = "An iterable knows how to produce an iterator (via iter()); the iterator knows how to produce values (via next())."\n\n[[figure_attachments]]\nslug = "type-aliases"\nanchor = "cell-0"\nfigure = "type-alias-name"\ncaption = "A type alias names a complex annotation once so call sites read as the domain meaning, not the type composition."\n\n[[figure_attachments]]\nslug = "typed-dicts"\nanchor = "cell-0"\nfigure = "typed-dict-shape"\ncaption = "TypedDict gives each key a typed value, so `obj[\'x\']` is checked against the declared shape."\n\n[[figure_attachments]]\nslug = "union-and-optional-types"\nanchor = "cell-0"\nfigure = "union-types"\ncaption = "`int | str | None` says one slot may hold any of three shapes — including expected absence."\n\n[[figure_attachments]]\nslug = "generics-and-typevar"\nanchor = "cell-0"\nfigure = "generic-preservation"\ncaption = "A generic preserves the input type through the call: the same T flows in and out of fn[T]."\n\n[[figure_attachments]]\nslug = "abstract-base-classes"\nanchor = "cell-0"\nfigure = "class-triangle"\ncaption = "An ABC sits on the same triangle as concrete classes; subclasses inherit the abstract methods they must implement."\n\n[[figure_attachments]]\nslug = "copying-collections"\nanchor = "cell-0"\nfigure = "aliasing-mutation"\ncaption = "Without copy() two names share the same object; mutating through one is visible through the other."\n\n[[figure_attachments]]\nslug = "truth-and-size"\nanchor = "cell-0"\nfigure = "truth-and-size"\ncaption = "bool(x) calls __bool__ first; if absent, __len__() != 0; if neither, defaults to True."\n\n[[figure_attachments]]\nslug = "descriptors"\nanchor = "cell-0"\nfigure = "descriptor-protocol"\ncaption = "Attribute access on an instance routes through the descriptor\'s __get__/__set__/__delete__ when the attribute is a descriptor."\n\n[[figure_attachments]]\nslug = "bound-and-unbound-methods"\nanchor = "cell-0"\nfigure = "bound-unbound"\ncaption = "Accessing a method via an instance binds self; accessing it via the class returns the underlying function."\n\n[[figure_attachments]]\nslug = "classmethods-and-staticmethods"\nanchor = "cell-0"\nfigure = "method-kinds"\ncaption = "Three method kinds, three first-argument conventions: classmethod gets the class, staticmethod gets nothing, instance gets self."\n\n[[figure_attachments]]\nslug = "callable-objects"\nanchor = "cell-0"\nfigure = "callable-objects"\ncaption = "Defining __call__ makes any object callable; functions are just one shape that satisfies this protocol."\n\n[[figure_attachments]]\nslug = "attribute-access"\nanchor = "cell-0"\nfigure = "attribute-lookup"\ncaption = "obj.x checks instance __dict__, then class __dict__, then __getattr__; the first hit wins."\n\n[[figure_attachments]]\nslug = "guard-clauses"\nanchor = "cell-0"\nfigure = "guard-clauses"\ncaption = "Early returns handle the exceptional cases first so the main work is the body of the function, not its tail."\n\n[[figure_attachments]]\nslug = "bytes-and-bytearray"\nanchor = "cell-0"\nfigure = "bytes-vs-bytearray"\ncaption = "bytes is a frozen sequence of integers; bytearray is the mutable counterpart with append/extend/etc."\n\n[[figure_attachments]]\nslug = "sentinel-iteration"\nanchor = "cell-0"\nfigure = "sentinel-iteration"\ncaption = "`iter(callable, sentinel)` calls the callable repeatedly, stopping when it returns the sentinel."\n\n[[figure_attachments]]\nslug = "partial-functions"\nanchor = "cell-0"\nfigure = "partial-functions"\ncaption = "`functools.partial(f, 1)` pre-fills `a=1`, returning a thinner callable `g(b, c)` that only needs the rest."\n\n[[figure_attachments]]\nslug = "args-and-kwargs"\nanchor = "cell-0"\nfigure = "args-kwargs"\ncaption = "*args captures the extra positionals as a tuple; **kwargs captures the extra keywords as a dict."\n\n[[figure_attachments]]\nslug = "multiple-return-values"\nanchor = "cell-0"\nfigure = "multiple-return"\ncaption = "A function returning multiple values really returns one tuple; the caller unpacks it into named bindings."\n\n[[figure_attachments]]\nslug = "lambdas"\nanchor = "cell-0"\nfigure = "lambda-expression"\ncaption = "A lambda is a function literal: parameters before the colon, a single expression after, no statement body."\n\n[[figure_attachments]]\nslug = "properties"\nanchor = "cell-0"\nfigure = "property-fork"\ncaption = "When x is a property, attribute access routes through fget/fset instead of touching __dict__."\n\n[[figure_attachments]]\nslug = "metaclasses"\nanchor = "cell-0"\nfigure = "metaclass-triangle"\ncaption = "A metaclass is the type of a class, just as a class is the type of its instances; type is the default metaclass."\n\n[[figure_attachments]]\nslug = "modules"\nanchor = "cell-0"\nfigure = "sys-path-resolution"\ncaption = "An import walks sys.path entry by entry; the first directory containing the module wins."\n\n[[figure_attachments]]\nslug = "import-aliases"\nanchor = "cell-0"\nfigure = "import-alias"\ncaption = "`import x as y` binds the name y to the same module object x would have."\n\n[[figure_attachments]]\nslug = "protocols"\nanchor = "cell-0"\nfigure = "protocol-check"\ncaption = "An object satisfies a protocol structurally — by having the required methods — not by inheriting it."\n\n[[figure_attachments]]\nslug = "enums"\nanchor = "cell-0"\nfigure = "enum-members"\ncaption = "An enum names a fixed set of symbolic values; no new members appear at runtime."\n\n[[figure_attachments]]\nslug = "datetime"\nanchor = "cell-0"\nfigure = "datetime-instant"\ncaption = "An aware datetime carries a UTC offset; one instant in time reads differently on two clocks."\n\n[[figure_attachments]]\nslug = "json"\nanchor = "cell-0"\nfigure = "json-python-mapping"\ncaption = "Six type pairs bridge the JSON text boundary; each json value maps to one Python type."\n\n[[figure_attachments]]\nslug = "regular-expressions"\nanchor = "cell-0"\nfigure = "regex-anchors"\ncaption = "^ and $ anchor the pattern; quantifiers like {2} bound how many times a token repeats."\n\n[[figure_attachments]]\nslug = "number-parsing"\nanchor = "cell-0"\nfigure = "number-parse"\ncaption = "int() turns text into a typed number; malformed input raises ValueError instead of guessing."\n\n[[figure_attachments]]\nslug = "string-formatting"\nanchor = "cell-1"\nfigure = "format-spec"\ncaption = "The format spec is a railroad of named optional fields: alignment, sign, width, precision, type."\n\n[[figure_attachments]]\nslug = "truthiness"\nanchor = "cell-0"\nfigure = "truthy-check"\ncaption = "bool(x) is True except for a small fixed set: 0, 0.0, \\"\\", [], {}, None, False."\n\n[[figure_attachments]]\nslug = "booleans"\nanchor = "cell-0"\nfigure = "boolean-truth-table"\ncaption = "`a and b` returns True only when both are True; otherwise it returns the first falsy value."\n\n[[figure_attachments]]\nslug = "sets"\nanchor = "cell-0"\nfigure = "set-buckets"\ncaption = "Sets are hash buckets without values; `x in s` averages O(1) regardless of size."\n\n[[figure_attachments]]\nslug = "tuples"\nanchor = "cell-0"\nfigure = "tuple-frozen"\ncaption = "Tuples are ordered, immutable sequences; positions matter, contents do not change once constructed."\n\n[[figure_attachments]]\nslug = "values"\nanchor = "cell-0"\nfigure = "value-types"\ncaption = "Every literal is an object with a type; the type carries the behaviour, not the variable name."\n\n[[figure_attachments]]\nslug = "yield-from"\nanchor = "cell-0"\nfigure = "yield-delegation"\ncaption = "`yield from inner` delegates iteration to an inner generator; its yields surface here unchanged."\n\n[[figure_attachments]]\nslug = "itertools"\nanchor = "cell-0"\nfigure = "itertools-chain"\ncaption = "chain stitches two iterables into one stream without materialising either: values arrive lazily."\n\n[[figure_attachments]]\nslug = "assertions"\nanchor = "cell-0"\nfigure = "assertion-check"\ncaption = "assert tests a condition; True passes silently, False raises AssertionError with the optional message."\n\n[[figure_attachments]]\nslug = "custom-exceptions"\nanchor = "cell-0"\nfigure = "custom-exception-chain"\ncaption = "Subclassing an existing exception gains a domain name without changing semantics."\n\n[[figure_attachments]]\nslug = "exception-groups"\nanchor = "cell-0"\nfigure = "exception-group-peel"\ncaption = "except* peels matched leaves out of an ExceptionGroup; survivors regroup and propagate."\n\n[[figure_attachments]]\nslug = "delete-statements"\nanchor = "cell-0"\nfigure = "delete-name-erased"\ncaption = "`del x` removes the name; the object survives if any other reference holds it, otherwise gets collected."\n\n[[figure_attachments]]\nslug = "conditionals"\nanchor = "cell-0"\nfigure = "branch-fork"\ncaption = "A predicate sorts a value into one of several branches; if/elif/else is the explicit spelling."\n\n[[figure_attachments]]\nslug = "match-statements"\nanchor = "cell-0"\nfigure = "match-dispatch-ladder"\ncaption = "match dispatches by pattern shape; the value flows down the patterns and the first match wins."\n\n[[figure_attachments]]\nslug = "assignment-expressions"\nanchor = "cell-0"\nfigure = "naming-decisions"\ncaption = "The walrus binds a name during the surrounding expression; one expression, two outputs."\n\n[[figure_attachments]]\nslug = "iterating-over-iterables"\nanchor = "cell-0"\nfigure = "iter-protocol"\ncaption = "`for` desugars to iter()+next(): one iter() call, then next() until StopIteration ends the loop."\n\n[[figure_attachments]]\nslug = "generator-expressions"\nanchor = "cell-1"\nfigure = "lazy-stream"\ncaption = "A generator expression composes filter and map lazily; values flow only when next() pulls them."\n\n[[figure_attachments]]\nslug = "async-iteration-and-context"\nanchor = "cell-0"\nfigure = "async-swimlane"\ncaption = "async iteration and async with both rest on the same loop-vs-coroutine handoff as await."\n\n[[figure_attachments]]\nslug = "loop-else"\nanchor = "cell-0"\nfigure = "loop-else-gate"\ncaption = "The loop\'s else branch runs only when the loop falls through naturally; break skips it."\n\n[[figure_attachments]]\nslug = "break-and-continue"\nanchor = "cell-0"\nfigure = "early-exit"\ncaption = "break exits the loop; continue skips to the next iteration. Both interrupt the natural fall-through."\n\n[[figure_attachments]]\nslug = "comprehension-patterns"\nanchor = "cell-0"\nfigure = "comprehension-equivalence"\ncaption = "Nested clauses compose left to right; the comprehension is still equivalent to a for-loop with append."\n\n[[figure_attachments]]\nslug = "container-protocols"\nanchor = "cell-0"\nfigure = "container-methods"\ncaption = "Container syntax routes to narrow protocol methods: assignment to __setitem__, membership to __contains__, lookup to __getitem__."\n\n[[figure_attachments]]\nslug = "functions"\nanchor = "cell-0"\nfigure = "function-with-body"\ncaption = "A function takes inputs, evaluates a body, and returns a value: `greet(\'Ada\')` produces `\'Hello, Ada\'`."\n\n[[figure_attachments]]\nslug = "constants"\nanchor = "cell-0"\nfigure = "variables-bind"\ncaption = "UPPER_CASE is a naming convention, not a language constraint; the binding behaves like any other variable."\n\n[[figure_attachments]]\nslug = "while-loops"\nanchor = "cell-0"\nfigure = "while-backedge"\ncaption = "`while` repeats the body while the test stays true; the back-edge returns control to the test before each pass."\n\n[[figure_attachments]]\nslug = "while-loops"\nanchor = "cell-0"\nfigure = "loop-repetition"\ncaption = "Each loop shape has its own stopping rule: `for` ends when the iterable is exhausted, `while` when the condition turns false, sentinel iteration when the marker value appears."\n\n[[figure_attachments]]\nslug = "advanced-match-patterns"\nanchor = "cell-0"\nfigure = "match-pattern-variants"\ncaption = "Capture, alternative, guard, and class patterns each name a different way a value can match a case."\n\n[[figure_attachments]]\nslug = "literals"\nanchor = "cell-0"\nfigure = "literal-forms"\ncaption = "Each Python type has its own literal spellings; ints accept decimal, hex, and binary; strings accept either quote."\n\n[[figure_attachments]]\nslug = "packages"\nanchor = "cell-0"\nfigure = "package-tree"\ncaption = "A directory with __init__.py becomes an importable package; submodules and subpackages nest beneath it."\n\n[[figure_attachments]]\nslug = "virtual-environments"\nanchor = "cell-0"\nfigure = "venv-boundary"\ncaption = "A venv carries its own interpreter and site-packages, isolating a project\'s dependencies from the system."\n\n[[figure_attachments]]\nslug = "subprocesses"\nanchor = "cell-0"\nfigure = "subprocess-spawn"\ncaption = "subprocess.run spawns a child process and captures its stdout, stderr, and exit code as portable evidence."\n\n[[figure_attachments]]\nslug = "logging"\nanchor = "cell-0"\nfigure = "logging-levels"\ncaption = "Five severity levels; the logger\'s configured threshold drops everything below it."\n\n[[figure_attachments]]\nslug = "testing"\nanchor = "cell-0"\nfigure = "aaa-pattern"\ncaption = "arrange-act-assert: set up the state, perform the behavior under test, compare the result to expectations."\n\n[[figure_attachments]]\nslug = "networking"\nanchor = "cell-0"\nfigure = "socket-byte-boundary"\ncaption = "Text crosses the socket as bytes — `encode` marks the python → wire boundary, `decode` brings the bytes back to a Python `str`."\n\n[[figure_attachments]]\nslug = "threads-and-processes"\nanchor = "cell-0"\nfigure = "gil-lanes"\ncaption = "Threads share memory but the GIL serialises Python bytecode; processes run in parallel with isolated memory."\n\n[[figure_attachments]]\nslug = "casts-and-any"\nanchor = "cell-0"\nfigure = "cast-escape"\ncaption = "cast(T, x) tells the type checker to treat x as T; the runtime is unaffected."\n\n[[figure_attachments]]\nslug = "newtype"\nanchor = "cell-0"\nfigure = "newtype-phantom"\ncaption = "NewType creates a distinct static identity backed by the same runtime type — UserId is int with a name."\n\n[[figure_attachments]]\nslug = "overloads"\nanchor = "cell-0"\nfigure = "overload-signatures"\ncaption = "@overload declares static call signatures for double(int) and double(str); one runtime implementation handles both."\n\n[[figure_attachments]]\nslug = "paramspec"\nanchor = "cell-0"\nfigure = "paramspec-preserve"\ncaption = "ParamSpec preserves the wrapped function\'s signature through a decorator, parameter for parameter."\n\n[[figure_attachments]]\nslug = "literal-and-final"\nanchor = "cell-0"\nfigure = "literal-constrained"\ncaption = "Literal narrows a slot to a fixed set of constant values; Final says the binding will not change."\n\n[[figure_attachments]]\nslug = "callable-types"\nanchor = "cell-0"\nfigure = "callable-type"\ncaption = "A Callable annotation describes the callback slot: this argument list must produce this return type."\n\n[[figure_attachments]]\nslug = "runtime-type-checks"\nanchor = "cell-0"\nfigure = "isinstance-check"\ncaption = "isinstance and issubclass ask the runtime; the answer is a bool, not a static type refinement."\n\n[[figure_attachments]]\nslug = "collections-module"\nanchor = "cell-0"\nfigure = "collections-containers"\ncaption = "Four specialised containers for shapes the built-in types don\'t cover well: deque, Counter, defaultdict, namedtuple."\n\n[[figure_attachments]]\nslug = "structured-data-shapes"\nanchor = "cell-0"\nfigure = "structured-shapes"\ncaption = "The same record can be a mutable dataclass, an immutable NamedTuple, or a runtime dict described by TypedDict."\n\n[[figure_attachments]]\nslug = "csv-data"\nanchor = "cell-0"\nfigure = "csv-records"\ncaption = "CSV files are rows of records; each line has the same columns in the same order."\n\n[[figure_attachments]]\nslug = "warnings"\nanchor = "cell-0"\nfigure = "warning-signal"\ncaption = "A warning is a soft signal: the message is reported, but execution continues unless filters elevate it."\n\n[[figure_attachments]]\nslug = "object-lifecycle"\nanchor = "cell-0"\nfigure = "object-lifecycle"\ncaption = "Names keep the same object reachable; deleting one name matters only when it removes the last reference."\n\n# Journey-section figures keyed by the visible section title.\n[[journey_section_figures]]\nsection = "Start with executable evidence."\nfigure = "runtime-evidence-loop"\ncaption = "Examples are evidence loops: source, a run step, and visible output stay together."\n\n[[journey_section_figures]]\nsection = "Separate value, identity, and absence."\nfigure = "runtime-object-axes"\ncaption = "Runtime objects answer separate questions: equal value, same identity, or the singleton that marks absence."\n\n[[journey_section_figures]]\nsection = "Read expressions as object operations."\nfigure = "runtime-expression-model"\ncaption = "Expression syntax enters the data model; object methods produce the result."\n\n[[journey_section_figures]]\nsection = "Choose between paths."\nfigure = "control-decision-map"\ncaption = "Facts flow into one decision point; exactly one branch owns the next step."\n\n[[journey_section_figures]]\nsection = "Name and shape decisions."\nfigure = "control-fact-shape"\ncaption = "Name a fact when a condition needs it; match shape when the data structure is the decision."\n\n[[journey_section_figures]]\nsection = "Stop as soon as the answer is known."\nfigure = "control-stop-boundary"\ncaption = "Early exits draw a boundary: once the answer is found, the tail stays unread."\n\n[[journey_section_figures]]\nsection = "Choose the right loop shape."\nfigure = "iteration-loop-selector"\ncaption = "Choose the loop from its stopping rule: exhaustion, condition, or sentinel marker."\n\n[[journey_section_figures]]\nsection = "See the protocol behind `for`."\nfigure = "iteration-protocol-map"\ncaption = "for is surface syntax; iter() creates an iterator and next() pulls values until StopIteration."\n\n[[journey_section_figures]]\nsection = "Compose lazy value streams."\nfigure = "iteration-lazy-pull"\ncaption = "Lazy pipelines run from the consumer\'s pull: next() requests one value through each stage."\n\n[[journey_section_figures]]\nsection = "Pick the container that matches the question."\nfigure = "container-questions"\ncaption = "Each container answers a different question: ordered, fixed, lookup, unique."\n\n[[journey_section_figures]]\nsection = "Move between shapes deliberately."\nfigure = "reshape-pipeline"\ncaption = "Most everyday code reshapes data: one input, one transform, one new value."\n\n[[journey_section_figures]]\nsection = "Cross text and data boundaries."\nfigure = "text-data-boundary"\ncaption = "Programs receive text and produce structured data; parsing makes the boundary explicit."\n\n[[journey_section_figures]]\nsection = "Start with functions as named behavior."\nfigure = "function-signature"\ncaption = "A function is the first abstraction boundary: arguments in, body, return value out."\n\n[[journey_section_figures]]\nsection = "Use functions as values."\nfigure = "function-as-value"\ncaption = "Functions are first-class values. A second name binds to the same function object."\n\n[[journey_section_figures]]\nsection = "Bundle behavior with state."\nfigure = "class-with-state"\ncaption = "Classes group fields and methods so data and behavior move together behind one interface."\n\n[[journey_section_figures]]\nsection = "Keep runtime and static analysis separate."\nfigure = "type-runtime-static-split"\ncaption = "Runtime values run the program; static tools inspect separate annotations and report before execution."\n\n[[journey_section_figures]]\nsection = "Describe realistic data shapes."\nfigure = "type-shape-catalog"\ncaption = "Real data contracts combine fields, variants, and expected absence instead of one scalar type."\n\n[[journey_section_figures]]\nsection = "Scale annotations for reusable libraries."\nfigure = "type-library-contract"\ncaption = "Reusable APIs carry caller contracts through the library boundary with generics, parameters, and overloads."\n\n[[journey_section_figures]]\nsection = "Make failure explicit."\nfigure = "reliability-signal-map"\ncaption = "Different failure shapes need explicit signals: assertions, recovery, chained causes, or warnings."\n\n[[journey_section_figures]]\nsection = "Control resource and module boundaries."\nfigure = "reliability-boundary-map"\ncaption = "Reliable programs name their boundaries: resources clean up, modules import, environments constrain runtime."\n\n[[journey_section_figures]]\nsection = "Handle operations that outlive one expression."\nfigure = "reliability-operation-boundary"\ncaption = "Async, threaded, test, and logging work cross an operation boundary before evidence comes back."\n\n# Curated scores for example-cell figures.\n[[example_figure_scores]]\nslug = "variables"\nscore = 9.5\ncomment = "the canonical name → object picture"\n\n[[example_figure_scores]]\nslug = "mutability"\nscore = 9.5\ncomment = "three-state small multiple of aliased mutation"\n\n[[example_figure_scores]]\nslug = "copying-collections"\nscore = 9.5\ncomment = "same picture as mutability, perfect match"\n\n[[example_figure_scores]]\nslug = "hello-world"\nscore = 9.0\ncomment = "program → output, smallest mechanism"\n\n[[example_figure_scores]]\nslug = "numbers"\nscore = 9.0\ncomment = "int unbounded vs float thinning, both registers"\n\n[[example_figure_scores]]\nslug = "operators"\nscore = 9.0\ncomment = "expression tree mechanism"\n\n[[example_figure_scores]]\nslug = "none"\nscore = 9.0\ncomment = "three names converging on one None"\n\n[[example_figure_scores]]\nslug = "equality-and-identity"\nscore = 9.0\ncomment = "shared vs separate object, side-by-side"\n\n[[example_figure_scores]]\nslug = "strings"\nscore = 9.0\ncomment = "codepoints + bytes registers"\n\n[[example_figure_scores]]\nslug = "for-loops"\nscore = 9.0\ncomment = "4-row caret advance"\n\n[[example_figure_scores]]\nslug = "sorting"\nscore = 9.0\ncomment = "stability ribbons preserved across keys"\n\n[[example_figure_scores]]\nslug = "keyword-only-arguments"\nscore = 9.0\ncomment = "signature with explicit `*` separator"\n\n[[example_figure_scores]]\nslug = "positional-only-parameters"\nscore = 9.0\ncomment = "signature with explicit `/` separator"\n\n[[example_figure_scores]]\nslug = "closures"\nscore = 9.0\ncomment = "captured cell reference"\n\n[[example_figure_scores]]\nslug = "scope-global-nonlocal"\nscore = 9.0\ncomment = "LEGB nested rings"\n\n[[example_figure_scores]]\nslug = "recursion"\nscore = 9.0\ncomment = "stacked frames with same name, different argument"\n\n[[example_figure_scores]]\nslug = "lists"\nscore = 9.0\ncomment = "cells with append mechanism"\n\n[[example_figure_scores]]\nslug = "dicts"\nscore = 9.0\ncomment = "hash buckets with collision chain"\n\n[[example_figure_scores]]\nslug = "slices"\nscore = 9.0\ncomment = "ruler with bracket overlay"\n\n[[example_figure_scores]]\nslug = "comprehensions"\nscore = 9.0\ncomment = "comprehension over equivalent for-loop"\n\n[[example_figure_scores]]\nslug = "type-hints"\nscore = 9.0\ncomment = "ghost annotations over runtime values"\n\n[[example_figure_scores]]\nslug = "generators"\nscore = 9.0\ncomment = "ribbon cut by yield gates"\n\n[[example_figure_scores]]\nslug = "exceptions"\nscore = 9.0\ncomment = "try/except/else/finally lanes with traced path"\n\n[[example_figure_scores]]\nslug = "context-managers"\nscore = 9.0\ncomment = "enter / body / exit bowtie"\n\n[[example_figure_scores]]\nslug = "async-await"\nscore = 9.0\ncomment = "loop/coro swimlane with await handoffs"\n\n[[example_figure_scores]]\nslug = "classes"\nscore = 9.0\ncomment = "instance/class/type triangle"\n\n[[example_figure_scores]]\nslug = "inheritance-and-super"\nscore = 9.0\ncomment = "MRO chain with diamond ghost"\n\n[[example_figure_scores]]\nslug = "dataclasses"\nscore = 9.0\ncomment = "fields → generated __init__ signature"\n\n[[example_figure_scores]]\nslug = "decorators"\nscore = 9.0\ncomment = "before/after rebinding through cell"\n\n[[example_figure_scores]]\nslug = "special-methods"\nscore = 9.0\ncomment = "syntax → method dispatch"\n\n[[example_figure_scores]]\nslug = "unpacking"\nscore = 9.0\ncomment = "binding-line mechanism with *rest"\n\n[[example_figure_scores]]\nslug = "exception-chaining"\nscore = 9.0\ncomment = "__cause__ vs __context__ distinguished"\n\n[[example_figure_scores]]\nslug = "iterating-over-iterables"\nscore = 9.0\ncomment = "iter() exposes the iterator"\n\n[[example_figure_scores]]\nslug = "iterators"\nscore = 9.0\ncomment = "three-state machine"\n\n[[example_figure_scores]]\nslug = "iterator-vs-iterable"\nscore = 9.0\ncomment = "the protocol exposed"\n\n[[example_figure_scores]]\nslug = "container-protocols"\nscore = 9.0\ncomment = "syntax routes to __setitem__, __contains__, __getitem__"\n\n[[example_figure_scores]]\nslug = "operator-overloading"\nscore = 9.0\ncomment = "dispatch arrow"\n\n[[example_figure_scores]]\nslug = "union-and-optional-types"\nscore = 9.0\ncomment = "type fork to several shapes"\n\n[[example_figure_scores]]\nslug = "abstract-base-classes"\nscore = 9.0\ncomment = "same triangle as concrete classes"\n\n[[example_figure_scores]]\nslug = "conditionals"\nscore = 9.0\ncomment = "predicate forks value to branch"\n\n[[example_figure_scores]]\nslug = "match-statements"\nscore = 9.0\ncomment = "dispatch ladder; first match wins"\n\n[[example_figure_scores]]\nslug = "advanced-match-patterns"\nscore = 9.0\ncomment = "four pattern variants"\n\n[[example_figure_scores]]\nslug = "loop-else"\nscore = 9.0\ncomment = "fell-through vs broke, two outcomes"\n\n[[example_figure_scores]]\nslug = "while-loops"\nscore = 9.0\ncomment = "back-edge mechanism plus the three stopping rules"\n\n[[example_figure_scores]]\nslug = "type-aliases"\nscore = 9.0\ncomment = "complex annotation collapses to a name"\n\n[[example_figure_scores]]\nslug = "typed-dicts"\nscore = 9.0\ncomment = "keys with declared value types"\n\n[[example_figure_scores]]\nslug = "comprehension-patterns"\nscore = 9.0\ncomment = "nested clauses compose"\n\n[[example_figure_scores]]\nslug = "lambdas"\nscore = 9.0\ncomment = "function literal: params / expression"\n\n[[example_figure_scores]]\nslug = "string-formatting"\nscore = 9.0\ncomment = "format-spec railroad"\n\n[[example_figure_scores]]\nslug = "regular-expressions"\nscore = 9.0\ncomment = "pattern ruler with anchors"\n\n[[example_figure_scores]]\nslug = "json"\nscore = 9.0\ncomment = "two-column type mapping"\n\n[[example_figure_scores]]\nslug = "metaclasses"\nscore = 9.0\ncomment = "extended triangle to metaclass"\n\n[[example_figure_scores]]\nslug = "datetime"\nscore = 9.0\ncomment = "one instant, two clock offsets"\n\n[[example_figure_scores]]\nslug = "values"\nscore = 9.0\ncomment = "every literal is a typed object"\n\n[[example_figure_scores]]\nslug = "literals"\nscore = 9.0\ncomment = "literal spellings per type"\n\n[[example_figure_scores]]\nslug = "booleans"\nscore = 9.0\ncomment = "2×2 truth table"\n\n[[example_figure_scores]]\nslug = "sets"\nscore = 9.0\ncomment = "hash buckets without values"\n\n[[example_figure_scores]]\nslug = "yield-from"\nscore = 9.0\ncomment = "stitched ribbons; delegation"\n\n[[example_figure_scores]]\nslug = "generator-expressions"\nscore = 9.0\ncomment = "lazy filter→map pipeline"\n\n[[example_figure_scores]]\nslug = "async-iteration-and-context"\nscore = 9.0\ncomment = "loop/coro lanes with await yields"\n\n[[example_figure_scores]]\nslug = "assignment-expressions"\nscore = 9.0\ncomment = "walrus binds while comparing"\n\n[[example_figure_scores]]\nslug = "break-and-continue"\nscore = 9.0\ncomment = "early exit at first match"\n\n[[example_figure_scores]]\nslug = "delete-statements"\nscore = 9.0\ncomment = "name erased; object survives if referenced"\n\n[[example_figure_scores]]\nslug = "exception-groups"\nscore = 9.0\ncomment = "except* peels matching leaves"\n\n[[example_figure_scores]]\nslug = "custom-exceptions"\nscore = 9.0\ncomment = "subclass chain to a domain name"\n\n[[example_figure_scores]]\nslug = "modules"\nscore = 9.0\ncomment = "sys.path resolution; first hit wins"\n\n[[example_figure_scores]]\nslug = "protocols"\nscore = 9.0\ncomment = "structural duck check"\n\n[[example_figure_scores]]\nslug = "enums"\nscore = 9.0\ncomment = "closed set of symbolic values"\n\n[[example_figure_scores]]\nslug = "functions"\nscore = 9.0\ncomment = "specific call: greet(\'Ada\') → \'Hello, Ada\'"\n\n[[example_figure_scores]]\nslug = "constants"\nscore = 9.0\ncomment = "name binding; UPPER_CASE is convention"\n\n[[example_figure_scores]]\nslug = "import-aliases"\nscore = 9.0\ncomment = "two names bind to the same module"\n\n[[example_figure_scores]]\nslug = "number-parsing"\nscore = 9.0\ncomment = "int() success path vs ValueError"\n\n[[example_figure_scores]]\nslug = "tuples"\nscore = 9.0\ncomment = "frozen sequence with struck-through .append"\n\n[[example_figure_scores]]\nslug = "truthiness"\nscore = 9.0\ncomment = "bool(x) with the falsy set as a strip"\n\n[[example_figure_scores]]\nslug = "itertools"\nscore = 9.0\ncomment = "chain joins two iterables into one stream"\n\n[[example_figure_scores]]\nslug = "assertions"\nscore = 9.0\ncomment = "True passes, False raises"\n\n[[example_figure_scores]]\nslug = "descriptors"\nscore = 9.0\ncomment = "get/set/delete protocol routed through descriptor"\n\n[[example_figure_scores]]\nslug = "attribute-access"\nscore = 9.0\ncomment = "instance __dict__ → class __dict__ → __getattr__"\n\n[[example_figure_scores]]\nslug = "bound-and-unbound-methods"\nscore = 9.0\ncomment = "instance.method bound vs Class.method unbound"\n\n[[example_figure_scores]]\nslug = "classmethods-and-staticmethods"\nscore = 9.0\ncomment = "three method kinds, three first-arg conventions"\n\n[[example_figure_scores]]\nslug = "callable-objects"\nscore = 9.0\ncomment = "__call__ makes any object callable"\n\n[[example_figure_scores]]\nslug = "generics-and-typevar"\nscore = 9.0\ncomment = "the same T flows in and out"\n\n[[example_figure_scores]]\nslug = "truth-and-size"\nscore = 9.0\ncomment = "__bool__ → __len__ → True fallback chain"\n\n[[example_figure_scores]]\nslug = "bytes-and-bytearray"\nscore = 9.0\ncomment = "frozen vs mutable contrast"\n\n[[example_figure_scores]]\nslug = "sentinel-iteration"\nscore = 9.0\ncomment = "iter(callable, sentinel) stop condition"\n\n[[example_figure_scores]]\nslug = "partial-functions"\nscore = 9.0\ncomment = "f → partial(f, 1) → g"\n\n[[example_figure_scores]]\nslug = "guard-clauses"\nscore = 9.0\ncomment = "early returns, main body at the tail"\n\n[[example_figure_scores]]\nslug = "packages"\nscore = 9.0\ncomment = "__init__.py + nested submodules"\n\n[[example_figure_scores]]\nslug = "virtual-environments"\nscore = 9.0\ncomment = "project / venv boundary"\n\n[[example_figure_scores]]\nslug = "subprocesses"\nscore = 9.0\ncomment = "spawn → child → captured output"\n\n[[example_figure_scores]]\nslug = "logging"\nscore = 9.0\ncomment = "five thresholded levels"\n\n[[example_figure_scores]]\nslug = "testing"\nscore = 9.0\ncomment = "arrange-act-assert three-row pattern"\n\n[[example_figure_scores]]\nslug = "networking"\nscore = 9.0\ncomment = "text ↔ bytes across the socket boundary"\n\n[[example_figure_scores]]\nslug = "casts-and-any"\nscore = 9.0\ncomment = "Any → cast(T, x) → T, runtime unchanged"\n\n[[example_figure_scores]]\nslug = "newtype"\nscore = 9.0\ncomment = "same runtime, distinct static identity"\n\n[[example_figure_scores]]\nslug = "paramspec"\nscore = 9.0\ncomment = "P preserved through decorator"\n\n[[example_figure_scores]]\nslug = "literal-and-final"\nscore = 9.0\ncomment = "slot narrows to a fixed set"\n\n[[example_figure_scores]]\nslug = "runtime-type-checks"\nscore = 9.0\ncomment = "isinstance returns bool"\n\n[[example_figure_scores]]\nslug = "collections-module"\nscore = 9.0\ncomment = "deque / Counter / defaultdict / namedtuple"\n\n[[example_figure_scores]]\nslug = "structured-data-shapes"\nscore = 9.0\ncomment = "dataclass vs NamedTuple vs TypedDict runtime trade-offs"\n\n[[example_figure_scores]]\nslug = "csv-data"\nscore = 9.0\ncomment = "rows × columns; same shape per line"\n\n[[example_figure_scores]]\nslug = "warnings"\nscore = 9.0\ncomment = "soft signal; execution continues"\n\n[[example_figure_scores]]\nslug = "object-lifecycle"\nscore = 9.0\ncomment = "names keep object reachable until last reference disappears"\n\n[[example_figure_scores]]\nslug = "args-and-kwargs"\nscore = 9.0\ncomment = "*args tuple, **kwargs dict regions"\n\n[[example_figure_scores]]\nslug = "multiple-return-values"\nscore = 9.0\ncomment = "function returns tuple; caller unpacks"\n\n[[example_figure_scores]]\nslug = "properties"\nscore = 9.0\ncomment = "obj.x routes through fget instead of __dict__"\n\n[[example_figure_scores]]\nslug = "overloads"\nscore = 9.0\ncomment = "static overload signatures contrasted with one runtime body"\n\n[[example_figure_scores]]\nslug = "callable-types"\nscore = 9.0\ncomment = "callback slot shows argument list and return type"\n\n[[example_figure_scores]]\nslug = "threads-and-processes"\nscore = 9.0\ncomment = "thread GIL sharing contrasted with process isolation"\n\n# Curated scores for journey-section figures.\n[[journey_section_figure_scores]]\nsection = "Start with executable evidence."\nscore = 9.0\ncomment = "journey-native source/run/output evidence loop"\n\n[[journey_section_figure_scores]]\nsection = "Separate value, identity, and absence."\nscore = 9.0\ncomment = "three runtime questions separated"\n\n[[journey_section_figure_scores]]\nsection = "Read expressions as object operations."\nscore = 9.0\ncomment = "syntax enters the data-model method surface"\n\n[[journey_section_figure_scores]]\nsection = "Choose between paths."\nscore = 9.0\ncomment = "branch map independent of one if-statement example"\n\n[[journey_section_figure_scores]]\nsection = "Name and shape decisions."\nscore = 9.0\ncomment = "name-vs-shape decision map"\n\n[[journey_section_figure_scores]]\nsection = "Stop as soon as the answer is known."\nscore = 9.0\ncomment = "early boundary leaves tail unread"\n\n[[journey_section_figure_scores]]\nsection = "Choose the right loop shape."\nscore = 9.0\ncomment = "loop choice by stopping rule"\n\n[[journey_section_figure_scores]]\nsection = "See the protocol behind `for`."\nscore = 9.5\ncomment = "for surface mapped to iter()/next()/StopIteration"\n\n[[journey_section_figure_scores]]\nsection = "Compose lazy value streams."\nscore = 9.0\ncomment = "consumer pull drives the lazy pipeline"\n\n[[journey_section_figure_scores]]\nsection = "Pick the container that matches the question."\nscore = 9.0\ncomment = "list/tuple/dict/set per question"\n\n[[journey_section_figure_scores]]\nsection = "Move between shapes deliberately."\nscore = 9.0\ncomment = "input → transform → result"\n\n[[journey_section_figure_scores]]\nsection = "Cross text and data boundaries."\nscore = 9.0\ncomment = "text in, structured value out"\n\n[[journey_section_figure_scores]]\nsection = "Start with functions as named behavior."\nscore = 9.0\ncomment = "concrete call, named body, and return boundary"\n\n[[journey_section_figure_scores]]\nsection = "Use functions as values."\nscore = 9.0\ncomment = "second name binds same function"\n\n[[journey_section_figure_scores]]\nsection = "Bundle behavior with state."\nscore = 9.0\ncomment = "class groups state + methods"\n\n[[journey_section_figure_scores]]\nsection = "Keep runtime and static analysis separate."\nscore = 9.0\ncomment = "runtime track separated from static-tool track"\n\n[[journey_section_figure_scores]]\nsection = "Describe realistic data shapes."\nscore = 9.0\ncomment = "field, variant, and absence contracts in one map"\n\n[[journey_section_figure_scores]]\nsection = "Scale annotations for reusable libraries."\nscore = 9.0\ncomment = "caller contracts survive library API boundary"\n\n[[journey_section_figure_scores]]\nsection = "Make failure explicit."\nscore = 9.0\ncomment = "failure shapes mapped to explicit signals"\n\n[[journey_section_figure_scores]]\nsection = "Control resource and module boundaries."\nscore = 9.0\ncomment = "resource/module/env boundaries shown together"\n\n[[journey_section_figure_scores]]\nsection = "Handle operations that outlive one expression."\nscore = 9.0\ncomment = "operation boundary returns evidence later"\n\n# Curated example quality scores.\n[[example_quality_scores]]\nslug = "hello-world"\nscore = 7.1\ncomment = "waived: traditional smallest complete program"\n\n[[example_quality_scores]]\nslug = "values"\nscore = 9.0\ncomment = "object/type/operation map with prerequisite graph edges"\n\n[[example_quality_scores]]\nslug = "literals"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich, note-heavy, multi-cell"\n\n[[example_quality_scores]]\nslug = "numbers"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "booleans"\nscore = 9.0\ncomment = "truth operations, short-circuiting, bool-as-int footgun"\n\n[[example_quality_scores]]\nslug = "operators"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich, note-heavy, multi-cell"\n\n[[example_quality_scores]]\nslug = "none"\nscore = 9.0\ncomment = "None, mapping default, and exception absence boundaries"\n\n[[example_quality_scores]]\nslug = "variables"\nscore = 9.0\ncomment = "binding, rebinding, augmented-assignment, lifecycle graph"\n\n[[example_quality_scores]]\nslug = "constants"\nscore = 9.0\ncomment = "constant convention, Final boundary, runtime rebinding evidence"\n\n[[example_quality_scores]]\nslug = "truthiness"\nscore = 9.0\ncomment = "truth-value categories, explicit-comparison boundary, graph-linked"\n\n[[example_quality_scores]]\nslug = "equality-and-identity"\nscore = 9.0\ncomment = "== versus is, singleton check, identity-cache warning"\n\n[[example_quality_scores]]\nslug = "mutability"\nscore = 9.0\ncomment = "in-place mutation, rebinding, alias visibility"\n\n[[example_quality_scores]]\nslug = "object-lifecycle"\nscore = 9.0\ncomment = "references, rebinding, and last-reference boundary"\n\n[[example_quality_scores]]\nslug = "strings"\nscore = 9.0\ncomment = "Unicode text, immutability, encoding boundary"\n\n[[example_quality_scores]]\nslug = "bytes-and-bytearray"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "string-formatting"\nscore = 9.0\ncomment = "f-string expression, format spec, debug/repr boundary"\n\n[[example_quality_scores]]\nslug = "conditionals"\nscore = 9.0\ncomment = "branch ordering, truthiness, and ternary boundary"\n\n[[example_quality_scores]]\nslug = "guard-clauses"\nscore = 9.0\ncomment = "nested contrast plus flattened guard path"\n\n[[example_quality_scores]]\nslug = "assignment-expressions"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "for-loops"\nscore = 9.0\ncomment = "direct iteration, range, and enumerate progression"\n\n[[example_quality_scores]]\nslug = "break-and-continue"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "loop-else"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "iterating-over-iterables"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "iterators"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "iterator-vs-iterable"\nscore = 9.0\ncomment = "graph-rich"\n\n[[example_quality_scores]]\nslug = "sentinel-iteration"\nscore = 9.0\ncomment = "call-until-marker shape contrasted with manual while"\n\n[[example_quality_scores]]\nslug = "match-statements"\nscore = 9.0\ncomment = "shape dispatch and guarded pattern payoff"\n\n[[example_quality_scores]]\nslug = "advanced-match-patterns"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "while-loops"\nscore = 9.0\ncomment = "state-driven repetition contrasted with for loops"\n\n[[example_quality_scores]]\nslug = "lists"\nscore = 9.0\ncomment = "ordered mutable sequence operations plus tuple/set/copy boundaries"\n\n[[example_quality_scores]]\nslug = "tuples"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "unpacking"\nscore = 9.0\ncomment = "sequence, starred, and mapping unpacking shapes"\n\n[[example_quality_scores]]\nslug = "dicts"\nscore = 9.0\ncomment = "lookup/default/update/delete dictionary boundaries"\n\n[[example_quality_scores]]\nslug = "sets"\nscore = 9.0\ncomment = "uniqueness, membership, set algebra, ordering boundary"\n\n[[example_quality_scores]]\nslug = "slices"\nscore = 9.0\ncomment = "range bounds, step, reverse, and unchanged-source evidence"\n\n[[example_quality_scores]]\nslug = "comprehensions"\nscore = 9.0\ncomment = "loop-equivalent collection building with filter/projection boundaries"\n\n[[example_quality_scores]]\nslug = "comprehension-patterns"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "sorting"\nscore = 9.0\ncomment = "key functions, reverse order, sorted/list.sort boundary"\n\n[[example_quality_scores]]\nslug = "collections-module"\nscore = 9.0\ncomment = "Counter, defaultdict, deque, namedtuple mapped to data shapes"\n\n[[example_quality_scores]]\nslug = "copying-collections"\nscore = 9.0\ncomment = "alias, shallow copy, and deepcopy boundaries"\n\n[[example_quality_scores]]\nslug = "functions"\nscore = 9.0\ncomment = "definition/call/return/default-boundary walkthrough"\n\n[[example_quality_scores]]\nslug = "keyword-only-arguments"\nscore = 9.0\ncomment = "call-site readability and boolean flag boundary"\n\n[[example_quality_scores]]\nslug = "positional-only-parameters"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "args-and-kwargs"\nscore = 9.0\ncomment = "flexible argument collection plus forwarding/API-boundary graph"\n\n[[example_quality_scores]]\nslug = "multiple-return-values"\nscore = 9.0\ncomment = "tuple return plus unpacking boundary"\n\n[[example_quality_scores]]\nslug = "closures"\nscore = 9.0\ncomment = "closed-over state, factory payoff, late-binding boundary"\n\n[[example_quality_scores]]\nslug = "partial-functions"\nscore = 9.0\ncomment = "before/after callable adaptation and introspection"\n\n[[example_quality_scores]]\nslug = "scope-global-nonlocal"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "recursion"\nscore = 9.0\ncomment = "base case and recursive tree-shape payoff"\n\n[[example_quality_scores]]\nslug = "lambdas"\nscore = 9.0\ncomment = "expression callable use contrasted with named def reuse"\n\n[[example_quality_scores]]\nslug = "generators"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "yield-from"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "generator-expressions"\nscore = 9.0\ncomment = "eager list contrast, lazy one-pass consumption, reducer use"\n\n[[example_quality_scores]]\nslug = "itertools"\nscore = 9.0\ncomment = "lazy standard-library pipeline and iterator-composition payoff"\n\n[[example_quality_scores]]\nslug = "decorators"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "classes"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "inheritance-and-super"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "classmethods-and-staticmethods"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "dataclasses"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "properties"\nscore = 9.0\ncomment = "managed attribute API without caller syntax change"\n\n[[example_quality_scores]]\nslug = "special-methods"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich, note-heavy, multi-cell"\n\n[[example_quality_scores]]\nslug = "truth-and-size"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "container-protocols"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "callable-objects"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "operator-overloading"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "attribute-access"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "bound-and-unbound-methods"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "descriptors"\nscore = 9.0\ncomment = "descriptor lookup, __set_name__, and validation mechanics"\n\n[[example_quality_scores]]\nslug = "metaclasses"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "context-managers"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "delete-statements"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "exceptions"\nscore = 9.0\ncomment = "specific handling, else/finally cleanup, broad-catch boundary"\n\n[[example_quality_scores]]\nslug = "assertions"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "exception-chaining"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "exception-groups"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "warnings"\nscore = 9.0\ncomment = "capture and escalate soft failures with filters"\n\n[[example_quality_scores]]\nslug = "modules"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "import-aliases"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "packages"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "virtual-environments"\nscore = 9.0\ncomment = "standard venv workflow plus explicit Worker deployment boundary"\n\n[[example_quality_scores]]\nslug = "type-hints"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich, note-heavy, multi-cell"\n\n[[example_quality_scores]]\nslug = "runtime-type-checks"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "union-and-optional-types"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "type-aliases"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "typed-dicts"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "structured-data-shapes"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "literal-and-final"\nscore = 9.0\ncomment = "Literal value set, Final rebinding promise, runtime caveat"\n\n[[example_quality_scores]]\nslug = "callable-types"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "generics-and-typevar"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "paramspec"\nscore = 9.0\ncomment = "Callable erasure contrasted with ParamSpec-preserving decorator"\n\n[[example_quality_scores]]\nslug = "overloads"\nscore = 9.0\ncomment = "static overload stubs contrasted with one runtime implementation"\n\n[[example_quality_scores]]\nslug = "casts-and-any"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "newtype"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "protocols"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "abstract-base-classes"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "enums"\nscore = 9.0\ncomment = "closed symbolic choice set contrasted with raw strings"\n\n[[example_quality_scores]]\nslug = "regular-expressions"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich, note-heavy, multi-cell"\n\n[[example_quality_scores]]\nslug = "number-parsing"\nscore = 9.0\ncomment = "decimal, explicit base, and recoverable ValueError boundary"\n\n[[example_quality_scores]]\nslug = "custom-exceptions"\nscore = 9.0\ncomment = "domain error naming, raise point, recovery boundary"\n\n[[example_quality_scores]]\nslug = "json"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "logging"\nscore = 9.0\ncomment = "logger, handler, formatter, and threshold boundaries"\n\n[[example_quality_scores]]\nslug = "testing"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n\n[[example_quality_scores]]\nslug = "subprocesses"\nscore = 9.0\ncomment = "standard subprocess contract plus explicit Worker boundary"\n\n[[example_quality_scores]]\nslug = "threads-and-processes"\nscore = 9.0\ncomment = "thread/process executor contrast plus explicit Worker boundary"\n\n[[example_quality_scores]]\nslug = "networking"\nscore = 9.0\ncomment = "protocol byte boundary plus explicit socket runtime caveat"\n\n[[example_quality_scores]]\nslug = "datetime"\nscore = 9.0\ncomment = "date/time/delta parsing with timezone scope made explicit"\n\n[[example_quality_scores]]\nslug = "csv-data"\nscore = 9.0\ncomment = "DictReader, conversion, and DictWriter row boundary"\n\n[[example_quality_scores]]\nslug = "async-await"\nscore = 9.0\ncomment = "graph-rich, note-heavy"\n\n[[example_quality_scores]]\nslug = "async-iteration-and-context"\nscore = 9.0\ncomment = "criterion-level pass: graph-rich"\n' diff --git a/src/example_sources/async-await.md b/src/example_sources/async-await.md index 6a8179f..811572a 100644 --- a/src/example_sources/async-await.md +++ b/src/example_sources/async-await.md @@ -23,9 +23,12 @@ The alternative is ordinary `def` for work that completes immediately. Use async ```python import asyncio +def slug_to_title(slug): + return slug.replace("-", " ").title() + async def fetch_title(slug): await asyncio.sleep(0) - return slug.replace("-", " ").title() + return slug_to_title(slug) async def main(): title = await fetch_title("async-await") @@ -61,6 +64,21 @@ asyncio.run(driver()) ``` ::: +:::cell +An ordinary `def` function computes its result immediately: calling it runs the body and hands the value straight back. This synchronous form is the baseline the rest of the page contrasts against. + +```python +def slug_to_title(slug): + return slug.replace("-", " ").title() + +print(slug_to_title("async-await")) +``` + +```output +Async Await +``` +::: + :::cell An `async def` function returns a coroutine object when called. The function body has not produced its final result yet. @@ -69,7 +87,7 @@ import asyncio async def fetch_title(slug): await asyncio.sleep(0) - return slug.replace("-", " ").title() + return slug_to_title(slug) coroutine = fetch_title("async-await") print(coroutine.__class__.__name__) @@ -114,7 +132,7 @@ asyncio.run(main()) ::: :::cell -`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. +`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](/examples/async-iteration-and-context) page explains the protocols in depth. ```python class Session: diff --git a/src/example_sources/attribute-access.md b/src/example_sources/attribute-access.md index 7fa4a98..8a57d9f 100644 --- a/src/example_sources/attribute-access.md +++ b/src/example_sources/attribute-access.md @@ -44,7 +44,7 @@ print(settings._values["volume"]) ::: :::cell -Normal initialization still needs to set real attributes. Calling `object.__setattr__` avoids recursing through your own hook. +The starting point is ordinary: `__init__` stores one real attribute, the `_values` backing dictionary. The hooks in the next cells customize lookup and assignment around it. ```python class Settings: @@ -84,7 +84,7 @@ dark ::: :::cell -`__setattr__` intercepts assignment. This example stores public names in the backing dictionary. +`__setattr__` intercepts every assignment, including the ones in `__init__`. Underscore names are stored as real attributes through `object.__setattr__`, which avoids recursing through your own hook; public names go to the backing dictionary. ```python class Settings: diff --git a/src/example_sources/bound-and-unbound-methods.md b/src/example_sources/bound-and-unbound-methods.md index 01ea077..06d42d3 100644 --- a/src/example_sources/bound-and-unbound-methods.md +++ b/src/example_sources/bound-and-unbound-methods.md @@ -143,6 +143,7 @@ True :::note - `instance.method` produces a bound method whose `__self__` is the instance. - `Class.method` produces the plain function and requires you to pass the instance. +- "Unbound method" is the historical Python 2 term; since Python 3, `Class.method` is simply a function, which is what this page demonstrates. - Each bound method is its own object; storing one captures its instance. - The binding is implemented by the descriptor protocol on the function object. ::: diff --git a/src/example_sources/bytes-and-bytearray.md b/src/example_sources/bytes-and-bytearray.md index 7d33859..fd14949 100644 --- a/src/example_sources/bytes-and-bytearray.md +++ b/src/example_sources/bytes-and-bytearray.md @@ -1,7 +1,7 @@ +++ slug = "bytes-and-bytearray" title = "Bytes and Bytearray" -section = "Basics" +section = "Text" summary = "bytes and bytearray store binary data, not Unicode text." doc_path = "/library/stdtypes.html#binary-sequence-types-bytes-bytearray-memoryview" see_also = [ diff --git a/src/example_sources/casts-and-any.md b/src/example_sources/casts-and-any.md index bf43c64..a776e11 100644 --- a/src/example_sources/casts-and-any.md +++ b/src/example_sources/casts-and-any.md @@ -28,6 +28,10 @@ score = int(score_text) print(score + 2) print(cast(list[int], raw) is raw) print(type(raw).__name__) + +value: object = {"score": "98"} +if isinstance(value, dict): + print(value["score"]) ``` ::: diff --git a/src/example_sources/comprehensions.md b/src/example_sources/comprehensions.md index d6fde1d..3ccea4b 100644 --- a/src/example_sources/comprehensions.md +++ b/src/example_sources/comprehensions.md @@ -29,7 +29,7 @@ high_scores = {name: score for name, score in scores.items() if score >= 10} print(high_scores) unique_scores = {score for score in scores.values()} -print(unique_scores) +print(sorted(unique_scores)) ``` ::: @@ -62,15 +62,15 @@ print(high_scores) ::: :::cell -A set comprehension keeps only unique results. Here two people have the same score, so the resulting set has two values. +A set comprehension keeps only unique results. Here two people have the same score, so the resulting set has two values — printed through `sorted()` because sets have no display order to rely on. ```python unique_scores = {score for score in scores.values()} -print(unique_scores) +print(sorted(unique_scores)) ``` ```output -{8, 10} +[8, 10] ``` ::: diff --git a/src/example_sources/constants.md b/src/example_sources/constants.md index edee895..f7ca3de 100644 --- a/src/example_sources/constants.md +++ b/src/example_sources/constants.md @@ -3,7 +3,7 @@ slug = "constants" title = "Constants" section = "Basics" summary = "Python uses naming conventions and optional types for values that should not change." -doc_path = "/tutorial/classes.html#python-scopes-and-namespaces" +doc_path = "/library/typing.html#typing.Final" see_also = [ "variables", "literal-and-final", diff --git a/src/example_sources/decorators.md b/src/example_sources/decorators.md index 4b19a65..5311c31 100644 --- a/src/example_sources/decorators.md +++ b/src/example_sources/decorators.md @@ -43,6 +43,7 @@ def welcome(name): print(welcome("workers")) print(welcome.__name__) +print(welcome.__doc__) ``` ::: diff --git a/src/example_sources/dicts.md b/src/example_sources/dicts.md index 417996f..4ec6f6d 100644 --- a/src/example_sources/dicts.md +++ b/src/example_sources/dicts.md @@ -86,7 +86,7 @@ Grace: 9 ::: :::cell -Mutating 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. +Adding or removing keys while iterating a dictionary raises `RuntimeError` ("dictionary changed size during iteration"); reassigning an existing key's value is allowed. Snapshot the keys with `list(d.keys())` (or build a list of changes and apply them after the loop) so deletions see a stable view. ```python inventory = {"apple": 0, "pear": 3, "plum": 0} @@ -105,5 +105,5 @@ print(inventory) - Dictionaries preserve insertion order in modern Python. - Use `get()` when a missing key has a reasonable default. - Use direct indexing when a missing key should be treated as an error. -- Snapshot keys with `list(d.keys())` before deleting items in a loop; mutating during iteration raises `RuntimeError`. +- Snapshot keys with `list(d.keys())` before deleting items in a loop; adding or removing keys during iteration raises `RuntimeError`. ::: diff --git a/src/example_sources/exceptions.md b/src/example_sources/exceptions.md index d0e587d..ae7221a 100644 --- a/src/example_sources/exceptions.md +++ b/src/example_sources/exceptions.md @@ -50,6 +50,12 @@ def safe_parse_fixed(text): print(safe_parse_broken("42")) print(safe_parse_fixed("42")) +print(safe_parse_broken(["4", "2"])) + +try: + safe_parse_fixed(["4", "2"]) +except TypeError as error: + print(type(error).__name__) ``` ::: @@ -99,7 +105,7 @@ checked python ::: :::cell -Bare `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. +Bare `except:` and broad `except Exception:` swallow far more than the failure you meant to handle, including `KeyboardInterrupt` (bare) and most programming bugs (broad). The two functions look interchangeable on good input — the divergence appears on a buggy call: passing a list is a programming error, yet the broad version converts it into a quiet `None` while the specific version lets the `TypeError` surface. ```python def safe_parse_broken(text): @@ -116,11 +122,19 @@ def safe_parse_fixed(text): print(safe_parse_broken("42")) print(safe_parse_fixed("42")) +print(safe_parse_broken(["4", "2"])) + +try: + safe_parse_fixed(["4", "2"]) +except TypeError as error: + print(type(error).__name__) ``` ```output 42 42 +None +TypeError ``` ::: diff --git a/src/example_sources/generics-and-typevar.md b/src/example_sources/generics-and-typevar.md index ff272ab..0420311 100644 --- a/src/example_sources/generics-and-typevar.md +++ b/src/example_sources/generics-and-typevar.md @@ -91,6 +91,7 @@ T :::note - A `TypeVar` stands for a type chosen by the caller. +- Python 3.12+ also accepts the inline PEP 695 spelling `def first[T](items: list[T]) -> T`, which declares the variable without a separate `TypeVar` line; the explicit form shown here works everywhere and reads the same way. - Generic functions avoid losing information to `object` or `Any`. - Use generics when input and output types are connected. ::: diff --git a/src/example_sources/hello-world.md b/src/example_sources/hello-world.md index eaf2aa6..f832a40 100644 --- a/src/example_sources/hello-world.md +++ b/src/example_sources/hello-world.md @@ -14,7 +14,7 @@ Every Python program starts by executing statements from top to bottom. Calling Strings 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. -`print()` writes text followed by a newline. Strings can be delimited with single or double quotes. +Run the program and compare what you see with the source: the text appears on standard output followed by a newline, which is why successive `print()` calls land on separate lines. :::program ```python @@ -23,7 +23,7 @@ print("hello world") ::: :::cell -Every 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. +The whole program is one statement. Python evaluates the string literal `"hello world"` and passes that value to `print()`, which writes it to standard output — the output panel shows exactly that line. ```python print("hello world") diff --git a/src/example_sources/keyword-only-arguments.md b/src/example_sources/keyword-only-arguments.md index 3d47749..391eb8c 100644 --- a/src/example_sources/keyword-only-arguments.md +++ b/src/example_sources/keyword-only-arguments.md @@ -27,6 +27,11 @@ def connect(host, *, timeout=5, secure=True): connect("example.com") connect("example.com", timeout=10) connect("localhost", secure=False) + +try: + connect("example.com", 10) +except TypeError as error: + print(type(error).__name__) ``` ::: @@ -70,6 +75,21 @@ http://localhost timeout=5 ``` ::: +:::cell +The bare `*` is enforced at the call site: passing the timeout positionally raises `TypeError` instead of silently filling the wrong slot. + +```python +try: + connect("example.com", 10) +except TypeError as error: + print(type(error).__name__) +``` + +```output +TypeError +``` +::: + :::note - Put `*` before options that callers should name. - Keyword-only flags avoid mysterious positional `True` and `False` arguments. diff --git a/src/example_sources/match-statements.md b/src/example_sources/match-statements.md index 54a0522..551cf28 100644 --- a/src/example_sources/match-statements.md +++ b/src/example_sources/match-statements.md @@ -93,5 +93,6 @@ unknown action: jump :::note - `match` compares structure, not just equality. - Patterns can bind names such as `x` and `y` while matching. +- Mapping patterns match when the named keys are present; extra keys in the subject are ignored rather than failing the case. - Put the catch-all `_` case last, because cases are tried from top to bottom. ::: diff --git a/src/example_sources/networking.md b/src/example_sources/networking.md index 5429d77..c0d9ede 100644 --- a/src/example_sources/networking.md +++ b/src/example_sources/networking.md @@ -36,7 +36,7 @@ finally: ::: :::unsupported -`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 — the Python By Example runner does not expose arbitrary sockets and disables outbound access for edited examples.) +`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. (The in-browser Run button cannot open sockets — the sandbox disables outbound access — so pressing Run on this page fails; the verified output below comes from a real socket pair under standard CPython at build time.) ```python left, right = socket.socketpair() diff --git a/src/example_sources/numbers.md b/src/example_sources/numbers.md index bc67f7f..ba0e2bd 100644 --- a/src/example_sources/numbers.md +++ b/src/example_sources/numbers.md @@ -26,6 +26,7 @@ z = 2 + 3j print(count + 5) print(count / 4) +print(ratio * 2) print(count // 4) print(count % 4) print(2 ** 5) diff --git a/src/example_sources/operator-overloading.md b/src/example_sources/operator-overloading.md index be10124..80b74c0 100644 --- a/src/example_sources/operator-overloading.md +++ b/src/example_sources/operator-overloading.md @@ -25,9 +25,13 @@ class Vector: self.y = y def __add__(self, other): + if not isinstance(other, Vector): + return NotImplemented return Vector(self.x + other.x, self.y + other.y) def __eq__(self, other): + if not isinstance(other, Vector): + return NotImplemented return (self.x, self.y) == (other.x, other.y) def __repr__(self): @@ -35,11 +39,12 @@ class Vector: print(Vector(2, 3) + Vector(4, 5)) print(Vector(1, 1) == Vector(1, 1)) +print(Vector(1, 1) == 5) ``` ::: :::cell -`__add__` defines how the `+` operator combines two objects. +`__add__` defines how the `+` operator combines two objects. Checking the operand type and returning `NotImplemented` for foreign types lets Python try the other operand's reflected method instead of crashing inside yours. ```python class Vector: @@ -48,6 +53,8 @@ class Vector: self.y = y def __add__(self, other): + if not isinstance(other, Vector): + return NotImplemented return Vector(self.x + other.x, self.y + other.y) def __repr__(self): @@ -62,7 +69,7 @@ Vector(6, 8) ::: :::cell -`__eq__` defines value equality for `==`. Without it, user-defined objects compare by identity. +`__eq__` defines value equality for `==`. Without it, user-defined objects compare by identity. Returning `NotImplemented` for foreign types matters most here: equality against an unrelated value should answer `False`, never raise — Python falls back to identity when both sides decline. ```python class Vector: @@ -71,13 +78,17 @@ class Vector: self.y = y def __eq__(self, other): + if not isinstance(other, Vector): + return NotImplemented return (self.x, self.y) == (other.x, other.y) print(Vector(1, 1) == Vector(1, 1)) +print(Vector(1, 1) == 5) ``` ```output True +False ``` ::: @@ -91,6 +102,8 @@ class Vector: self.y = y def __add__(self, other): + if not isinstance(other, Vector): + return NotImplemented return Vector(self.x + other.x, self.y + other.y) def __repr__(self): diff --git a/src/example_sources/operators.md b/src/example_sources/operators.md index ef01c29..cf9d9f1 100644 --- a/src/example_sources/operators.md +++ b/src/example_sources/operators.md @@ -49,6 +49,14 @@ print(Scale(2) @ Scale(3)) items = ["a", "b"] if (size := len(items)) > 0: print(size) + +def loud(): + print("ran") + return True + +print(False and loud()) +print(True or loud()) +print(True and loud()) ``` ::: diff --git a/src/example_sources/paramspec.md b/src/example_sources/paramspec.md index 7ea1370..8c6b873 100644 --- a/src/example_sources/paramspec.md +++ b/src/example_sources/paramspec.md @@ -110,5 +110,6 @@ calling add :::note - `ParamSpec` preserves a callable's parameter list through transparent wrappers. - Pair `ParamSpec` with a `TypeVar` when the return type should also be preserved. +- Python 3.12+ also accepts the inline PEP 695 spelling `def wrap[**P, R](func: Callable[P, R])`, which declares both variables in the signature itself. - If the wrapper changes the public signature, write that new signature directly instead. ::: diff --git a/src/example_sources/positional-only-parameters.md b/src/example_sources/positional-only-parameters.md index 9340c15..2e7bad6 100644 --- a/src/example_sources/positional-only-parameters.md +++ b/src/example_sources/positional-only-parameters.md @@ -27,7 +27,12 @@ def scale(value, /, factor=2, *, clamp=False): print(scale(4)) print(scale(4, factor=3)) -print(scale(4, clamp=True)) +print(scale(4, factor=3, clamp=True)) + +try: + scale(value=4) +except TypeError as error: + print(type(error).__name__) ``` ::: @@ -52,14 +57,29 @@ print(scale(4, factor=3)) ::: :::cell -Parameters after `*` are keyword-only. That makes options such as `clamp` explicit at the call site. +Parameters after `*` are keyword-only. That makes options such as `clamp` explicit at the call site — here `4 * 3` would be `12`, and the clamp visibly caps the result at `10`. ```python -print(scale(4, clamp=True)) +print(scale(4, factor=3, clamp=True)) ``` ```output -8 +10 +``` +::: + +:::cell +The restriction is enforced, not advisory: passing the positional-only `value` by keyword raises `TypeError` at the call site. + +```python +try: + scale(value=4) +except TypeError as error: + print(type(error).__name__) +``` + +```output +TypeError ``` ::: diff --git a/src/example_sources/regular-expressions.md b/src/example_sources/regular-expressions.md index 122b1b4..71caec0 100644 --- a/src/example_sources/regular-expressions.md +++ b/src/example_sources/regular-expressions.md @@ -104,7 +104,7 @@ None ::: :::cell -`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. +`re.compile` produces a reusable pattern object and gives the pattern a name. The `re` module also caches recently compiled patterns internally, so the practical wins are readability and a place to attach flags more than raw speed. ```python scoreline = re.compile(pattern) diff --git a/src/example_sources/sets.md b/src/example_sources/sets.md index 50ae1db..d7c483f 100644 --- a/src/example_sources/sets.md +++ b/src/example_sources/sets.md @@ -81,7 +81,6 @@ print(sorted(allowed - compiled)) ::: :::note -- Use lists when order and repeated values matter. - Use sets when uniqueness and membership are the main operations. - Prefer lists when order or repeated values are part of the meaning. - Sets are unordered, so sort them when examples need deterministic display. diff --git a/src/example_sources/special-methods.md b/src/example_sources/special-methods.md index 7f4a136..26b639f 100644 --- a/src/example_sources/special-methods.md +++ b/src/example_sources/special-methods.md @@ -209,7 +209,7 @@ Bag(['a', 'b']) ::: :::cell -`__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()`. +`__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 — but only for types treated as immutable: this `Bag` hashes its current items, so mutating one after adding it to a set makes it unfindable. `__lt__` alone is enough for `<` and for `sorted()`. ```python class Bag: @@ -238,7 +238,7 @@ True ::: :::cell -The 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. +The 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](/examples/container-protocols) for the full surface. ```python class Bag: @@ -274,7 +274,7 @@ False ::: :::cell -`__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. +`__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](/examples/callable-objects) and [context-managers](/examples/context-managers) pages go deeper. ```python class Multiplier: diff --git a/src/example_sources/string-formatting.md b/src/example_sources/string-formatting.md index 2882cdb..bf99b86 100644 --- a/src/example_sources/string-formatting.md +++ b/src/example_sources/string-formatting.md @@ -52,7 +52,7 @@ Ada scored 9.5 ::: :::cell -Format 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. +Format specifications after `:` control display without changing the underlying values. Here the rank is right-aligned, the name is left-aligned, and `05.1f` zero-pads the score to a width of five characters with one decimal place. ```python row = f"{rank:>2} | {name:<8} | {score:05.1f}" diff --git a/src/example_sources/subprocesses.md b/src/example_sources/subprocesses.md index e208ca9..60ab86e 100644 --- a/src/example_sources/subprocesses.md +++ b/src/example_sources/subprocesses.md @@ -36,7 +36,7 @@ print(result.returncode) ::: :::unsupported -`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 — the Python By Example runner does 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. (The in-browser Run button cannot spawn child processes, so pressing Run on this page fails in the sandbox; the verified output below comes from standard CPython at build time.) ```python result = subprocess.run( @@ -76,5 +76,5 @@ child process - Use a list of arguments instead of shell strings when possible. - Capture output when the parent program needs to inspect it. - `check=True` turns non-zero exits into exceptions. -- If you run this in local/server Python, the child process is real; on this site, the runnable evidence preserves the API shape without spawning a process. +- The output shown here was produced by really spawning the child under standard CPython when the example was verified; the site's in-browser sandbox cannot create processes, so live runs of this page fail there. ::: diff --git a/src/example_sources/threads-and-processes.md b/src/example_sources/threads-and-processes.md index 5b5e871..6ead116 100644 --- a/src/example_sources/threads-and-processes.md +++ b/src/example_sources/threads-and-processes.md @@ -34,7 +34,7 @@ print(ProcessPoolExecutor.__name__) ::: :::unsupported -`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 — the Python By Example runner does 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. (The in-browser Run button cannot create native threads or child processes, so pressing Run on this page fails in the sandbox; the verified thread-pool output below comes from standard CPython at build time.) ```python with ThreadPoolExecutor(max_workers=2) as pool: @@ -80,5 +80,5 @@ ProcessPoolExecutor - Threads share memory, so mutable shared state needs care. - Processes avoid shared interpreter state but require values to cross a process boundary. - Prefer `asyncio` for coroutine-based I/O and executors for ordinary blocking callables. -- The displayed executor names are standard Python concepts; the site avoids actually creating host threads or processes in the live runner. +- The thread-pool output here was produced by real worker threads under standard CPython when the example was verified; the site's in-browser sandbox cannot create threads or processes, so live runs of this page fail there. ::: diff --git a/src/example_sources/truthiness.md b/src/example_sources/truthiness.md index e0177b0..f05b161 100644 --- a/src/example_sources/truthiness.md +++ b/src/example_sources/truthiness.md @@ -35,9 +35,7 @@ print(bool(42)) ::: :::cell -Truthiness is one of Python's most important conveniences: conditions can test objects directly instead of requiring explicit boolean comparisons everywhere. - -Empty containers, numeric zero, None, and False are false; most other values are true. This makes common checks such as if items: concise and idiomatic. +An empty list is false, so `not items` reads as "items is empty". The condition tests the object directly — no `len(items) == 0` comparison is needed. ```python items = [] @@ -53,7 +51,7 @@ no items ::: :::cell -Use truthiness when it reads naturally, but choose explicit comparisons when the distinction matters, such as checking whether a value is exactly None. +A non-empty string is true, so `if name:` asks "did we get a name?" in one word. Reach for an explicit comparison instead when the distinction matters — `if name is not None:` treats an empty string differently from a missing one. ```python if name: @@ -66,7 +64,7 @@ has a name ::: :::cell -Use truthiness when it reads naturally, but choose explicit comparisons when the distinction matters, such as checking whether a value is exactly None. +`bool()` reveals the truth value any condition would use. Zero-like numbers convert to `False`; other numbers convert to `True`. ```python print(bool(0)) diff --git a/src/example_sources/type-hints.md b/src/example_sources/type-hints.md index 5e50641..b4d3cdb 100644 --- a/src/example_sources/type-hints.md +++ b/src/example_sources/type-hints.md @@ -20,8 +20,6 @@ The alternative to an annotation is prose, tests, or runtime validation. Good Py :::program ```python -from typing import TypeAlias - def total(numbers: list[int]) -> int: return sum(numbers) @@ -52,7 +50,7 @@ print(lookup("Ada")) print(lookup("Guido")) -Score: TypeAlias = int +type Score = int def grade(score: Score) -> str: return "pass" if score >= 50 else "fail" @@ -133,12 +131,10 @@ None ::: :::cell -`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. +The `type` statement names a type so it can be reused with intent. `type Score = int` keeps the underlying type at runtime but lets the API talk about a domain concept rather than a primitive. Older code spells this `Score: TypeAlias = int`; `typing.TypeAlias` is deprecated since Python 3.12, and the type-aliases page covers the modern statement in depth. ```python -from typing import TypeAlias - -Score: TypeAlias = int +type Score = int def grade(score: Score) -> str: return "pass" if score >= 50 else "fail" @@ -155,6 +151,6 @@ pass - Python does not enforce most type hints at runtime. - Tools like type checkers and editors use annotations to catch mistakes earlier. - Use `X | Y` for unions and `Optional[X]` for "X or None"; both spellings mean the same thing. -- Reach for `TypeAlias` when a domain name reads better than a raw primitive type. +- Reach for a `type` alias when a domain name reads better than a raw primitive type. - Use runtime validation when untrusted input must be rejected while the program runs. ::: diff --git a/src/example_sources/unpacking.md b/src/example_sources/unpacking.md index 58b1458..58b4b62 100644 --- a/src/example_sources/unpacking.md +++ b/src/example_sources/unpacking.md @@ -36,7 +36,7 @@ describe(**data) ::: :::cell -Unpacking binds multiple names from one iterable or mapping. It makes the structure of data visible at the point where values are introduced. +Tuple unpacking assigns each position to a name in one statement: `x` receives the first element of `point` and `y` the second. The assignment fails loudly if the number of names and elements disagree, which catches shape mistakes early. ```python point = (3, 4) @@ -50,7 +50,7 @@ print(x, y) ::: :::cell -Starred unpacking handles variable-length sequences by collecting the middle or remaining values. This keeps common head-tail patterns readable. +The starred name collects however many elements the head and tail don't claim — here `first` and `last` take the ends and `*middle` gathers the rest into a list. The same list works whether it has four elements or forty. ```python first, *middle, last = [1, 2, 3, 4] @@ -63,9 +63,7 @@ print(first, middle, last) ::: :::cell -Dictionary unpacking with ** connects structured data to function calls. It is widely used in configuration, adapters, and code that bridges APIs. - -Dictionary unpacking with ** connects structured data to function calls. It is widely used in configuration, adapters, and code that bridges APIs. +`describe(**data)` spreads the dictionary's keys as keyword arguments, so the call site never repeats `name=` and `language=` by hand. This is the bridge between dict-shaped data (configuration, parsed JSON) and function signatures. ```python def describe(name, language): diff --git a/src/example_sources_data.py b/src/example_sources_data.py index 80364b3..ac56edd 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"\nsee_also = [\n "functions",\n "keyword-only-arguments",\n "partial-functions",\n "paramspec",\n]\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"\nsee_also = [\n "truthiness",\n "operators",\n "conditionals",\n]\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"\nsee_also = [\n "functions",\n "lambdas",\n "decorators",\n "partial-functions",\n]\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"\nsee_also = [\n "dicts",\n "lists",\n "tuples",\n "sets",\n]\n+++\n\n`collections` provides specialized containers for common shapes that would otherwise require repetitive plumbing. Use it when the shape has a name: counting, grouping, queueing, or lightweight records.\n\nThese types are not replacements for `list`, `dict`, `tuple`, and `set`. They are small standard-library tools for cases where an ordinary container would hide the intent behind manual bookkeeping.\n\nThe examples below map each type to the question it answers.\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 the question is "how many times did each value appear?"\n\n```python\nfrom collections import Counter\n\ncounts = Counter("banana")\nprint(counts.most_common(2))\n```\n\n```output\n[(\'a\', 3), (\'n\', 2)]\n```\n:::\n\n:::cell\nUse `defaultdict(list)` when each key gathers multiple values and the missing-key case should create an empty list automatically.\n\n```python\nfrom collections import defaultdict\n\ngroups = defaultdict(list)\nfor name, team in [("Ada", "red"), ("Grace", "blue"), ("Lin", "red")]:\n groups[team].append(name)\nprint(dict(groups))\n```\n\n```output\n{\'red\': [\'Ada\', \'Lin\'], \'blue\': [\'Grace\']}\n```\n:::\n\n:::cell\nUse `deque` for queue operations at both ends, and `namedtuple` when a tiny immutable record needs names as well as positions.\n\n```python\nfrom collections import deque, namedtuple\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\nfirst\n2\n```\n:::\n\n:::note\n- `Counter` counts, `defaultdict` groups, `deque` queues, and `namedtuple` names record fields.\n- Prefer the built-in containers until a specialized shape makes the code clearer.\n- For new structured records with defaults and methods, consider `dataclasses` instead of `namedtuple`.\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"\nsee_also = [\n "for-loops",\n "generator-expressions",\n "comprehension-patterns",\n "lists",\n]\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"\nsee_also = [\n "booleans",\n "truthiness",\n "guard-clauses",\n "match-statements",\n]\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 and optional types for values that should not change."\ndoc_path = "/tutorial/classes.html#python-scopes-and-namespaces"\nsee_also = [\n "variables",\n "literal-and-final",\n "type-hints",\n]\n+++\n\nPython has no `const` keyword for ordinary names. Modules use all-caps names such as `MAX_RETRIES` to say “treat this as fixed configuration, not changing state.”\n\nThe interpreter will still let code rebind the name. That is why constants are primarily an API and readability convention. If a project also uses static typing, `Final` can make the convention machine-checkable.\n\nNamed constants remove magic values from code and give repeated literals one place to change.\n\n:::program\n```python\nfrom typing import Final\n\nMAX_RETRIES: Final = 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\nMAX_RETRIES = 5\nprint(MAX_RETRIES)\n```\n:::\n\n:::cell\nAll-caps names communicate design intent: this value is configuration that callers should treat as fixed.\n\n```python\nMAX_RETRIES = 3\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 when a repeated literal deserves a name at the domain boundary.\n\n```python\nAPI_VERSION = "2026-05"\nprint(API_VERSION)\n```\n\n```output\n2026-05\n```\n:::\n\n:::cell\n`Final` lets type checkers reject reassignment, but Python still runs ordinary rebinding at runtime.\n\n```python\nfrom typing import Final\n\nMAX_RETRIES: Final = 3\nMAX_RETRIES = 5\nprint(MAX_RETRIES)\n```\n\n```output\n5\n```\n:::\n\n:::note\n- Python constants are a convention, not a runtime lock.\n- Use all-caps names for fixed module-level configuration.\n- Add `Final` when static tooling should flag accidental rebinding.\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 the outer container while nested objects may still be shared."\ndoc_path = "/library/copy.html"\nsee_also = [\n "mutability",\n "lists",\n "dicts",\n]\n+++\n\nCopying answers two different questions: do you need a new outer container, or do you also need independent nested objects? A plain assignment gives another name for the same object. A shallow copy duplicates only the outer container. `copy.deepcopy()` recursively copies contained objects.\n\nMost Python code wants a shallow copy or a deliberate rebuild. Use a deep copy only when shared nested state would be wrong and the objects involved are safe to duplicate.\n\nThe outputs below show the footgun directly: a shallow copy has a different outer list, but its inner lists are still the same objects.\n\n:::program\n```python\nimport copy\n\nrows = [["Ada"], ["Grace"]]\nalias = rows\nshallow = rows.copy()\ndeep = copy.deepcopy(rows)\n\nrows[0].append("Lovelace")\n\nprint(alias is rows)\nprint(shallow is rows)\nprint(rows[0] is shallow[0])\nprint(rows[0] is deep[0])\nprint(shallow)\nprint(deep)\n```\n:::\n\n:::cell\nAssignment does not copy a collection. It gives the same list another name.\n\n```python\nrows = [["Ada"], ["Grace"]]\nalias = rows\n\nprint(alias is rows)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\nA shallow copy creates a new outer list, but nested lists are still shared.\n\n```python\nshallow = rows.copy()\nrows[0].append("Lovelace")\n\nprint(shallow is rows)\nprint(rows[0] is shallow[0])\nprint(shallow)\n```\n\n```output\nFalse\nTrue\n[[\'Ada\', \'Lovelace\'], [\'Grace\']]\n```\n:::\n\n:::cell\nA deep copy is independent at the nested level, so later mutation of `rows[0]` does not appear in `deep`.\n\n```python\nimport copy\n\nrows = [["Ada"], ["Grace"]]\ndeep = copy.deepcopy(rows)\nrows[0].append("Lovelace")\n\nprint(rows[0] is deep[0])\nprint(deep)\n```\n\n```output\nFalse\n[[\'Ada\'], [\'Grace\']]\n```\n:::\n\n:::note\n- Assignment aliases; it does not copy.\n- Shallow copies duplicate the outer container only.\n- Deep copies are useful for nested independence, but they can be expensive and surprising for objects with external resources.\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"\nsee_also = [\n "strings",\n "dicts",\n "json",\n]\n+++\n\nCSV is row-shaped text: each line is a record, and each comma-separated field arrives as a string. The `csv` module understands quoting, delimiters, and newlines, so it is safer than splitting lines by comma yourself.\n\nUse `DictReader` when a header row names the columns. Convert fields explicitly after reading, and use `DictWriter` when the program needs to produce the same row shape again.\n\nCSV is a good fit for flat tabular data. Use JSON or another structured format when values are nested or when types need to survive the text boundary.\n\n:::program\n```python\nimport csv\nimport io\n\ntext = "name,score\\nAda,98\\nGrace,95\\n"\nrows = list(csv.DictReader(io.StringIO(text)))\nprint(rows[0])\nprint(sum(int(row["score"]) for row in rows))\n\noutput = io.StringIO(newline="")\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\n`DictReader` uses the header row as dictionary keys. The values are still strings because CSV is text.\n\n```python\nimport csv\nimport io\n\ntext = "name,score\\nAda,98\\nGrace,95\\n"\nrows = list(csv.DictReader(io.StringIO(text)))\n\nprint(rows[0])\nprint(type(rows[0]["score"]).__name__)\n```\n\n```output\n{\'name\': \'Ada\', \'score\': \'98\'}\nstr\n```\n:::\n\n:::cell\nConvert numeric fields at the boundary where the program leaves CSV text and starts doing arithmetic.\n\n```python\nprint(sum(int(row["score"]) for row in rows))\n```\n\n```output\n193\n```\n:::\n\n:::cell\n`DictWriter` turns dictionaries back into row-shaped text with the same column order.\n\n```python\noutput = io.StringIO(newline="")\nwriter = csv.DictWriter(output, fieldnames=["name", "passed"])\nwriter.writeheader()\nwriter.writerow({"name": "Ada", "passed": True})\n\nprint(output.getvalue().splitlines()[1])\n```\n\n```output\nAda,True\n```\n:::\n\n:::note\n- Let `csv` handle quoting and delimiters instead of calling `split(",")`.\n- CSV fields are text until your code converts them.\n- Reach for JSON when records need nested lists, dictionaries, booleans, or numbers that preserve their type.\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"\nsee_also = [\n "exceptions",\n "exception-chaining",\n "warnings",\n "logging",\n]\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"\nsee_also = [\n "string-formatting",\n "json",\n "number-parsing",\n]\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\nA descriptor is an object stored on a class that defines `__get__`, `__set__`, or `__delete__`. When an instance attribute lookup finds that object on the class, Python calls the descriptor method instead of returning the descriptor object directly.\n\nDescriptors are the machinery behind methods, `property`, validators, and many ORM fields. Use them when one reusable object should control access for many attributes or classes; use `property` for a single simple managed attribute.\n\nThis example implements a positive-number validator. `__set_name__` learns the attribute name when the owner class is created, `__set__` validates writes, and `__get__` reads the stored value back from the instance.\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 if obj is None:\n return self\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)\nprint(Product.price.private_name)\ntry:\n item.price = -1\nexcept ValueError as error:\n print(error)\n```\n:::\n\n:::cell\nA descriptor object lives on the class. `__set_name__` lets it learn which managed attribute it is serving.\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 if obj is None:\n return self\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\nprint(Product.price.private_name)\n```\n\n```output\n_price\n```\n:::\n\n:::cell\nAssigning `item.price` calls `Positive.__set__`, and reading it calls `Positive.__get__`.\n\n```python\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- Descriptors are class attributes that participate in instance attribute access.\n- Data descriptors with `__set__` can validate or transform assignments.\n- `property` is usually simpler for one-off managed attributes; descriptors shine when the behavior is reusable.\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"\nsee_also = [\n "lists",\n "sets",\n "typed-dicts",\n "json",\n]\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"\nsee_also = [\n "literals",\n "classes",\n "literal-and-final",\n]\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"\nsee_also = [\n "none",\n "values",\n "object-lifecycle",\n "mutability",\n]\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"\nsee_also = [\n "conditionals",\n "guard-clauses",\n "custom-exceptions",\n "warnings",\n]\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 values produced by an iterable."\ndoc_path = "/tutorial/controlflow.html#for-statements"\nsee_also = [\n "while-loops",\n "iterating-over-iterables",\n "iterators",\n]\n+++\n\nA `for` loop asks an iterable for values and runs the indented block once per value. Python\'s loop is not primarily a numeric counter; it is a consumer of lists, ranges, files, generators, and any object that implements the iterator protocol.\n\nPrefer direct iteration when you need each value. Use `range()` when the numbers themselves are the data, and use `enumerate()` when the position and the value both matter.\n\nThe loop body is the indented block. When the iterable is exhausted, execution continues after the block. The neighboring `while` loop shape is for conditions that must be rechecked manually.\n\n:::program\n```python\nfor name in ["Ada", "Grace", "Guido"]:\n print(name)\n\nfor number in range(3):\n print(number)\n\nfor index, name in enumerate(["Ada", "Grace"], start=1):\n print(index, name)\n```\n:::\n\n:::cell\nDirect iteration keeps the code focused on the values in the collection.\n\n```python\nfor name in ["Ada", "Grace", "Guido"]:\n print(name)\n```\n\n```output\nAda\nGrace\nGuido\n```\n:::\n\n:::cell\n`range(3)` yields `0`, `1`, and `2` lazily. Use it when those integers are the thing being iterated over.\n\n```python\nfor number in range(3):\n print(number)\n```\n\n```output\n0\n1\n2\n```\n:::\n\n:::cell\n`enumerate()` is the usual Python way to keep a counter beside each value without indexing back into the list.\n\n```python\nfor index, name in enumerate(["Ada", "Grace"], start=1):\n print(index, name)\n```\n\n```output\n1 Ada\n2 Grace\n```\n:::\n\n:::note\n- A `for` loop consumes an iterable until it is exhausted.\n- Reach for `while` when the stopping condition must be rechecked manually.\n- `iter()` and `next()` expose the protocol that `for` uses internally.\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"\nsee_also = [\n "variables",\n "args-and-kwargs",\n "keyword-only-arguments",\n "closures",\n]\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"\nsee_also = [\n "comprehensions",\n "generators",\n "itertools",\n "yield-from",\n]\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 boundary cases early so the main path stays flat."\ndoc_path = "/tutorial/controlflow.html#if-statements"\nsee_also = [\n "conditionals",\n "exceptions",\n "functions",\n]\n+++\n\nA guard clause is an early `return`, `raise`, `break`, or `continue` that handles a case the rest of the function should not process. The point is not new syntax; the point is moving boundaries out of the way so the successful path can be read straight through.\n\nUse guards when a function has clear invalid, empty, or already-finished cases. If every branch is equally important, an ordinary `if`/`elif` chain may be clearer.\n\nThe contrast below shows the payoff: the nested version makes the valid path live inside two conditions, while the guard version names the invalid cases first and leaves the calculation at the outer indentation level.\n\n:::program\n```python\ndef nested_discount(price, percent):\n if price >= 0:\n if 0 <= percent <= 100:\n return round(price - price * percent / 100, 2)\n return "invalid discount"\n return "invalid price"\n\n\ndef guarded_discount(price, percent):\n if price < 0:\n return "invalid price"\n if not 0 <= percent <= 100:\n return "invalid discount"\n\n return round(price - price * percent / 100, 2)\n\nprint(nested_discount(100, 15))\nprint(guarded_discount(-5, 10))\nprint(guarded_discount(100, 120))\n```\n:::\n\n:::cell\nThe nested version is correct, but the useful work is buried inside both tests.\n\n```python\ndef nested_discount(price, percent):\n if price >= 0:\n if 0 <= percent <= 100:\n return round(price - price * percent / 100, 2)\n return "invalid discount"\n return "invalid price"\n\nprint(nested_discount(100, 15))\n```\n\n```output\n85.0\n```\n:::\n\n:::cell\nThe guard-clause version handles impossible inputs first, then lets the ordinary calculation sit at the top level of the function body.\n\n```python\ndef guarded_discount(price, percent):\n if price < 0:\n return "invalid price"\n if not 0 <= percent <= 100:\n return "invalid discount"\n\n return round(price - price * percent / 100, 2)\n\nprint(guarded_discount(-5, 10))\nprint(guarded_discount(100, 120))\n```\n\n```output\ninvalid price\ninvalid discount\n```\n:::\n\n:::note\n- Guard clauses are a readability pattern, not a separate Python feature.\n- They work best when the early cases are true boundaries.\n- For exceptional failures, raise an exception instead of returning a sentinel string.\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"\nsee_also = [\n "values",\n "variables",\n]\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"\nsee_also = [\n "iterators",\n "generator-expressions",\n "sentinel-iteration",\n "comprehension-patterns",\n]\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"\nsee_also = [\n "functions",\n "args-and-kwargs",\n "positional-only-parameters",\n "partial-functions",\n]\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"\nsee_also = [\n "functions",\n "sorting",\n "callable-objects",\n]\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"\nsee_also = [\n "tuples",\n "sets",\n "slices",\n "copying-collections",\n]\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 exact values, while Final marks names that should not be rebound."\ndoc_path = "/library/typing.html#typing.Literal"\nsee_also = [\n "type-hints",\n "constants",\n "union-and-optional-types",\n "overloads",\n]\n+++\n\n`Literal` and `Final` make two different static promises. `Literal` narrows a value to exact allowed options. `Final` tells a type checker that a name should not be rebound after its first assignment.\n\nBoth annotations help at API boundaries: a function can accept only known modes, and a module can publish a constant that other code should treat as fixed. Python still runs the same assignment rules at runtime, so these are promises for tools and readers rather than runtime locks.\n\nUse `Literal` when a small closed set is clearer than a broad `str` or `int`. Use `Final` when rebinding would be a bug in the design, especially for module constants and class attributes.\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"))\n\nDEFAULT_MODE = "debug"\nprint(DEFAULT_MODE)\n```\n:::\n\n:::cell\n`Literal` describes exact allowed values. A type checker can reject `"debug"` as a `Mode` even though it is an ordinary string at runtime.\n\n```python\nfrom typing import Final, Literal\n\nMode = Literal["read", "write"]\n\n\ndef open_label(mode: Mode) -> str:\n return f"opening for {mode}"\n\nprint(open_label("write"))\n```\n\n```output\nopening for write\n```\n:::\n\n:::cell\n`Final` marks a name that should not be rebound. It is stronger documentation than the all-caps constant convention because static tools can flag reassignment.\n\n```python\nDEFAULT_MODE: Final[Mode] = "read"\nprint(open_label(DEFAULT_MODE))\n```\n\n```output\nopening for read\n```\n:::\n\n:::cell\nThe annotation is not a runtime lock. Python still rebinds the name; the mistake is that a type checker and human reader should reject the design.\n\n```python\nDEFAULT_MODE = "debug"\nprint(DEFAULT_MODE)\n```\n\n```output\ndebug\n```\n:::\n\n:::note\n- `Literal` narrows values to a small exact set.\n- `Final` prevents rebinding in static analysis, not at runtime.\n- Use enums when the option set needs names, behavior, or iteration over members.\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"\nsee_also = [\n "exceptions",\n "testing",\n "modules",\n]\n+++\n\n`logging` records operational events without using `print` as infrastructure. A logger names where an event came from, a handler decides where records go, a formatter chooses their text shape, and a level decides which records are important enough to emit.\n\nUse logging for services, command-line tools, scheduled jobs, and libraries that need diagnostics operators can filter. Use `print` for a program\'s intentional user-facing output.\n\nThe example writes to stdout so the page stays deterministic. Real applications usually configure handlers 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.worker")\nlogger.setLevel(logging.DEBUG)\nhandler = logging.StreamHandler(sys.stdout)\nhandler.setLevel(logging.INFO)\nparts = ["%(levelname)s", "%(name)s", "%(message)s"]\nformatter = logging.Formatter(":".join(parts))\nhandler.setFormatter(formatter)\nlogger.handlers[:] = [handler]\nlogger.propagate = False\n\nlogger.debug("hidden detail")\nlogger.info("service started")\nlogger.warning("disk almost full")\n\nhandler.setLevel(logging.WARNING)\nlogger.info("hidden after threshold change")\nlogger.error("write failed")\n```\n:::\n\n:::cell\nA logger name records which part of the program produced the event. The handler and formatter choose where and how the event is shown.\n\n```python\nimport logging\nimport sys\n\nlogger = logging.getLogger("example.worker")\nlogger.setLevel(logging.DEBUG)\nhandler = logging.StreamHandler(sys.stdout)\nhandler.setLevel(logging.INFO)\nparts = ["%(levelname)s", "%(name)s", "%(message)s"]\nformatter = logging.Formatter(":".join(parts))\nhandler.setFormatter(formatter)\nlogger.handlers[:] = [handler]\nlogger.propagate = False\n\nlogger.debug("hidden detail")\nlogger.info("service started")\nlogger.warning("disk almost full")\n```\n\n```output\nINFO:example.worker:service started\nWARNING:example.worker:disk almost full\n```\n:::\n\n:::cell\nLevels are thresholds. Raising the handler level to `WARNING` suppresses later `INFO` records without changing the call sites.\n\n```python\nimport logging\nimport sys\n\nlogger = logging.getLogger("example.worker")\nlogger.setLevel(logging.DEBUG)\nhandler = logging.StreamHandler(sys.stdout)\nhandler.setLevel(logging.WARNING)\nparts = ["%(levelname)s", "%(name)s", "%(message)s"]\nformatter = logging.Formatter(":".join(parts))\nhandler.setFormatter(formatter)\nlogger.handlers[:] = [handler]\nlogger.propagate = False\n\nlogger.info("hidden after threshold change")\nlogger.error("write failed")\n```\n\n```output\nERROR:example.worker:write failed\n```\n:::\n\n:::note\n- Configure logging once; call named loggers throughout the program.\n- Logger and handler levels both participate in filtering.\n- Use exceptions for control flow failures, logging for operational evidence, and warnings for soft compatibility problems.\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"\nsee_also = [\n "conditionals",\n "advanced-match-patterns",\n "structured-data-shapes",\n "dicts",\n]\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"\nsee_also = [\n "tuples",\n "unpacking",\n "functions",\n]\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"\nsee_also = [\n "variables",\n "object-lifecycle",\n "copying-collections",\n "lists",\n]\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"\nsee_also = [\n "bytes-and-bytearray",\n "subprocesses",\n "async-await",\n]\nexpected_output = "b\'ping\'\\nping\\n"\n+++\n\nNetworking code sends and receives bytes across protocol boundaries. Higher-level HTTP clients hide many details, but the core rule remains: text is encoded before it leaves the process and decoded after bytes come back.\n\nIn standard Python, the socket version of this lesson uses connected endpoints such as `socket.create_connection()` or, for a local deterministic demonstration, `socket.socketpair()`. This site\'s live example runner does not expose arbitrary OS sockets or outbound calls, so this page teaches the socket contract while making the runner constraint explicit.\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 — the Python By Example runner does not expose arbitrary sockets and disables outbound access for edited examples.)\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- Cloudflare Workers support HTTP-style networking through platform APIs; this example avoids outbound calls so the editable lesson stays deterministic and safe.\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"\nsee_also = [\n "values",\n "truthiness",\n "exceptions",\n "dicts",\n]\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"\nsee_also = [\n "exceptions",\n "strings",\n "numbers",\n]\n+++\n\nParsing turns text from files, forms, command lines, or network messages into numeric objects. `int()` parses whole-number text, and `float()` parses decimal or scientific-notation text.\n\nInvalid numeric text raises `ValueError`. Catch that specific exception when bad user input is expected and recoverable; let it fail loudly when the string is supposed to be trusted program data.\n\n`int()` also accepts a base, which is useful at protocol boundaries where numbers are written in hexadecimal, binary, or another explicit notation.\n\n:::program\n```python\nprint(int("42"))\nprint(float("3.5"))\nprint(int("ff", 16))\n\ntexts = ["10", "python", "20"]\nfor text in texts:\n try:\n print(int(text) * 2)\n except ValueError:\n print(f"skip {text!r}")\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\nPass a base when the text format says the number is not decimal.\n\n```python\nprint(int("ff", 16))\n```\n\n```output\n255\n```\n:::\n\n:::cell\nCatch `ValueError` at the input boundary when invalid text is normal and recoverable.\n\n```python\ntexts = ["10", "python", "20"]\nfor text in texts:\n try:\n print(int(text) * 2)\n except ValueError:\n print(f"skip {text!r}")\n```\n\n```output\n20\nskip \'python\'\n40\n```\n:::\n\n:::note\n- `int()` and `float()` are constructors that also parse strings.\n- `int(text, base)` makes non-decimal input explicit.\n- Catch `ValueError` for recoverable user input; do not hide unexpected data corruption.\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 = "Names keep objects reachable until the last reference goes away."\ndoc_path = "/reference/datamodel.html#objects-values-and-types"\nsee_also = [\n "variables",\n "mutability",\n "classes",\n]\n+++\n\nPython objects live independently from the names that refer to them. Assignment adds another reference to an object; rebinding a name points that name somewhere else; `del` removes a name. The object can be reclaimed only after it is no longer reachable.\n\nMost programs do not manually destroy objects. They control lifetime by controlling which containers, local variables, and object attributes still hold references.\n\nThis example uses a small class so the object has visible state. The important evidence is that deleting one name does not destroy the object while another name still refers to it.\n\n:::program\n```python\nclass Box:\n def __init__(self, label):\n self.label = label\n\nbox = Box("draft")\nalias = box\n\nprint(box is alias)\nprint(alias.label)\n\nbox = Box("published")\nprint(alias.label)\nprint(box.label)\n\ndel alias\nprint("old object unreachable")\n```\n:::\n\n:::cell\nTwo names can refer to the same object. Mutating through one name would affect the object seen through the other.\n\n```python\nclass Box:\n def __init__(self, label):\n self.label = label\n\nbox = Box("draft")\nalias = box\n\nprint(box is alias)\nprint(alias.label)\n```\n\n```output\nTrue\ndraft\n```\n:::\n\n:::cell\nRebinding `box` does not change the original object. `alias` still reaches the first `Box` until that reference is removed too.\n\n```python\nbox = Box("published")\nprint(alias.label)\nprint(box.label)\n\ndel alias\nprint("old object unreachable")\n```\n\n```output\ndraft\npublished\nold object unreachable\n```\n:::\n\n:::note\n- Assignment binds names to objects; it does not copy the object.\n- `del name` removes one reference, not necessarily the object itself.\n- Python reclaims unreachable objects automatically, so lifetime bugs usually come from keeping references longer than intended.\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"\nsee_also = [\n "type-hints",\n "union-and-optional-types",\n "generics-and-typevar",\n]\n+++\n\n`@overload` lets type checkers describe a function whose return type depends on the argument types. The overload declarations are static-only promises; the runtime function is still the single implementation that appears after them.\n\nUse overloads when a union return type would be too vague for callers. For example, `double(4)` returns an `int`, while `double("ha")` returns a `str`; `int | str` loses that relationship.\n\nAt runtime the overload stubs are not dispatch cases. The implementation must inspect or operate on the value just like any other Python function.\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: int | str) -> int | str:\n return value * 2\n\nprint(double(4))\nprint(double("ha"))\nprint(double.__annotations__)\n```\n:::\n\n:::cell\nThe overload stubs give static tools precise call shapes: integer in, integer out; string in, string out.\n\n```python\nfrom typing import overload\n\n@overload\ndef double(value: int) -> int: ...\n\n@overload\ndef double(value: str) -> str: ...\n\nprint("static signatures only")\n```\n\n```output\nstatic signatures only\n```\n:::\n\n:::cell\nThere is still one runtime implementation. It must accept every shape promised by the overloads.\n\n```python\ndef double(value: int | str) -> int | str:\n return value * 2\n\nprint(double(4))\nprint(double("ha"))\n```\n\n```output\n8\nhaha\n```\n:::\n\n:::cell\nOnly the implementation\'s annotations are visible on the runtime function. The overload declarations were for the type checker.\n\n```python\nprint(double.__annotations__)\n```\n\n```output\n{\'value\': int | str, \'return\': int | str}\n```\n:::\n\n:::note\n- Put `@overload` declarations immediately before the implementation.\n- Overloads improve static precision; they do not create runtime dispatch.\n- If all callers can work with one broad return type, a simple union annotation is usually enough.\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"\nsee_also = [\n "callable-types",\n "decorators",\n "generics-and-typevar",\n]\n+++\n\n`ParamSpec` is for decorators and wrapper functions that should keep the wrapped callable\'s parameter shape. Without it, a generic decorator often falls back to `Callable[..., R]`, which says “this returns the right type, but I no longer know what arguments are valid.”\n\nUse `ParamSpec` when the wrapper forwards `*args` and `**kwargs` to the original function without changing the signature. Use a plain `Callable` when the wrapper deliberately accepts a different set of parameters.\n\n`P.args` and `P.kwargs` annotate the wrapper\'s forwarded arguments. A separate `TypeVar` keeps the return type tied to the wrapped function\'s return type.\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 erased(func: Callable[..., R]) -> Callable[..., R]:\n return func\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(erased(add)(2, 3))\nprint(add(2, 3))\n```\n:::\n\n:::cell\n`Callable[..., R]` is sometimes too broad. It preserves the return type, but the ellipsis means the callable accepts any argument list as far as the type checker can tell.\n\n```python\nfrom collections.abc import Callable\nfrom typing import ParamSpec, TypeVar\n\nR = TypeVar("R")\n\n\ndef erased(func: Callable[..., R]) -> Callable[..., R]:\n return func\n\nprint(erased.__name__)\n```\n\n```output\nerased\n```\n:::\n\n:::cell\n`ParamSpec` captures the original parameters and lets the wrapper forward exactly that shape.\n\n```python\nP = ParamSpec("P")\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\nprint(logged.__name__)\n```\n\n```output\nlogged\n```\n:::\n\n:::cell\nThe decorated function still runs normally. The benefit is static: tools can keep checking that `add` receives two integers.\n\n```python\n@logged\ndef add(left: int, right: int) -> int:\n return left + right\n\nprint(erased(add)(2, 3))\nprint(add(2, 3))\n```\n\n```output\ncalling add\n5\ncalling add\n5\n```\n:::\n\n:::note\n- `ParamSpec` preserves a callable\'s parameter list through transparent wrappers.\n- Pair `ParamSpec` with a `TypeVar` when the return type should also be preserved.\n- If the wrapper changes the public signature, write that new signature directly instead.\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"\nsee_also = [\n "functions",\n "args-and-kwargs",\n "callable-objects",\n]\n+++\n\n`functools.partial` turns a general callable into a more specific callable by remembering some positional or keyword arguments. It is useful when another API wants a one-argument callback but your underlying function needs more context.\n\nA partial object is still callable. It keeps the original function in `.func`, pre-filled positional arguments in `.args`, and pre-filled keyword arguments in `.keywords`.\n\nPrefer a named wrapper function when the adapted behavior needs branching, validation, or a docstring. Use `partial` when the adaptation is simply "call this function with these arguments already supplied."\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(apply_tax(0.2, 50))\nprint(vat(50))\nprint(service_tax(amount=80))\nprint(vat.func.__name__)\nprint(vat.args)\n```\n:::\n\n:::cell\nWithout `partial`, callers repeat the same fixed argument every time they want the specialized behavior.\n\n```python\ndef apply_tax(rate, amount):\n return round(amount * (1 + rate), 2)\n\nprint(apply_tax(0.2, 50))\n```\n\n```output\n60.0\n```\n:::\n\n:::cell\n`partial` stores that fixed argument and returns a callable shaped for the remaining arguments.\n\n```python\nfrom functools import partial\n\nvat = partial(apply_tax, 0.2)\nservice_tax = partial(apply_tax, rate=0.1)\n\nprint(vat(50))\nprint(service_tax(amount=80))\n```\n\n```output\n60.0\n88.0\n```\n:::\n\n:::cell\nPartial objects expose the function and stored arguments, which is helpful when debugging callback wiring.\n\n```python\nprint(vat.func.__name__)\nprint(vat.args)\n```\n\n```output\napply_tax\n(0.2,)\n```\n:::\n\n:::note\n- `partial` adapts a callable by pre-filling arguments.\n- The resulting object can be passed anywhere a callable with the remaining parameters is expected.\n- Use a regular function when the adapter needs more logic than argument binding.\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"\nsee_also = [\n "classes",\n "attribute-access",\n "descriptors",\n "dataclasses",\n]\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"\nsee_also = [\n "functions",\n "conditionals",\n "generators",\n]\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"\nsee_also = [\n "iterators",\n "while-loops",\n "break-and-continue",\n]\n+++\n\n`iter(callable, sentinel)` calls a zero-argument callable over and over. It yields each result until the callable returns the sentinel value, and the sentinel itself is not yielded.\n\nThis shape is useful for repeated reads: file blocks until `b""`, socket chunks until an empty response, queue items until a stop marker. It removes the common `while True` plus `break` scaffolding when the loop body is otherwise just "read, then process".\n\nThe callable must take no arguments. Wrap a parameterized reader in a `lambda`, `functools.partial`, or object method when the underlying API needs parameters.\n\n:::program\n```python\nchunks = iter(["py", "thon", ""])\n\n\ndef read_chunk():\n return next(chunks)\n\nprint(list(iter(read_chunk, "")))\n\nchunks = iter(["py", "thon", ""])\nword = ""\nwhile True:\n chunk = next(chunks)\n if chunk == "":\n break\n word += chunk\nprint(word)\n```\n:::\n\n:::cell\nThe two-argument form turns a polling callable into an iterator. The empty string stops the loop without appearing in the result.\n\n```python\nchunks = iter(["py", "thon", ""])\n\n\ndef read_chunk():\n return next(chunks)\n\nprint(list(iter(read_chunk, "")))\n```\n\n```output\n[\'py\', \'thon\']\n```\n:::\n\n:::cell\nThe equivalent manual loop needs an explicit read, comparison, and `break`. Use this shape when the stop condition is more complicated than a single sentinel value.\n\n```python\nchunks = iter(["py", "thon", ""])\nword = ""\nwhile True:\n chunk = next(chunks)\n if chunk == "":\n break\n word += chunk\nprint(word)\n```\n\n```output\npython\n```\n:::\n\n:::note\n- The callable passed to `iter(callable, sentinel)` must take no arguments.\n- The sentinel stops iteration and is not yielded.\n- When the loop needs richer branching, an explicit `while` loop may be clearer.\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"\nsee_also = [\n "lists",\n "dicts",\n "comprehensions",\n]\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"\nsee_also = [\n "lists",\n "tuples",\n "strings",\n]\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"\nsee_also = [\n "lists",\n "lambdas",\n "functions",\n]\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"\nsee_also = [\n "strings",\n "logging",\n "csv-data",\n "values",\n]\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"\nsee_also = [\n "values",\n "string-formatting",\n "bytes-and-bytearray",\n "regular-expressions",\n]\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"\nsee_also = [\n "virtual-environments",\n "networking",\n "threads-and-processes",\n]\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\nIn standard Python this is the right tool for calling Git, compilers, shells, or another Python interpreter. This site\'s live example runner does not expose an operating-system process table, so the page teaches the proper `subprocess.run()` contract and labels the runner boundary instead of pretending the command can run here.\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 important boundary is between Python objects and the operating system: Python prepares arguments and environment, then the child program 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 — the Python By Example runner does 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- If you run this in local/server Python, the child process is real; on this site, the runnable evidence preserves the API shape without spawning a process.\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"\nsee_also = [\n "async-await",\n "subprocesses",\n "networking",\n]\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\nIn standard Python, `ThreadPoolExecutor` and `ProcessPoolExecutor` are the ordinary tools for this lesson. This site\'s live example runner does not expose native threads or child processes, so this page keeps the proper executor model visible and separates the standard Python idea from what can execute here.\n\nThis is different from `asyncio`: threads and processes run ordinary callables through executors, while `async` code cooperatively awaits coroutines. Choose the smallest concurrency model that matches the bottleneck.\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 — the Python By Example runner does 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- The displayed executor names are standard Python concepts; the site avoids actually creating host threads or processes in the live runner.\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"\nsee_also = [\n "booleans",\n "none",\n "conditionals",\n "special-methods",\n]\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"\nsee_also = [\n "tuples",\n "multiple-return-values",\n "args-and-kwargs",\n "dicts",\n]\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"\nsee_also = [\n "variables",\n "booleans",\n "none",\n "literals",\n]\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"\nsee_also = [\n "values",\n "mutability",\n "object-lifecycle",\n "constants",\n]\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"\nsee_also = [\n "packages",\n "modules",\n "import-aliases",\n]\nexpected_output = ".venv\\nTrue\\n"\n+++\n\nVirtual environments isolate a project\'s installed packages from the global Python installation and from other projects. The usual workflow is a command-line one: create `.venv`, activate it, then install project dependencies there.\n\nIn standard Python, `python -m venv .venv` is the everyday command. This site\'s live example runner is built from declared dependencies rather than an activated shell environment, so the runnable part keeps to deterministic evidence while the page still teaches the standard-Python workflow.\n\nA virtual environment changes installation and import paths. It does not change the Python language, package layout rules, or 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 config = (env_path / "pyvenv.cfg").read_text()\n print(env_path.name)\n print("home" in config)\n```\n:::\n\n:::unsupported\nThe standard project setup command is `python -m venv .venv`. It creates a directory with its own interpreter entry points and package install location. After activation, `python -m pip install ...` installs into that environment rather than into another project. (This workflow is for standard Python projects. The Python By Example runner is deployed from declared dependencies instead of an activated shell environment.)\n\n```python\nimport subprocess\nimport sys\n\nsubprocess.run([sys.executable, "-m", "venv", ".venv"], check=True)\nsubprocess.run([".venv/bin/python", "-m", "pip", "install", "requests"], check=True)\n```\n:::\n\n:::cell\n`venv.EnvBuilder` exposes the same environment-creation mechanism 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 config = (env_path / "pyvenv.cfg").read_text()\n print(env_path.name)\n print("home" in config)\n```\n\n```output\n.venv\nTrue\n```\n:::\n\n:::note\n- Use `python -m venv .venv` for everyday standard-Python project setup.\n- A venv isolates installed packages; it does not change how imports are written.\n- This site\'s runner uses a deployment dependency model, not an activated shell environment.\n- That runner constraint is separate from the standard Python `venv` workflow you would use in local projects.\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"\nsee_also = [\n "exceptions",\n "logging",\n "testing",\n]\n+++\n\nA warning reports a problem that callers should know about, but it does not have to stop the current operation. Deprecations are the classic case: the old API can still return a value while telling users to migrate.\n\nWarnings sit between logging and exceptions. Logging records operational evidence; exceptions stop the current path; warnings make compatibility or correctness concerns visible according to a filter.\n\nTests often capture warnings so deprecations are asserted instead of merely printed. Filters can also turn warnings into errors when a project wants to enforce cleanup.\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\nwith warnings.catch_warnings(record=True) as caught:\n warnings.simplefilter("always", DeprecationWarning)\n print(old_name())\n print(caught[0].category.__name__)\n print(str(caught[0].message))\n\nwith warnings.catch_warnings():\n warnings.simplefilter("error", DeprecationWarning)\n try:\n old_name()\n except DeprecationWarning:\n print("warning became error")\n```\n:::\n\n:::cell\nCapture warnings in tests when the returned value still matters but the migration notice must be asserted.\n\n```python\nimport warnings\n\n\ndef old_name():\n warnings.warn("old_name is deprecated", DeprecationWarning, stacklevel=2)\n return "result"\n\nwith warnings.catch_warnings(record=True) as caught:\n warnings.simplefilter("always", DeprecationWarning)\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:::cell\nA filter can promote selected warnings to exceptions, which is useful in CI when deprecated calls should fail the build.\n\n```python\nwith warnings.catch_warnings():\n warnings.simplefilter("error", DeprecationWarning)\n try:\n old_name()\n except DeprecationWarning:\n print("warning became error")\n```\n\n```output\nwarning became error\n```\n:::\n\n:::note\n- Use warnings for soft problems callers can act on later.\n- Use exceptions when the current operation cannot continue.\n- `stacklevel` should point the warning at the caller rather than inside the helper.\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"\nsee_also = [\n "for-loops",\n "sentinel-iteration",\n "break-and-continue",\n]\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"\nsee_also = [\n "functions",\n "keyword-only-arguments",\n "partial-functions",\n "paramspec",\n]\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\ndef slug_to_title(slug):\n return slug.replace("-", " ").title()\n\nasync def fetch_title(slug):\n await asyncio.sleep(0)\n return slug_to_title(slug)\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 ordinary `def` function computes its result immediately: calling it runs the body and hands the value straight back. This synchronous form is the baseline the rest of the page contrasts against.\n\n```python\ndef slug_to_title(slug):\n return slug.replace("-", " ").title()\n\nprint(slug_to_title("async-await"))\n```\n\n```output\nAsync Await\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_to_title(slug)\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](/examples/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\nThe starting point is ordinary: `__init__` stores one real attribute, the `_values` backing dictionary. The hooks in the next cells customize lookup and assignment around it.\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 every assignment, including the ones in `__init__`. Underscore names are stored as real attributes through `object.__setattr__`, which avoids recursing through your own hook; public names go to 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"\nsee_also = [\n "truthiness",\n "operators",\n "conditionals",\n]\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- "Unbound method" is the historical Python 2 term; since Python 3, `Class.method` is simply a function, which is what this page demonstrates.\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 = "Text"\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\nvalue: object = {"score": "98"}\nif isinstance(value, dict):\n print(value["score"])\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"\nsee_also = [\n "functions",\n "lambdas",\n "decorators",\n "partial-functions",\n]\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"\nsee_also = [\n "dicts",\n "lists",\n "tuples",\n "sets",\n]\n+++\n\n`collections` provides specialized containers for common shapes that would otherwise require repetitive plumbing. Use it when the shape has a name: counting, grouping, queueing, or lightweight records.\n\nThese types are not replacements for `list`, `dict`, `tuple`, and `set`. They are small standard-library tools for cases where an ordinary container would hide the intent behind manual bookkeeping.\n\nThe examples below map each type to the question it answers.\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 the question is "how many times did each value appear?"\n\n```python\nfrom collections import Counter\n\ncounts = Counter("banana")\nprint(counts.most_common(2))\n```\n\n```output\n[(\'a\', 3), (\'n\', 2)]\n```\n:::\n\n:::cell\nUse `defaultdict(list)` when each key gathers multiple values and the missing-key case should create an empty list automatically.\n\n```python\nfrom collections import defaultdict\n\ngroups = defaultdict(list)\nfor name, team in [("Ada", "red"), ("Grace", "blue"), ("Lin", "red")]:\n groups[team].append(name)\nprint(dict(groups))\n```\n\n```output\n{\'red\': [\'Ada\', \'Lin\'], \'blue\': [\'Grace\']}\n```\n:::\n\n:::cell\nUse `deque` for queue operations at both ends, and `namedtuple` when a tiny immutable record needs names as well as positions.\n\n```python\nfrom collections import deque, namedtuple\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\nfirst\n2\n```\n:::\n\n:::note\n- `Counter` counts, `defaultdict` groups, `deque` queues, and `namedtuple` names record fields.\n- Prefer the built-in containers until a specialized shape makes the code clearer.\n- For new structured records with defaults and methods, consider `dataclasses` instead of `namedtuple`.\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"\nsee_also = [\n "for-loops",\n "generator-expressions",\n "comprehension-patterns",\n "lists",\n]\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(sorted(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 — printed through `sorted()` because sets have no display order to rely on.\n\n```python\nunique_scores = {score for score in scores.values()}\nprint(sorted(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"\nsee_also = [\n "booleans",\n "truthiness",\n "guard-clauses",\n "match-statements",\n]\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 and optional types for values that should not change."\ndoc_path = "/library/typing.html#typing.Final"\nsee_also = [\n "variables",\n "literal-and-final",\n "type-hints",\n]\n+++\n\nPython has no `const` keyword for ordinary names. Modules use all-caps names such as `MAX_RETRIES` to say “treat this as fixed configuration, not changing state.”\n\nThe interpreter will still let code rebind the name. That is why constants are primarily an API and readability convention. If a project also uses static typing, `Final` can make the convention machine-checkable.\n\nNamed constants remove magic values from code and give repeated literals one place to change.\n\n:::program\n```python\nfrom typing import Final\n\nMAX_RETRIES: Final = 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\nMAX_RETRIES = 5\nprint(MAX_RETRIES)\n```\n:::\n\n:::cell\nAll-caps names communicate design intent: this value is configuration that callers should treat as fixed.\n\n```python\nMAX_RETRIES = 3\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 when a repeated literal deserves a name at the domain boundary.\n\n```python\nAPI_VERSION = "2026-05"\nprint(API_VERSION)\n```\n\n```output\n2026-05\n```\n:::\n\n:::cell\n`Final` lets type checkers reject reassignment, but Python still runs ordinary rebinding at runtime.\n\n```python\nfrom typing import Final\n\nMAX_RETRIES: Final = 3\nMAX_RETRIES = 5\nprint(MAX_RETRIES)\n```\n\n```output\n5\n```\n:::\n\n:::note\n- Python constants are a convention, not a runtime lock.\n- Use all-caps names for fixed module-level configuration.\n- Add `Final` when static tooling should flag accidental rebinding.\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 the outer container while nested objects may still be shared."\ndoc_path = "/library/copy.html"\nsee_also = [\n "mutability",\n "lists",\n "dicts",\n]\n+++\n\nCopying answers two different questions: do you need a new outer container, or do you also need independent nested objects? A plain assignment gives another name for the same object. A shallow copy duplicates only the outer container. `copy.deepcopy()` recursively copies contained objects.\n\nMost Python code wants a shallow copy or a deliberate rebuild. Use a deep copy only when shared nested state would be wrong and the objects involved are safe to duplicate.\n\nThe outputs below show the footgun directly: a shallow copy has a different outer list, but its inner lists are still the same objects.\n\n:::program\n```python\nimport copy\n\nrows = [["Ada"], ["Grace"]]\nalias = rows\nshallow = rows.copy()\ndeep = copy.deepcopy(rows)\n\nrows[0].append("Lovelace")\n\nprint(alias is rows)\nprint(shallow is rows)\nprint(rows[0] is shallow[0])\nprint(rows[0] is deep[0])\nprint(shallow)\nprint(deep)\n```\n:::\n\n:::cell\nAssignment does not copy a collection. It gives the same list another name.\n\n```python\nrows = [["Ada"], ["Grace"]]\nalias = rows\n\nprint(alias is rows)\n```\n\n```output\nTrue\n```\n:::\n\n:::cell\nA shallow copy creates a new outer list, but nested lists are still shared.\n\n```python\nshallow = rows.copy()\nrows[0].append("Lovelace")\n\nprint(shallow is rows)\nprint(rows[0] is shallow[0])\nprint(shallow)\n```\n\n```output\nFalse\nTrue\n[[\'Ada\', \'Lovelace\'], [\'Grace\']]\n```\n:::\n\n:::cell\nA deep copy is independent at the nested level, so later mutation of `rows[0]` does not appear in `deep`.\n\n```python\nimport copy\n\nrows = [["Ada"], ["Grace"]]\ndeep = copy.deepcopy(rows)\nrows[0].append("Lovelace")\n\nprint(rows[0] is deep[0])\nprint(deep)\n```\n\n```output\nFalse\n[[\'Ada\'], [\'Grace\']]\n```\n:::\n\n:::note\n- Assignment aliases; it does not copy.\n- Shallow copies duplicate the outer container only.\n- Deep copies are useful for nested independence, but they can be expensive and surprising for objects with external resources.\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"\nsee_also = [\n "strings",\n "dicts",\n "json",\n]\n+++\n\nCSV is row-shaped text: each line is a record, and each comma-separated field arrives as a string. The `csv` module understands quoting, delimiters, and newlines, so it is safer than splitting lines by comma yourself.\n\nUse `DictReader` when a header row names the columns. Convert fields explicitly after reading, and use `DictWriter` when the program needs to produce the same row shape again.\n\nCSV is a good fit for flat tabular data. Use JSON or another structured format when values are nested or when types need to survive the text boundary.\n\n:::program\n```python\nimport csv\nimport io\n\ntext = "name,score\\nAda,98\\nGrace,95\\n"\nrows = list(csv.DictReader(io.StringIO(text)))\nprint(rows[0])\nprint(sum(int(row["score"]) for row in rows))\n\noutput = io.StringIO(newline="")\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\n`DictReader` uses the header row as dictionary keys. The values are still strings because CSV is text.\n\n```python\nimport csv\nimport io\n\ntext = "name,score\\nAda,98\\nGrace,95\\n"\nrows = list(csv.DictReader(io.StringIO(text)))\n\nprint(rows[0])\nprint(type(rows[0]["score"]).__name__)\n```\n\n```output\n{\'name\': \'Ada\', \'score\': \'98\'}\nstr\n```\n:::\n\n:::cell\nConvert numeric fields at the boundary where the program leaves CSV text and starts doing arithmetic.\n\n```python\nprint(sum(int(row["score"]) for row in rows))\n```\n\n```output\n193\n```\n:::\n\n:::cell\n`DictWriter` turns dictionaries back into row-shaped text with the same column order.\n\n```python\noutput = io.StringIO(newline="")\nwriter = csv.DictWriter(output, fieldnames=["name", "passed"])\nwriter.writeheader()\nwriter.writerow({"name": "Ada", "passed": True})\n\nprint(output.getvalue().splitlines()[1])\n```\n\n```output\nAda,True\n```\n:::\n\n:::note\n- Let `csv` handle quoting and delimiters instead of calling `split(",")`.\n- CSV fields are text until your code converts them.\n- Reach for JSON when records need nested lists, dictionaries, booleans, or numbers that preserve their type.\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"\nsee_also = [\n "exceptions",\n "exception-chaining",\n "warnings",\n "logging",\n]\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"\nsee_also = [\n "string-formatting",\n "json",\n "number-parsing",\n]\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__)\nprint(welcome.__doc__)\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\nA descriptor is an object stored on a class that defines `__get__`, `__set__`, or `__delete__`. When an instance attribute lookup finds that object on the class, Python calls the descriptor method instead of returning the descriptor object directly.\n\nDescriptors are the machinery behind methods, `property`, validators, and many ORM fields. Use them when one reusable object should control access for many attributes or classes; use `property` for a single simple managed attribute.\n\nThis example implements a positive-number validator. `__set_name__` learns the attribute name when the owner class is created, `__set__` validates writes, and `__get__` reads the stored value back from the instance.\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 if obj is None:\n return self\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)\nprint(Product.price.private_name)\ntry:\n item.price = -1\nexcept ValueError as error:\n print(error)\n```\n:::\n\n:::cell\nA descriptor object lives on the class. `__set_name__` lets it learn which managed attribute it is serving.\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 if obj is None:\n return self\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\nprint(Product.price.private_name)\n```\n\n```output\n_price\n```\n:::\n\n:::cell\nAssigning `item.price` calls `Positive.__set__`, and reading it calls `Positive.__get__`.\n\n```python\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- Descriptors are class attributes that participate in instance attribute access.\n- Data descriptors with `__set__` can validate or transform assignments.\n- `property` is usually simpler for one-off managed attributes; descriptors shine when the behavior is reusable.\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"\nsee_also = [\n "lists",\n "sets",\n "typed-dicts",\n "json",\n]\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\nAdding or removing keys while iterating a dictionary raises `RuntimeError` ("dictionary changed size during iteration"); reassigning an existing key\'s value is allowed. Snapshot the keys with `list(d.keys())` (or build a list of changes and apply them after the loop) so deletions see 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; adding or removing keys 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"\nsee_also = [\n "literals",\n "classes",\n "literal-and-final",\n]\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"\nsee_also = [\n "none",\n "values",\n "object-lifecycle",\n "mutability",\n]\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"\nsee_also = [\n "conditionals",\n "guard-clauses",\n "custom-exceptions",\n "warnings",\n]\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"))\nprint(safe_parse_broken(["4", "2"]))\n\ntry:\n safe_parse_fixed(["4", "2"])\nexcept TypeError as error:\n print(type(error).__name__)\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). The two functions look interchangeable on good input — the divergence appears on a buggy call: passing a list is a programming error, yet the broad version converts it into a quiet `None` while the specific version lets the `TypeError` 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"))\nprint(safe_parse_broken(["4", "2"]))\n\ntry:\n safe_parse_fixed(["4", "2"])\nexcept TypeError as error:\n print(type(error).__name__)\n```\n\n```output\n42\n42\nNone\nTypeError\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 values produced by an iterable."\ndoc_path = "/tutorial/controlflow.html#for-statements"\nsee_also = [\n "while-loops",\n "iterating-over-iterables",\n "iterators",\n]\n+++\n\nA `for` loop asks an iterable for values and runs the indented block once per value. Python\'s loop is not primarily a numeric counter; it is a consumer of lists, ranges, files, generators, and any object that implements the iterator protocol.\n\nPrefer direct iteration when you need each value. Use `range()` when the numbers themselves are the data, and use `enumerate()` when the position and the value both matter.\n\nThe loop body is the indented block. When the iterable is exhausted, execution continues after the block. The neighboring `while` loop shape is for conditions that must be rechecked manually.\n\n:::program\n```python\nfor name in ["Ada", "Grace", "Guido"]:\n print(name)\n\nfor number in range(3):\n print(number)\n\nfor index, name in enumerate(["Ada", "Grace"], start=1):\n print(index, name)\n```\n:::\n\n:::cell\nDirect iteration keeps the code focused on the values in the collection.\n\n```python\nfor name in ["Ada", "Grace", "Guido"]:\n print(name)\n```\n\n```output\nAda\nGrace\nGuido\n```\n:::\n\n:::cell\n`range(3)` yields `0`, `1`, and `2` lazily. Use it when those integers are the thing being iterated over.\n\n```python\nfor number in range(3):\n print(number)\n```\n\n```output\n0\n1\n2\n```\n:::\n\n:::cell\n`enumerate()` is the usual Python way to keep a counter beside each value without indexing back into the list.\n\n```python\nfor index, name in enumerate(["Ada", "Grace"], start=1):\n print(index, name)\n```\n\n```output\n1 Ada\n2 Grace\n```\n:::\n\n:::note\n- A `for` loop consumes an iterable until it is exhausted.\n- Reach for `while` when the stopping condition must be rechecked manually.\n- `iter()` and `next()` expose the protocol that `for` uses internally.\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"\nsee_also = [\n "variables",\n "args-and-kwargs",\n "keyword-only-arguments",\n "closures",\n]\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"\nsee_also = [\n "comprehensions",\n "generators",\n "itertools",\n "yield-from",\n]\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- Python 3.12+ also accepts the inline PEP 695 spelling `def first[T](items: list[T]) -> T`, which declares the variable without a separate `TypeVar` line; the explicit form shown here works everywhere and reads the same way.\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 boundary cases early so the main path stays flat."\ndoc_path = "/tutorial/controlflow.html#if-statements"\nsee_also = [\n "conditionals",\n "exceptions",\n "functions",\n]\n+++\n\nA guard clause is an early `return`, `raise`, `break`, or `continue` that handles a case the rest of the function should not process. The point is not new syntax; the point is moving boundaries out of the way so the successful path can be read straight through.\n\nUse guards when a function has clear invalid, empty, or already-finished cases. If every branch is equally important, an ordinary `if`/`elif` chain may be clearer.\n\nThe contrast below shows the payoff: the nested version makes the valid path live inside two conditions, while the guard version names the invalid cases first and leaves the calculation at the outer indentation level.\n\n:::program\n```python\ndef nested_discount(price, percent):\n if price >= 0:\n if 0 <= percent <= 100:\n return round(price - price * percent / 100, 2)\n return "invalid discount"\n return "invalid price"\n\n\ndef guarded_discount(price, percent):\n if price < 0:\n return "invalid price"\n if not 0 <= percent <= 100:\n return "invalid discount"\n\n return round(price - price * percent / 100, 2)\n\nprint(nested_discount(100, 15))\nprint(guarded_discount(-5, 10))\nprint(guarded_discount(100, 120))\n```\n:::\n\n:::cell\nThe nested version is correct, but the useful work is buried inside both tests.\n\n```python\ndef nested_discount(price, percent):\n if price >= 0:\n if 0 <= percent <= 100:\n return round(price - price * percent / 100, 2)\n return "invalid discount"\n return "invalid price"\n\nprint(nested_discount(100, 15))\n```\n\n```output\n85.0\n```\n:::\n\n:::cell\nThe guard-clause version handles impossible inputs first, then lets the ordinary calculation sit at the top level of the function body.\n\n```python\ndef guarded_discount(price, percent):\n if price < 0:\n return "invalid price"\n if not 0 <= percent <= 100:\n return "invalid discount"\n\n return round(price - price * percent / 100, 2)\n\nprint(guarded_discount(-5, 10))\nprint(guarded_discount(100, 120))\n```\n\n```output\ninvalid price\ninvalid discount\n```\n:::\n\n:::note\n- Guard clauses are a readability pattern, not a separate Python feature.\n- They work best when the early cases are true boundaries.\n- For exceptional failures, raise an exception instead of returning a sentinel string.\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"\nsee_also = [\n "values",\n "variables",\n]\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\nRun the program and compare what you see with the source: the text appears on standard output followed by a newline, which is why successive `print()` calls land on separate lines.\n\n:::program\n```python\nprint("hello world")\n```\n:::\n\n:::cell\nThe whole program is one statement. Python evaluates the string literal `"hello world"` and passes that value to `print()`, which writes it to standard output — the output panel shows exactly that line.\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"\nsee_also = [\n "iterators",\n "generator-expressions",\n "sentinel-iteration",\n "comprehension-patterns",\n]\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"\nsee_also = [\n "functions",\n "args-and-kwargs",\n "positional-only-parameters",\n "partial-functions",\n]\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\ntry:\n connect("example.com", 10)\nexcept TypeError as error:\n print(type(error).__name__)\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:::cell\nThe bare `*` is enforced at the call site: passing the timeout positionally raises `TypeError` instead of silently filling the wrong slot.\n\n```python\ntry:\n connect("example.com", 10)\nexcept TypeError as error:\n print(type(error).__name__)\n```\n\n```output\nTypeError\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"\nsee_also = [\n "functions",\n "sorting",\n "callable-objects",\n]\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"\nsee_also = [\n "tuples",\n "sets",\n "slices",\n "copying-collections",\n]\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 exact values, while Final marks names that should not be rebound."\ndoc_path = "/library/typing.html#typing.Literal"\nsee_also = [\n "type-hints",\n "constants",\n "union-and-optional-types",\n "overloads",\n]\n+++\n\n`Literal` and `Final` make two different static promises. `Literal` narrows a value to exact allowed options. `Final` tells a type checker that a name should not be rebound after its first assignment.\n\nBoth annotations help at API boundaries: a function can accept only known modes, and a module can publish a constant that other code should treat as fixed. Python still runs the same assignment rules at runtime, so these are promises for tools and readers rather than runtime locks.\n\nUse `Literal` when a small closed set is clearer than a broad `str` or `int`. Use `Final` when rebinding would be a bug in the design, especially for module constants and class attributes.\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"))\n\nDEFAULT_MODE = "debug"\nprint(DEFAULT_MODE)\n```\n:::\n\n:::cell\n`Literal` describes exact allowed values. A type checker can reject `"debug"` as a `Mode` even though it is an ordinary string at runtime.\n\n```python\nfrom typing import Final, Literal\n\nMode = Literal["read", "write"]\n\n\ndef open_label(mode: Mode) -> str:\n return f"opening for {mode}"\n\nprint(open_label("write"))\n```\n\n```output\nopening for write\n```\n:::\n\n:::cell\n`Final` marks a name that should not be rebound. It is stronger documentation than the all-caps constant convention because static tools can flag reassignment.\n\n```python\nDEFAULT_MODE: Final[Mode] = "read"\nprint(open_label(DEFAULT_MODE))\n```\n\n```output\nopening for read\n```\n:::\n\n:::cell\nThe annotation is not a runtime lock. Python still rebinds the name; the mistake is that a type checker and human reader should reject the design.\n\n```python\nDEFAULT_MODE = "debug"\nprint(DEFAULT_MODE)\n```\n\n```output\ndebug\n```\n:::\n\n:::note\n- `Literal` narrows values to a small exact set.\n- `Final` prevents rebinding in static analysis, not at runtime.\n- Use enums when the option set needs names, behavior, or iteration over members.\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"\nsee_also = [\n "exceptions",\n "testing",\n "modules",\n]\n+++\n\n`logging` records operational events without using `print` as infrastructure. A logger names where an event came from, a handler decides where records go, a formatter chooses their text shape, and a level decides which records are important enough to emit.\n\nUse logging for services, command-line tools, scheduled jobs, and libraries that need diagnostics operators can filter. Use `print` for a program\'s intentional user-facing output.\n\nThe example writes to stdout so the page stays deterministic. Real applications usually configure handlers 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.worker")\nlogger.setLevel(logging.DEBUG)\nhandler = logging.StreamHandler(sys.stdout)\nhandler.setLevel(logging.INFO)\nparts = ["%(levelname)s", "%(name)s", "%(message)s"]\nformatter = logging.Formatter(":".join(parts))\nhandler.setFormatter(formatter)\nlogger.handlers[:] = [handler]\nlogger.propagate = False\n\nlogger.debug("hidden detail")\nlogger.info("service started")\nlogger.warning("disk almost full")\n\nhandler.setLevel(logging.WARNING)\nlogger.info("hidden after threshold change")\nlogger.error("write failed")\n```\n:::\n\n:::cell\nA logger name records which part of the program produced the event. The handler and formatter choose where and how the event is shown.\n\n```python\nimport logging\nimport sys\n\nlogger = logging.getLogger("example.worker")\nlogger.setLevel(logging.DEBUG)\nhandler = logging.StreamHandler(sys.stdout)\nhandler.setLevel(logging.INFO)\nparts = ["%(levelname)s", "%(name)s", "%(message)s"]\nformatter = logging.Formatter(":".join(parts))\nhandler.setFormatter(formatter)\nlogger.handlers[:] = [handler]\nlogger.propagate = False\n\nlogger.debug("hidden detail")\nlogger.info("service started")\nlogger.warning("disk almost full")\n```\n\n```output\nINFO:example.worker:service started\nWARNING:example.worker:disk almost full\n```\n:::\n\n:::cell\nLevels are thresholds. Raising the handler level to `WARNING` suppresses later `INFO` records without changing the call sites.\n\n```python\nimport logging\nimport sys\n\nlogger = logging.getLogger("example.worker")\nlogger.setLevel(logging.DEBUG)\nhandler = logging.StreamHandler(sys.stdout)\nhandler.setLevel(logging.WARNING)\nparts = ["%(levelname)s", "%(name)s", "%(message)s"]\nformatter = logging.Formatter(":".join(parts))\nhandler.setFormatter(formatter)\nlogger.handlers[:] = [handler]\nlogger.propagate = False\n\nlogger.info("hidden after threshold change")\nlogger.error("write failed")\n```\n\n```output\nERROR:example.worker:write failed\n```\n:::\n\n:::note\n- Configure logging once; call named loggers throughout the program.\n- Logger and handler levels both participate in filtering.\n- Use exceptions for control flow failures, logging for operational evidence, and warnings for soft compatibility problems.\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"\nsee_also = [\n "conditionals",\n "advanced-match-patterns",\n "structured-data-shapes",\n "dicts",\n]\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- Mapping patterns match when the named keys are present; extra keys in the subject are ignored rather than failing the case.\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"\nsee_also = [\n "tuples",\n "unpacking",\n "functions",\n]\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"\nsee_also = [\n "variables",\n "object-lifecycle",\n "copying-collections",\n "lists",\n]\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"\nsee_also = [\n "bytes-and-bytearray",\n "subprocesses",\n "async-await",\n]\nexpected_output = "b\'ping\'\\nping\\n"\n+++\n\nNetworking code sends and receives bytes across protocol boundaries. Higher-level HTTP clients hide many details, but the core rule remains: text is encoded before it leaves the process and decoded after bytes come back.\n\nIn standard Python, the socket version of this lesson uses connected endpoints such as `socket.create_connection()` or, for a local deterministic demonstration, `socket.socketpair()`. This site\'s live example runner does not expose arbitrary OS sockets or outbound calls, so this page teaches the socket contract while making the runner constraint explicit.\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. (The in-browser Run button cannot open sockets — the sandbox disables outbound access — so pressing Run on this page fails; the verified output below comes from a real socket pair under standard CPython at build time.)\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- Cloudflare Workers support HTTP-style networking through platform APIs; this example avoids outbound calls so the editable lesson stays deterministic and safe.\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"\nsee_also = [\n "values",\n "truthiness",\n "exceptions",\n "dicts",\n]\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"\nsee_also = [\n "exceptions",\n "strings",\n "numbers",\n]\n+++\n\nParsing turns text from files, forms, command lines, or network messages into numeric objects. `int()` parses whole-number text, and `float()` parses decimal or scientific-notation text.\n\nInvalid numeric text raises `ValueError`. Catch that specific exception when bad user input is expected and recoverable; let it fail loudly when the string is supposed to be trusted program data.\n\n`int()` also accepts a base, which is useful at protocol boundaries where numbers are written in hexadecimal, binary, or another explicit notation.\n\n:::program\n```python\nprint(int("42"))\nprint(float("3.5"))\nprint(int("ff", 16))\n\ntexts = ["10", "python", "20"]\nfor text in texts:\n try:\n print(int(text) * 2)\n except ValueError:\n print(f"skip {text!r}")\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\nPass a base when the text format says the number is not decimal.\n\n```python\nprint(int("ff", 16))\n```\n\n```output\n255\n```\n:::\n\n:::cell\nCatch `ValueError` at the input boundary when invalid text is normal and recoverable.\n\n```python\ntexts = ["10", "python", "20"]\nfor text in texts:\n try:\n print(int(text) * 2)\n except ValueError:\n print(f"skip {text!r}")\n```\n\n```output\n20\nskip \'python\'\n40\n```\n:::\n\n:::note\n- `int()` and `float()` are constructors that also parse strings.\n- `int(text, base)` makes non-decimal input explicit.\n- Catch `ValueError` for recoverable user input; do not hide unexpected data corruption.\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(ratio * 2)\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 = "Names keep objects reachable until the last reference goes away."\ndoc_path = "/reference/datamodel.html#objects-values-and-types"\nsee_also = [\n "variables",\n "mutability",\n "classes",\n]\n+++\n\nPython objects live independently from the names that refer to them. Assignment adds another reference to an object; rebinding a name points that name somewhere else; `del` removes a name. The object can be reclaimed only after it is no longer reachable.\n\nMost programs do not manually destroy objects. They control lifetime by controlling which containers, local variables, and object attributes still hold references.\n\nThis example uses a small class so the object has visible state. The important evidence is that deleting one name does not destroy the object while another name still refers to it.\n\n:::program\n```python\nclass Box:\n def __init__(self, label):\n self.label = label\n\nbox = Box("draft")\nalias = box\n\nprint(box is alias)\nprint(alias.label)\n\nbox = Box("published")\nprint(alias.label)\nprint(box.label)\n\ndel alias\nprint("old object unreachable")\n```\n:::\n\n:::cell\nTwo names can refer to the same object. Mutating through one name would affect the object seen through the other.\n\n```python\nclass Box:\n def __init__(self, label):\n self.label = label\n\nbox = Box("draft")\nalias = box\n\nprint(box is alias)\nprint(alias.label)\n```\n\n```output\nTrue\ndraft\n```\n:::\n\n:::cell\nRebinding `box` does not change the original object. `alias` still reaches the first `Box` until that reference is removed too.\n\n```python\nbox = Box("published")\nprint(alias.label)\nprint(box.label)\n\ndel alias\nprint("old object unreachable")\n```\n\n```output\ndraft\npublished\nold object unreachable\n```\n:::\n\n:::note\n- Assignment binds names to objects; it does not copy the object.\n- `del name` removes one reference, not necessarily the object itself.\n- Python reclaims unreachable objects automatically, so lifetime bugs usually come from keeping references longer than intended.\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 if not isinstance(other, Vector):\n return NotImplemented\n return Vector(self.x + other.x, self.y + other.y)\n\n def __eq__(self, other):\n if not isinstance(other, Vector):\n return NotImplemented\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))\nprint(Vector(1, 1) == 5)\n```\n:::\n\n:::cell\n`__add__` defines how the `+` operator combines two objects. Checking the operand type and returning `NotImplemented` for foreign types lets Python try the other operand\'s reflected method instead of crashing inside yours.\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 if not isinstance(other, Vector):\n return NotImplemented\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. Returning `NotImplemented` for foreign types matters most here: equality against an unrelated value should answer `False`, never raise — Python falls back to identity when both sides decline.\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 if not isinstance(other, Vector):\n return NotImplemented\n return (self.x, self.y) == (other.x, other.y)\n\nprint(Vector(1, 1) == Vector(1, 1))\nprint(Vector(1, 1) == 5)\n```\n\n```output\nTrue\nFalse\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 if not isinstance(other, Vector):\n return NotImplemented\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\ndef loud():\n print("ran")\n return True\n\nprint(False and loud())\nprint(True or loud())\nprint(True and loud())\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"\nsee_also = [\n "type-hints",\n "union-and-optional-types",\n "generics-and-typevar",\n]\n+++\n\n`@overload` lets type checkers describe a function whose return type depends on the argument types. The overload declarations are static-only promises; the runtime function is still the single implementation that appears after them.\n\nUse overloads when a union return type would be too vague for callers. For example, `double(4)` returns an `int`, while `double("ha")` returns a `str`; `int | str` loses that relationship.\n\nAt runtime the overload stubs are not dispatch cases. The implementation must inspect or operate on the value just like any other Python function.\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: int | str) -> int | str:\n return value * 2\n\nprint(double(4))\nprint(double("ha"))\nprint(double.__annotations__)\n```\n:::\n\n:::cell\nThe overload stubs give static tools precise call shapes: integer in, integer out; string in, string out.\n\n```python\nfrom typing import overload\n\n@overload\ndef double(value: int) -> int: ...\n\n@overload\ndef double(value: str) -> str: ...\n\nprint("static signatures only")\n```\n\n```output\nstatic signatures only\n```\n:::\n\n:::cell\nThere is still one runtime implementation. It must accept every shape promised by the overloads.\n\n```python\ndef double(value: int | str) -> int | str:\n return value * 2\n\nprint(double(4))\nprint(double("ha"))\n```\n\n```output\n8\nhaha\n```\n:::\n\n:::cell\nOnly the implementation\'s annotations are visible on the runtime function. The overload declarations were for the type checker.\n\n```python\nprint(double.__annotations__)\n```\n\n```output\n{\'value\': int | str, \'return\': int | str}\n```\n:::\n\n:::note\n- Put `@overload` declarations immediately before the implementation.\n- Overloads improve static precision; they do not create runtime dispatch.\n- If all callers can work with one broad return type, a simple union annotation is usually enough.\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"\nsee_also = [\n "callable-types",\n "decorators",\n "generics-and-typevar",\n]\n+++\n\n`ParamSpec` is for decorators and wrapper functions that should keep the wrapped callable\'s parameter shape. Without it, a generic decorator often falls back to `Callable[..., R]`, which says “this returns the right type, but I no longer know what arguments are valid.”\n\nUse `ParamSpec` when the wrapper forwards `*args` and `**kwargs` to the original function without changing the signature. Use a plain `Callable` when the wrapper deliberately accepts a different set of parameters.\n\n`P.args` and `P.kwargs` annotate the wrapper\'s forwarded arguments. A separate `TypeVar` keeps the return type tied to the wrapped function\'s return type.\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 erased(func: Callable[..., R]) -> Callable[..., R]:\n return func\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(erased(add)(2, 3))\nprint(add(2, 3))\n```\n:::\n\n:::cell\n`Callable[..., R]` is sometimes too broad. It preserves the return type, but the ellipsis means the callable accepts any argument list as far as the type checker can tell.\n\n```python\nfrom collections.abc import Callable\nfrom typing import ParamSpec, TypeVar\n\nR = TypeVar("R")\n\n\ndef erased(func: Callable[..., R]) -> Callable[..., R]:\n return func\n\nprint(erased.__name__)\n```\n\n```output\nerased\n```\n:::\n\n:::cell\n`ParamSpec` captures the original parameters and lets the wrapper forward exactly that shape.\n\n```python\nP = ParamSpec("P")\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\nprint(logged.__name__)\n```\n\n```output\nlogged\n```\n:::\n\n:::cell\nThe decorated function still runs normally. The benefit is static: tools can keep checking that `add` receives two integers.\n\n```python\n@logged\ndef add(left: int, right: int) -> int:\n return left + right\n\nprint(erased(add)(2, 3))\nprint(add(2, 3))\n```\n\n```output\ncalling add\n5\ncalling add\n5\n```\n:::\n\n:::note\n- `ParamSpec` preserves a callable\'s parameter list through transparent wrappers.\n- Pair `ParamSpec` with a `TypeVar` when the return type should also be preserved.\n- Python 3.12+ also accepts the inline PEP 695 spelling `def wrap[**P, R](func: Callable[P, R])`, which declares both variables in the signature itself.\n- If the wrapper changes the public signature, write that new signature directly instead.\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"\nsee_also = [\n "functions",\n "args-and-kwargs",\n "callable-objects",\n]\n+++\n\n`functools.partial` turns a general callable into a more specific callable by remembering some positional or keyword arguments. It is useful when another API wants a one-argument callback but your underlying function needs more context.\n\nA partial object is still callable. It keeps the original function in `.func`, pre-filled positional arguments in `.args`, and pre-filled keyword arguments in `.keywords`.\n\nPrefer a named wrapper function when the adapted behavior needs branching, validation, or a docstring. Use `partial` when the adaptation is simply "call this function with these arguments already supplied."\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(apply_tax(0.2, 50))\nprint(vat(50))\nprint(service_tax(amount=80))\nprint(vat.func.__name__)\nprint(vat.args)\n```\n:::\n\n:::cell\nWithout `partial`, callers repeat the same fixed argument every time they want the specialized behavior.\n\n```python\ndef apply_tax(rate, amount):\n return round(amount * (1 + rate), 2)\n\nprint(apply_tax(0.2, 50))\n```\n\n```output\n60.0\n```\n:::\n\n:::cell\n`partial` stores that fixed argument and returns a callable shaped for the remaining arguments.\n\n```python\nfrom functools import partial\n\nvat = partial(apply_tax, 0.2)\nservice_tax = partial(apply_tax, rate=0.1)\n\nprint(vat(50))\nprint(service_tax(amount=80))\n```\n\n```output\n60.0\n88.0\n```\n:::\n\n:::cell\nPartial objects expose the function and stored arguments, which is helpful when debugging callback wiring.\n\n```python\nprint(vat.func.__name__)\nprint(vat.args)\n```\n\n```output\napply_tax\n(0.2,)\n```\n:::\n\n:::note\n- `partial` adapts a callable by pre-filling arguments.\n- The resulting object can be passed anywhere a callable with the remaining parameters is expected.\n- Use a regular function when the adapter needs more logic than argument binding.\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, factor=3, clamp=True))\n\ntry:\n scale(value=4)\nexcept TypeError as error:\n print(type(error).__name__)\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 — here `4 * 3` would be `12`, and the clamp visibly caps the result at `10`.\n\n```python\nprint(scale(4, factor=3, clamp=True))\n```\n\n```output\n10\n```\n:::\n\n:::cell\nThe restriction is enforced, not advisory: passing the positional-only `value` by keyword raises `TypeError` at the call site.\n\n```python\ntry:\n scale(value=4)\nexcept TypeError as error:\n print(type(error).__name__)\n```\n\n```output\nTypeError\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"\nsee_also = [\n "classes",\n "attribute-access",\n "descriptors",\n "dataclasses",\n]\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"\nsee_also = [\n "functions",\n "conditionals",\n "generators",\n]\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 and gives the pattern a name. The `re` module also caches recently compiled patterns internally, so the practical wins are readability and a place to attach flags more than raw speed.\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"\nsee_also = [\n "iterators",\n "while-loops",\n "break-and-continue",\n]\n+++\n\n`iter(callable, sentinel)` calls a zero-argument callable over and over. It yields each result until the callable returns the sentinel value, and the sentinel itself is not yielded.\n\nThis shape is useful for repeated reads: file blocks until `b""`, socket chunks until an empty response, queue items until a stop marker. It removes the common `while True` plus `break` scaffolding when the loop body is otherwise just "read, then process".\n\nThe callable must take no arguments. Wrap a parameterized reader in a `lambda`, `functools.partial`, or object method when the underlying API needs parameters.\n\n:::program\n```python\nchunks = iter(["py", "thon", ""])\n\n\ndef read_chunk():\n return next(chunks)\n\nprint(list(iter(read_chunk, "")))\n\nchunks = iter(["py", "thon", ""])\nword = ""\nwhile True:\n chunk = next(chunks)\n if chunk == "":\n break\n word += chunk\nprint(word)\n```\n:::\n\n:::cell\nThe two-argument form turns a polling callable into an iterator. The empty string stops the loop without appearing in the result.\n\n```python\nchunks = iter(["py", "thon", ""])\n\n\ndef read_chunk():\n return next(chunks)\n\nprint(list(iter(read_chunk, "")))\n```\n\n```output\n[\'py\', \'thon\']\n```\n:::\n\n:::cell\nThe equivalent manual loop needs an explicit read, comparison, and `break`. Use this shape when the stop condition is more complicated than a single sentinel value.\n\n```python\nchunks = iter(["py", "thon", ""])\nword = ""\nwhile True:\n chunk = next(chunks)\n if chunk == "":\n break\n word += chunk\nprint(word)\n```\n\n```output\npython\n```\n:::\n\n:::note\n- The callable passed to `iter(callable, sentinel)` must take no arguments.\n- The sentinel stops iteration and is not yielded.\n- When the loop needs richer branching, an explicit `while` loop may be clearer.\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"\nsee_also = [\n "lists",\n "dicts",\n "comprehensions",\n]\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 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"\nsee_also = [\n "lists",\n "tuples",\n "strings",\n]\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"\nsee_also = [\n "lists",\n "lambdas",\n "functions",\n]\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 — but only for types treated as immutable: this `Bag` hashes its current items, so mutating one after adding it to a set makes it unfindable. `__lt__` alone is enough for `<` and for `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](/examples/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](/examples/callable-objects) and [context-managers](/examples/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"\nsee_also = [\n "strings",\n "logging",\n "csv-data",\n "values",\n]\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 `05.1f` zero-pads the score to a width of five characters with 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"\nsee_also = [\n "values",\n "string-formatting",\n "bytes-and-bytearray",\n "regular-expressions",\n]\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"\nsee_also = [\n "virtual-environments",\n "networking",\n "threads-and-processes",\n]\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\nIn standard Python this is the right tool for calling Git, compilers, shells, or another Python interpreter. This site\'s live example runner does not expose an operating-system process table, so the page teaches the proper `subprocess.run()` contract and labels the runner boundary instead of pretending the command can run here.\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 important boundary is between Python objects and the operating system: Python prepares arguments and environment, then the child program 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. (The in-browser Run button cannot spawn child processes, so pressing Run on this page fails in the sandbox; the verified output below comes from standard CPython at build time.)\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- The output shown here was produced by really spawning the child under standard CPython when the example was verified; the site\'s in-browser sandbox cannot create processes, so live runs of this page fail there.\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"\nsee_also = [\n "async-await",\n "subprocesses",\n "networking",\n]\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\nIn standard Python, `ThreadPoolExecutor` and `ProcessPoolExecutor` are the ordinary tools for this lesson. This site\'s live example runner does not expose native threads or child processes, so this page keeps the proper executor model visible and separates the standard Python idea from what can execute here.\n\nThis is different from `asyncio`: threads and processes run ordinary callables through executors, while `async` code cooperatively awaits coroutines. Choose the smallest concurrency model that matches the bottleneck.\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. (The in-browser Run button cannot create native threads or child processes, so pressing Run on this page fails in the sandbox; the verified thread-pool output below comes from standard CPython at build time.)\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- The thread-pool output here was produced by real worker threads under standard CPython when the example was verified; the site\'s in-browser sandbox cannot create threads or processes, so live runs of this page fail there.\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"\nsee_also = [\n "booleans",\n "none",\n "conditionals",\n "special-methods",\n]\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\nAn empty list is false, so `not items` reads as "items is empty". The condition tests the object directly — no `len(items) == 0` comparison is needed.\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\nA non-empty string is true, so `if name:` asks "did we get a name?" in one word. Reach for an explicit comparison instead when the distinction matters — `if name is not None:` treats an empty string differently from a missing one.\n\n```python\nif name:\n print("has a name")\n```\n\n```output\nhas a name\n```\n:::\n\n:::cell\n`bool()` reveals the truth value any condition would use. Zero-like numbers convert to `False`; other numbers convert to `True`.\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\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\ntype Score = 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\nThe `type` statement names a type so it can be reused with intent. `type Score = int` keeps the underlying type at runtime but lets the API talk about a domain concept rather than a primitive. Older code spells this `Score: TypeAlias = int`; `typing.TypeAlias` is deprecated since Python 3.12, and the type-aliases page covers the modern statement in depth.\n\n```python\ntype Score = 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 a `type` alias 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"\nsee_also = [\n "tuples",\n "multiple-return-values",\n "args-and-kwargs",\n "dicts",\n]\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\nTuple unpacking assigns each position to a name in one statement: `x` receives the first element of `point` and `y` the second. The assignment fails loudly if the number of names and elements disagree, which catches shape mistakes early.\n\n```python\npoint = (3, 4)\nx, y = point\nprint(x, y)\n```\n\n```output\n3 4\n```\n:::\n\n:::cell\nThe starred name collects however many elements the head and tail don\'t claim — here `first` and `last` take the ends and `*middle` gathers the rest into a list. The same list works whether it has four elements or forty.\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\n`describe(**data)` spreads the dictionary\'s keys as keyword arguments, so the call site never repeats `name=` and `language=` by hand. This is the bridge between dict-shaped data (configuration, parsed JSON) and function signatures.\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"\nsee_also = [\n "variables",\n "booleans",\n "none",\n "literals",\n]\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"\nsee_also = [\n "values",\n "mutability",\n "object-lifecycle",\n "constants",\n]\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"\nsee_also = [\n "packages",\n "modules",\n "import-aliases",\n]\nexpected_output = ".venv\\nTrue\\n"\n+++\n\nVirtual environments isolate a project\'s installed packages from the global Python installation and from other projects. The usual workflow is a command-line one: create `.venv`, activate it, then install project dependencies there.\n\nIn standard Python, `python -m venv .venv` is the everyday command. This site\'s live example runner is built from declared dependencies rather than an activated shell environment, so the runnable part keeps to deterministic evidence while the page still teaches the standard-Python workflow.\n\nA virtual environment changes installation and import paths. It does not change the Python language, package layout rules, or 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 config = (env_path / "pyvenv.cfg").read_text()\n print(env_path.name)\n print("home" in config)\n```\n:::\n\n:::unsupported\nThe standard project setup command is `python -m venv .venv`. It creates a directory with its own interpreter entry points and package install location. After activation, `python -m pip install ...` installs into that environment rather than into another project. (This workflow is for standard Python projects. The Python By Example runner is deployed from declared dependencies instead of an activated shell environment.)\n\n```python\nimport subprocess\nimport sys\n\nsubprocess.run([sys.executable, "-m", "venv", ".venv"], check=True)\nsubprocess.run([".venv/bin/python", "-m", "pip", "install", "requests"], check=True)\n```\n:::\n\n:::cell\n`venv.EnvBuilder` exposes the same environment-creation mechanism 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 config = (env_path / "pyvenv.cfg").read_text()\n print(env_path.name)\n print("home" in config)\n```\n\n```output\n.venv\nTrue\n```\n:::\n\n:::note\n- Use `python -m venv .venv` for everyday standard-Python project setup.\n- A venv isolates installed packages; it does not change how imports are written.\n- This site\'s runner uses a deployment dependency model, not an activated shell environment.\n- That runner constraint is separate from the standard Python `venv` workflow you would use in local projects.\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"\nsee_also = [\n "exceptions",\n "logging",\n "testing",\n]\n+++\n\nA warning reports a problem that callers should know about, but it does not have to stop the current operation. Deprecations are the classic case: the old API can still return a value while telling users to migrate.\n\nWarnings sit between logging and exceptions. Logging records operational evidence; exceptions stop the current path; warnings make compatibility or correctness concerns visible according to a filter.\n\nTests often capture warnings so deprecations are asserted instead of merely printed. Filters can also turn warnings into errors when a project wants to enforce cleanup.\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\nwith warnings.catch_warnings(record=True) as caught:\n warnings.simplefilter("always", DeprecationWarning)\n print(old_name())\n print(caught[0].category.__name__)\n print(str(caught[0].message))\n\nwith warnings.catch_warnings():\n warnings.simplefilter("error", DeprecationWarning)\n try:\n old_name()\n except DeprecationWarning:\n print("warning became error")\n```\n:::\n\n:::cell\nCapture warnings in tests when the returned value still matters but the migration notice must be asserted.\n\n```python\nimport warnings\n\n\ndef old_name():\n warnings.warn("old_name is deprecated", DeprecationWarning, stacklevel=2)\n return "result"\n\nwith warnings.catch_warnings(record=True) as caught:\n warnings.simplefilter("always", DeprecationWarning)\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:::cell\nA filter can promote selected warnings to exceptions, which is useful in CI when deprecated calls should fail the build.\n\n```python\nwith warnings.catch_warnings():\n warnings.simplefilter("error", DeprecationWarning)\n try:\n old_name()\n except DeprecationWarning:\n print("warning became error")\n```\n\n```output\nwarning became error\n```\n:::\n\n:::note\n- Use warnings for soft problems callers can act on later.\n- Use exceptions when the current operation cannot continue.\n- `stacklevel` should point the warning at the caller rather than inside the helper.\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"\nsee_also = [\n "for-loops",\n "sentinel-iteration",\n "break-and-continue",\n]\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/main.py b/src/main.py index d990746..6099557 100644 --- a/src/main.py +++ b/src/main.py @@ -11,9 +11,24 @@ from starlette.exceptions import HTTPException as StarletteHTTPException from workers import WorkerEntrypoint, python_from_rpc -from app import FAVICON_SVG, build_dynamic_worker_code, get_example, render_example_page, route +from app import ( + FAVICON_SVG, + JOURNEYS_BY_SLUG, + build_dynamic_worker_code, + get_example, + render_cell_output_flow_option, + render_example_not_found, + render_example_page, + render_home, + render_journey_not_found, + render_journey_page, + render_journeys_index, + render_mobile_run_first_option, + render_not_found, +) from asset_manifest import HTML_CACHE_VERSION from examples import PYTHON_VERSION +from security import CONTENT_SECURITY_POLICY, STRICT_TRANSPORT_SECURITY import observability import worker_asgi_bridge as asgi @@ -22,6 +37,10 @@ SMOKE_BYPASS_HEADER = "x-pythonbyexample-smoke-secret" TURNSTILE_CLEARANCE_COOKIE = "pbe_turnstile_clearance" DEFAULT_TURNSTILE_CLEARANCE_SECONDS = 60 * 60 * 8 +# Generous ceiling for an edited example program (the largest curated +# program is well under 4 KB). Bounds the bytes we hash, embed in a +# Dynamic Worker module, and echo back into the page. +MAX_SUBMITTED_BODY_BYTES = 100_000 try: from js import Object, Request as JsRequest, caches, fetch as js_fetch @@ -79,13 +98,38 @@ def should_cache_get_url(url: str) -> bool: return not path.startswith("/layout-options/") +# Static assets get the same set from public/_headers; Worker-rendered +# HTML needs them applied here. frame-ancestors 'none' (plus the legacy +# X-Frame-Options) keeps the Run button out of clickjacking frames. +SECURITY_HEADERS = { + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Frame-Options": "DENY", + "Strict-Transport-Security": STRICT_TRANSPORT_SECURITY, + "Content-Security-Policy": CONTENT_SECURITY_POLICY, +} + + +def _apply_security_headers(response) -> None: + headers = getattr(response, "headers", None) + if headers is None: + return + for name, value in SECURITY_HEADERS.items(): + headers.set(name, value) + + def html_cache_key_url(url: str, turnstile_site_key: str = "") -> str: - separator = "&" if "?" in url else "?" + parsed = urlparse(url) + # Routes ignore ordinary query strings, so the Worker cache key must ignore + # them too. Otherwise ?utm=... or arbitrary cache-busting params would create + # unbounded copies of identical HTML. Only the generated HTML version and the + # Turnstile site-key fragment are meaningful cache dimensions. + base_url = parsed._replace(path=parsed.path or "/", params="", query="", fragment="").geturl() turnstile_fragment = "" if turnstile_site_key: digest = hashlib.sha256(turnstile_site_key.encode("utf-8")).hexdigest()[:8] turnstile_fragment = f"&__turnstile={digest}" - return f"{url}{separator}__html_v={HTML_CACHE_VERSION}{turnstile_fragment}" + return f"{base_url}?__html_v={HTML_CACHE_VERSION}{turnstile_fragment}" @app.get("/favicon.svg") @@ -94,9 +138,8 @@ async def favicon(): @app.get("/", response_class=HTMLResponse) -async def home(request: Request): - response = route(str(request.url), method="GET") - return _html(response.body, response.status) +async def home(): + return _html(render_home()) def _env_text(request: Request, name: str) -> str: @@ -131,7 +174,8 @@ def _turnstile_clearance_seconds(request: Request) -> int: def _smoke_bypass_ok(request: Request) -> bool: bypass_secret = _env_text(request, "PBE_SMOKE_BYPASS_SECRET") - return bool(bypass_secret and request.headers.get(SMOKE_BYPASS_HEADER) == bypass_secret) + supplied = request.headers.get(SMOKE_BYPASS_HEADER, "") + return bool(bypass_secret) and hmac.compare_digest(supplied, bypass_secret) def _sign_clearance(secret: str, expires: int) -> str: @@ -160,9 +204,11 @@ def _requires_turnstile(request: Request) -> bool: if not _turnstile_secret(request) or _smoke_bypass_ok(request): return False mode = _turnstile_challenge_mode(request) - if mode in {"session", "once", "once-per-session"}: - return not _clearance_valid(request) - return False + if mode == "off": + return False + # Any other value — including a typo of "session" — fails closed and + # requires the challenge rather than silently disabling protection. + return not _clearance_valid(request) def _set_turnstile_clearance(response: HTMLResponse, request: Request) -> None: @@ -182,10 +228,35 @@ def _set_turnstile_clearance(response: HTMLResponse, request: Request) -> None: ) +@app.get("/layout-options/mobile-run-first", response_class=HTMLResponse) +async def mobile_run_first_option(): + return _html(render_mobile_run_first_option(get_example("values"))) + + +@app.get("/layout-options/cell-output-flow", response_class=HTMLResponse) +async def cell_output_flow_option(): + return _html(render_cell_output_flow_option(get_example("values"))) + + +@app.get("/journeys", response_class=HTMLResponse) +async def journeys_index(): + return _html(render_journeys_index()) + + +@app.get("/journeys/{slug}", response_class=HTMLResponse) +async def journey_page(slug: str): + journey = JOURNEYS_BY_SLUG.get(slug) + if journey is None: + return _html(render_journey_not_found(), 404) + return _html(render_journey_page(journey)) + + @app.get("/examples/{slug}", response_class=HTMLResponse) async def example_page(slug: str, request: Request): - response = route(str(request.url), method="GET", turnstile_site_key=_turnstile_site_key(request)) - return _html(response.body, response.status) + example = get_example(slug) + if example is None: + return _html(render_example_not_found(slug), 404) + return _html(render_example_page(example, turnstile_site_key=_turnstile_site_key(request))) @app.post("/examples/{slug}", response_class=HTMLResponse) @@ -193,7 +264,19 @@ async def run_example(slug: str, request: Request): example = get_example(slug) if example is None: return _html("

    Example not found

    ", 404) - body = (await request.body()).decode("utf-8") + raw_body = await request.body() + if len(raw_body) > MAX_SUBMITTED_BODY_BYTES: + if event := _wide_event(request): + event["example"] = {"slug": example["slug"], "code_bytes": len(raw_body), "rejected": "too_large"} + return _html( + render_example_page( + example, + output=f"Submitted code is too large (over {MAX_SUBMITTED_BODY_BYTES // 1000} kB). Trim it and run again.", + turnstile_site_key=_turnstile_site_key(request), + ), + 413, + ) + body = raw_body.decode("utf-8") form = parse_qs(body) submitted = form.get("code", [example["code"]])[0] submitted_bytes = submitted.encode("utf-8") @@ -388,9 +471,8 @@ def provide_worker_code(): @app.get("/{path:path}") -async def not_found(path: str, request: Request): - response = route(str(request.url), method="GET") - return _html(response.body, response.status) +async def not_found(path: str): + return _html(render_not_found(), 404) class Default(WorkerEntrypoint): @@ -424,13 +506,18 @@ async def fetch(self, request): asgi_request, self.env, state={"wide_event": event}, + max_body_bytes=MAX_SUBMITTED_BODY_BYTES, ) + _apply_security_headers(response) if getattr(response, "status", 200) == 200: response.headers.set( "Cache-Control", "public, max-age=300, stale-while-revalidate=86400", ) await caches.default.put(cache_key, response.clone()) + else: + # Error pages must not linger in browser heuristic caches. + response.headers.set("Cache-Control", "no-store") return response event["cache"] = "bypass" response = await asgi.fetch( @@ -438,17 +525,22 @@ async def fetch(self, request): asgi_request, self.env, state={"wide_event": event}, + max_body_bytes=MAX_SUBMITTED_BODY_BYTES, ) + _apply_security_headers(response) response.headers.set("Cache-Control", "no-store") return response event["cache"] = "bypass" - return await asgi.fetch( + response = await asgi.fetch( app, asgi_request, self.env, state={"wide_event": event}, + max_body_bytes=MAX_SUBMITTED_BODY_BYTES, ) + _apply_security_headers(response) + return response except Exception as exc: event["status_code"] = 500 event["outcome"] = "error" diff --git a/src/marginalia.py b/src/marginalia.py index 07e2cb8..9af49bf 100644 --- a/src/marginalia.py +++ b/src/marginalia.py @@ -20,13 +20,28 @@ from __future__ import annotations -import html from typing import Callable try: + from .editorial_registry import ( + example_figure_scores as load_example_figure_scores, + example_quality_scores as load_example_quality_scores, + figure_attachments as load_figure_attachments, + journey_section_figure_scores as load_journey_section_figure_scores, + journey_section_figures as load_journey_section_figures, + ) from .marginalia_grammar import Canvas + from .textfmt import render_inline except ImportError: # Cloudflare Workers import siblings without the package prefix. + from editorial_registry import ( # type: ignore[no-redef] + example_figure_scores as load_example_figure_scores, + example_quality_scores as load_example_quality_scores, + figure_attachments as load_figure_attachments, + journey_section_figure_scores as load_journey_section_figure_scores, + journey_section_figures as load_journey_section_figures, + ) from marginalia_grammar import Canvas + from textfmt import render_inline # ─── Named figures ───────────────────────────────────────────────────── @@ -125,6 +140,20 @@ def loop_repetition(c: Canvas) -> None: c.cell(106, y, stop, w=150, h=22, soft=(loop == "sentinel")) +def while_backedge(c: Canvas) -> None: + """while-loops · the back-edge that returns control to the test each pass.""" + c.cell(64, 4, "while test:", w=104, h=22) + c.closed_arrow(116, 26, 116, 46, emphasis=False) + c.label(124, 40, "true") + c.cell(64, 48, "body", w=104, h=22) + c.stroke(168, 59, 204, 59) + c.stroke(204, 59, 204, 15) + c.closed_arrow(204, 15, 170, 15, emphasis=True) + c.label(212, 40, "repeat") + c.closed_arrow(64, 15, 30, 15, emphasis=False) + c.label(0, 9, "false") + + 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) @@ -834,12 +863,12 @@ def partial_functions(c: Canvas) -> None: 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") + # *args spans signature indices 6-10 (center char 8); **kwargs spans + # 13-20 (center falls on the 16/17 boundary, index 16.5). + args_x = c.mono_divider(20, 8, 26, 44) + kwargs_x = c.mono_divider(20, 16.5, 26, 44) + c.label(args_x, 56, "→ tuple", anchor="middle") + c.label(kwargs_x, 56, "→ dict", anchor="middle") def multiple_return(c: Canvas) -> None: @@ -1403,6 +1432,7 @@ def structured_shapes(c: Canvas) -> None: "slice-ruler": (slice_ruler, 232, 120), "branch-fork": (branch_fork, 232, 100), "loop-repetition": (loop_repetition, 260, 100), + "while-backedge": (while_backedge, 252, 80), "iter-protocol": (iter_protocol, 304, 70), # Runtime "program-output": (program_output, 240, 80), @@ -1535,490 +1565,8 @@ def structured_shapes(c: Canvas) -> None: # ─── 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-0", "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-1", "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-1", "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", "container-methods", - "Container syntax routes to narrow protocol methods: assignment to __setitem__, membership to __contains__, lookup to __getitem__.", - )], - "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 static call signatures for double(int) and double(str); one runtime implementation handles both.", - )], - "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", - "A Callable annotation describes the callback slot: this argument list must produce this 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", "structured-shapes", - "The same record can be a mutable dataclass, an immutable NamedTuple, or a runtime dict described by TypedDict.", - )], - "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", - "Names keep the same object reachable; deleting one name matters only when it removes the last reference.", - )], -} +# Loaded from docs/quality-registries.toml: slug -> [(anchor, figure_name, caption_or_None), …] +ATTACHMENTS: dict[str, list[tuple[str, str, str | None]]] = load_figure_attachments() # ─── Render helpers ──────────────────────────────────────────────────── @@ -2045,7 +1593,7 @@ def render_for_anchor(slug: str, anchor: str) -> str: return "" figures: list[str] = [] for name, caption in matched: - cap = f"
    {html.escape(caption)}
    " if caption else "" + cap = f"
    {render_inline(caption)}
    " if caption else "" figures.append(f"
    {_render_svg(name)}{cap}
    ") count_class = f" cell-banner--{len(matched)}" return f'
    {"".join(figures)}
    ' @@ -2059,99 +1607,7 @@ def render_for_anchor(slug: str, anchor: str) -> str: # 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.": ( - "runtime-evidence-loop", - "Examples are evidence loops: source, a run step, and visible output stay together.", - ), - "Separate value, identity, and absence.": ( - "runtime-object-axes", - "Runtime objects answer separate questions: equal value, same identity, or the singleton that marks absence.", - ), - "Read expressions as object operations.": ( - "runtime-expression-model", - "Expression syntax enters the data model; object methods produce the result.", - ), - # Control Flow - "Choose between paths.": ( - "control-decision-map", - "Facts flow into one decision point; exactly one branch owns the next step.", - ), - "Name and shape decisions.": ( - "control-fact-shape", - "Name a fact when a condition needs it; match shape when the data structure is the decision.", - ), - "Stop as soon as the answer is known.": ( - "control-stop-boundary", - "Early exits draw a boundary: once the answer is found, the tail stays unread.", - ), - # Iteration - "Choose the right loop shape.": ( - "iteration-loop-selector", - "Choose the loop from its stopping rule: exhaustion, condition, or sentinel marker.", - ), - "See the protocol behind `for`.": ( - "iteration-protocol-map", - "for is surface syntax; iter() creates an iterator and next() pulls values until StopIteration.", - ), - "Compose lazy value streams.": ( - "iteration-lazy-pull", - "Lazy pipelines run from the consumer's pull: next() requests one value through each stage.", - ), - # 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.": ( - "type-runtime-static-split", - "Runtime values run the program; static tools inspect separate annotations and report before execution.", - ), - "Describe realistic data shapes.": ( - "type-shape-catalog", - "Real data contracts combine fields, variants, and expected absence instead of one scalar type.", - ), - "Scale annotations for reusable libraries.": ( - "type-library-contract", - "Reusable APIs carry caller contracts through the library boundary with generics, parameters, and overloads.", - ), - # Reliability - "Make failure explicit.": ( - "reliability-signal-map", - "Different failure shapes need explicit signals: assertions, recovery, chained causes, or warnings.", - ), - "Control resource and module boundaries.": ( - "reliability-boundary-map", - "Reliable programs name their boundaries: resources clean up, modules import, environments constrain runtime.", - ), - "Handle operations that outlive one expression.": ( - "reliability-operation-boundary", - "Async, threaded, test, and logging work cross an operation boundary before evidence comes back.", - ), -} +SECTION_FIGURES: dict[str, tuple[str, str]] = load_journey_section_figures() def render_for_section(section_title: str) -> str: @@ -2162,7 +1618,7 @@ def render_for_section(section_title: str) -> str: if not entry: return "" name, caption = entry - cap = f"
    {html.escape(caption)}
    " if caption else "" + cap = f"
    {render_inline(caption)}
    " if caption else "" return f'
    {_render_svg(name)}{cap}
    ' @@ -2170,157 +1626,16 @@ def render_for_section(section_title: str) -> str: # Score every journey-section figure against # docs/journey-visualisation-rubric.md (10-point scale). # Keyed by section title to match SECTION_FIGURES. -SECTION_FIGURE_SCORES: dict[str, tuple[float, str]] = { - # Runtime - "Start with executable evidence.": (9.0, "journey-native source/run/output evidence loop"), - "Separate value, identity, and absence.": (9.0, "three runtime questions separated"), - "Read expressions as object operations.": (9.0, "syntax enters the data-model method surface"), - # Control Flow - "Choose between paths.": (9.0, "branch map independent of one if-statement example"), - "Name and shape decisions.": (9.0, "name-vs-shape decision map"), - "Stop as soon as the answer is known.": (9.0, "early boundary leaves tail unread"), - # Iteration - "Choose the right loop shape.": (9.0, "loop choice by stopping rule"), - "See the protocol behind `for`.": (9.5, "for surface mapped to iter()/next()/StopIteration"), - "Compose lazy value streams.": (9.0, "consumer pull drives the lazy pipeline"), - # Shapes - "Pick the container that matches the question.": (9.0, "list/tuple/dict/set per question"), - "Move between shapes deliberately.": (9.0, "input → transform → result"), - "Cross text and data boundaries.": (9.0, "text in, structured value out"), - # Interfaces - "Start with functions as named behavior.": (9.0, "concrete call, named body, and return boundary"), - "Use functions as values.": (9.0, "second name binds same function"), - "Bundle behavior with state.": (9.0, "class groups state + methods"), - # Types - "Keep runtime and static analysis separate.": (9.0, "runtime track separated from static-tool track"), - "Describe realistic data shapes.": (9.0, "field, variant, and absence contracts in one map"), - "Scale annotations for reusable libraries.": (9.0, "caller contracts survive library API boundary"), - # Reliability - "Make failure explicit.": (9.0, "failure shapes mapped to explicit signals"), - "Control resource and module boundaries.": (9.0, "resource/module/env boundaries shown together"), - "Handle operations that outlive one expression.": (9.0, "operation boundary returns evidence later"), -} +SECTION_FIGURE_SCORES: dict[str, tuple[float, str]] = load_journey_section_figure_scores() # ─── 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 +# loaded from the TOML source of truth used by 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, "syntax routes to __setitem__, __contains__, __getitem__"), - "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, "dataclass vs NamedTuple vs TypedDict runtime trade-offs"), - "csv-data": (9.0, "rows × columns; same shape per line"), - "warnings": (9.0, "soft signal; execution continues"), - "object-lifecycle": (9.0, "names keep object reachable until last reference disappears"), - "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__"), - "overloads": (9.0, "static overload signatures contrasted with one runtime body"), - "callable-types": (9.0, "callback slot shows argument list and return type"), - "threads-and-processes": (9.0, "thread GIL sharing contrasted with process isolation"), -} +SCORES: dict[str, tuple[float, str]] = load_example_figure_scores() def figure_score(slug: str) -> tuple[float, str] | None: @@ -2336,114 +1651,4 @@ def figure_score(slug: str) -> tuple[float, str] | None: # point of the registry is to surface distribution and outliers, not # to pretend a script can grade pedagogy. -EXAMPLE_QUALITY_SCORES: dict[str, tuple[float, str]] = { - "hello-world": (7.1, "waived: traditional smallest complete program"), - "values": (9.0, "object/type/operation map with prerequisite graph edges"), - "literals": (9.0, "criterion-level pass: graph-rich, note-heavy, multi-cell"), - "numbers": (9.0, "graph-rich, note-heavy"), - "booleans": (9.0, "truth operations, short-circuiting, bool-as-int footgun"), - "operators": (9.0, "criterion-level pass: graph-rich, note-heavy, multi-cell"), - "none": (9.0, "None, mapping default, and exception absence boundaries"), - "variables": (9.0, "binding, rebinding, augmented-assignment, lifecycle graph"), - "constants": (9.0, "constant convention, Final boundary, runtime rebinding evidence"), - "truthiness": (9.0, "truth-value categories, explicit-comparison boundary, graph-linked"), - "equality-and-identity": (9.0, "== versus is, singleton check, identity-cache warning"), - "mutability": (9.0, "in-place mutation, rebinding, alias visibility"), - "object-lifecycle": (9.0, "references, rebinding, and last-reference boundary"), - "strings": (9.0, "Unicode text, immutability, encoding boundary"), - "bytes-and-bytearray": (9.0, "graph-rich, note-heavy"), - "string-formatting": (9.0, "f-string expression, format spec, debug/repr boundary"), - "conditionals": (9.0, "branch ordering, truthiness, and ternary boundary"), - "guard-clauses": (9.0, "nested contrast plus flattened guard path"), - "assignment-expressions": (9.0, "criterion-level pass: graph-rich"), - "for-loops": (9.0, "direct iteration, range, and enumerate progression"), - "break-and-continue": (9.0, "criterion-level pass: graph-rich"), - "loop-else": (9.0, "criterion-level pass: graph-rich"), - "iterating-over-iterables": (9.0, "criterion-level pass: graph-rich"), - "iterators": (9.0, "criterion-level pass: graph-rich"), - "iterator-vs-iterable": (9.0, "graph-rich"), - "sentinel-iteration": (9.0, "call-until-marker shape contrasted with manual while"), - "match-statements": (9.0, "shape dispatch and guarded pattern payoff"), - "advanced-match-patterns": (9.0, "criterion-level pass: graph-rich"), - "while-loops": (9.0, "state-driven repetition contrasted with for loops"), - "lists": (9.0, "ordered mutable sequence operations plus tuple/set/copy boundaries"), - "tuples": (9.0, "graph-rich, note-heavy"), - "unpacking": (9.0, "sequence, starred, and mapping unpacking shapes"), - "dicts": (9.0, "lookup/default/update/delete dictionary boundaries"), - "sets": (9.0, "uniqueness, membership, set algebra, ordering boundary"), - "slices": (9.0, "range bounds, step, reverse, and unchanged-source evidence"), - "comprehensions": (9.0, "loop-equivalent collection building with filter/projection boundaries"), - "comprehension-patterns": (9.0, "criterion-level pass: graph-rich"), - "sorting": (9.0, "key functions, reverse order, sorted/list.sort boundary"), - "collections-module": (9.0, "Counter, defaultdict, deque, namedtuple mapped to data shapes"), - "copying-collections": (9.0, "alias, shallow copy, and deepcopy boundaries"), - "functions": (9.0, "definition/call/return/default-boundary walkthrough"), - "keyword-only-arguments": (9.0, "call-site readability and boolean flag boundary"), - "positional-only-parameters": (9.0, "criterion-level pass: graph-rich"), - "args-and-kwargs": (9.0, "flexible argument collection plus forwarding/API-boundary graph"), - "multiple-return-values": (9.0, "tuple return plus unpacking boundary"), - "closures": (9.0, "closed-over state, factory payoff, late-binding boundary"), - "partial-functions": (9.0, "before/after callable adaptation and introspection"), - "scope-global-nonlocal": (9.0, "criterion-level pass: graph-rich"), - "recursion": (9.0, "base case and recursive tree-shape payoff"), - "lambdas": (9.0, "expression callable use contrasted with named def reuse"), - "generators": (9.0, "graph-rich, note-heavy"), - "yield-from": (9.0, "criterion-level pass: graph-rich"), - "generator-expressions": (9.0, "eager list contrast, lazy one-pass consumption, reducer use"), - "itertools": (9.0, "lazy standard-library pipeline and iterator-composition payoff"), - "decorators": (9.0, "criterion-level pass: graph-rich"), - "classes": (9.0, "graph-rich, note-heavy"), - "inheritance-and-super": (9.0, "criterion-level pass: graph-rich"), - "classmethods-and-staticmethods": (9.0, "graph-rich, note-heavy"), - "dataclasses": (9.0, "criterion-level pass: graph-rich"), - "properties": (9.0, "managed attribute API without caller syntax change"), - "special-methods": (9.0, "criterion-level pass: graph-rich, note-heavy, multi-cell"), - "truth-and-size": (9.0, "criterion-level pass: graph-rich"), - "container-protocols": (9.0, "criterion-level pass: graph-rich"), - "callable-objects": (9.0, "criterion-level pass: graph-rich"), - "operator-overloading": (9.0, "criterion-level pass: graph-rich"), - "attribute-access": (9.0, "criterion-level pass: graph-rich"), - "bound-and-unbound-methods": (9.0, "graph-rich, note-heavy"), - "descriptors": (9.0, "descriptor lookup, __set_name__, and validation mechanics"), - "metaclasses": (9.0, "criterion-level pass: graph-rich"), - "context-managers": (9.0, "criterion-level pass: graph-rich, note-heavy"), - "delete-statements": (9.0, "criterion-level pass: graph-rich"), - "exceptions": (9.0, "specific handling, else/finally cleanup, broad-catch boundary"), - "assertions": (9.0, "criterion-level pass: graph-rich"), - "exception-chaining": (9.0, "criterion-level pass: graph-rich"), - "exception-groups": (9.0, "criterion-level pass: graph-rich"), - "warnings": (9.0, "capture and escalate soft failures with filters"), - "modules": (9.0, "graph-rich, note-heavy"), - "import-aliases": (9.0, "criterion-level pass: graph-rich, note-heavy"), - "packages": (9.0, "graph-rich, note-heavy"), - "virtual-environments": (9.0, "standard venv workflow plus explicit Worker deployment boundary"), - "type-hints": (9.0, "criterion-level pass: graph-rich, note-heavy, multi-cell"), - "runtime-type-checks": (9.0, "criterion-level pass: graph-rich"), - "union-and-optional-types": (9.0, "criterion-level pass: graph-rich"), - "type-aliases": (9.0, "criterion-level pass: graph-rich"), - "typed-dicts": (9.0, "criterion-level pass: graph-rich"), - "structured-data-shapes": (9.0, "graph-rich, note-heavy"), - "literal-and-final": (9.0, "Literal value set, Final rebinding promise, runtime caveat"), - "callable-types": (9.0, "criterion-level pass: graph-rich"), - "generics-and-typevar": (9.0, "criterion-level pass: graph-rich"), - "paramspec": (9.0, "Callable erasure contrasted with ParamSpec-preserving decorator"), - "overloads": (9.0, "static overload stubs contrasted with one runtime implementation"), - "casts-and-any": (9.0, "criterion-level pass: graph-rich"), - "newtype": (9.0, "criterion-level pass: graph-rich"), - "protocols": (9.0, "criterion-level pass: graph-rich"), - "abstract-base-classes": (9.0, "graph-rich, note-heavy"), - "enums": (9.0, "closed symbolic choice set contrasted with raw strings"), - "regular-expressions": (9.0, "criterion-level pass: graph-rich, note-heavy, multi-cell"), - "number-parsing": (9.0, "decimal, explicit base, and recoverable ValueError boundary"), - "custom-exceptions": (9.0, "domain error naming, raise point, recovery boundary"), - "json": (9.0, "graph-rich, note-heavy"), - "logging": (9.0, "logger, handler, formatter, and threshold boundaries"), - "testing": (9.0, "criterion-level pass: graph-rich"), - "subprocesses": (9.0, "standard subprocess contract plus explicit Worker boundary"), - "threads-and-processes": (9.0, "thread/process executor contrast plus explicit Worker boundary"), - "networking": (9.0, "protocol byte boundary plus explicit socket runtime caveat"), - "datetime": (9.0, "date/time/delta parsing with timezone scope made explicit"), - "csv-data": (9.0, "DictReader, conversion, and DictWriter row boundary"), - "async-await": (9.0, "graph-rich, note-heavy"), - "async-iteration-and-context": (9.0, "criterion-level pass: graph-rich"), -} +EXAMPLE_QUALITY_SCORES: dict[str, tuple[float, str]] = load_example_quality_scores() diff --git a/src/marginalia_grammar.py b/src/marginalia_grammar.py index 3cbf272..6bef394 100644 --- a/src/marginalia_grammar.py +++ b/src/marginalia_grammar.py @@ -15,6 +15,14 @@ import math from dataclasses import dataclass, field from typing import Callable +from xml.sax.saxutils import escape as xml_escape + +# Padding emitted around every figure's registered canvas by +# Canvas.to_svg(); the geometry contracts import these so paint code and +# tests share one source of truth. +PAD_TOP = 14 +PAD_X = 14 +PAD_BOTTOM = 14 # ─── Palette ─────────────────────────────────────────────────────────── # Aligned with public/site.css design tokens. These are the only @@ -207,7 +215,7 @@ def _text(self, x, y, s, *, family, size, anchor, color, italic=False, tracking= attrs.append('font-style="italic"') if tracking: attrs.append(f'letter-spacing="{tracking}"') - self._add(f"{s}") + self._add(f"{xml_escape(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) @@ -462,7 +470,7 @@ def lanes(self, ys_labels, *, x0=40, x1=300, path=None): INTRINSIC_SCALE = 1.6 def to_svg(self) -> str: - pad_top, pad_x, pad_bottom = 14, 14, 14 + pad_top, pad_x, pad_bottom = PAD_TOP, PAD_X, PAD_BOTTOM vb_w = self.w + 2 * pad_x vb_h = self.h + pad_top + pad_bottom out_w = round(vb_w * self.INTRINSIC_SCALE) @@ -489,6 +497,7 @@ class Card: order: int | str figure: Callable[[Canvas], None] note: str = "" + caption: str = "" width: int = 320 height: int = 110 is_journey: bool = False @@ -503,6 +512,12 @@ def render_html(self) -> str: eyebrow = f"{self.section} · {self.order:02d}" else: eyebrow = f"Journey · {self.order}" + # The production caption is part of what reviewers must judge: + # a caption that asserts something the figure does not draw is a + # shipped defect, so the gestalt shows the exact figcaption text. + caption_html = ( + f'

    {xml_escape(self.caption)}

    \n' if self.caption else "" + ) note_html = f'

    {self.note}

    \n' if self.note else "" score_html = "" if self.score is not None: @@ -518,6 +533,7 @@ def render_html(self) -> str: f'

    {eyebrow}

    \n' f'

    {self.title}

    \n' f" {c.to_svg()}\n" + f"{caption_html}" f"{score_html}" f"{note_html}" f"" diff --git a/src/security.py b/src/security.py new file mode 100644 index 0000000..3a52836 --- /dev/null +++ b/src/security.py @@ -0,0 +1,30 @@ +"""Shared browser security constants for Worker-rendered and static HTML.""" + +# The inline runner scripts are curated application code. They carry this +# nonce so script-src can avoid unsafe-inline while still serving cached HTML. +CSP_SCRIPT_NONCE = "pbe-inline-v1" +STRICT_TRANSPORT_SECURITY = "max-age=31536000; includeSubDomains; preload" + +CONTENT_SECURITY_POLICY = "; ".join( + [ + "default-src 'self'", + "base-uri 'none'", + "object-src 'none'", + "frame-ancestors 'none'", + "form-action 'self'", + "img-src 'self' data:", + "font-src 'self'", + # CodeMirror injects style elements and Shiki emits style attributes. + # Keep script execution strict while allowing those rendering styles. + "style-src 'self' 'unsafe-inline'", + ( + "script-src 'self' " + f"'nonce-{CSP_SCRIPT_NONCE}' " + "https://esm.sh https://challenges.cloudflare.com 'wasm-unsafe-eval'" + ), + "script-src-attr 'none'", + "connect-src 'self' https://esm.sh https://challenges.cloudflare.com", + "frame-src https://challenges.cloudflare.com", + "worker-src 'self'", + ] +) diff --git a/src/templates/example.html b/src/templates/example.html index 259401b..726f353 100644 --- a/src/templates/example.html +++ b/src/templates/example.html @@ -15,22 +15,24 @@

    Run the complete example

    Example code

    - + __TURNSTILE_CHALLENGE__
    - +

    __OUTPUT_HEADING__

    __SHOWN_OUTPUT__

    __EXECUTION_TIME__

    -