diff --git a/docs/libraries/numpy.md b/docs/libraries/numpy.md
index a42f472..d0fcd64 100644
--- a/docs/libraries/numpy.md
+++ b/docs/libraries/numpy.md
@@ -101,11 +101,11 @@ print(lengths_m)
| Python `for` loop |
O(n) | — |
| Vectorized |
O(n) (same class, smaller constant) | — |
- A Python `for` loop over a list and a vectorized NumPy operation both touch every element once — O(n) either way, the same [Big O](../style.md#big-o-notation) class.
+ A Python `for` loop over a list and a vectorized NumPy operation both touch every element once — O(n) either way, the same [Big O](../practices/style.md#big-o-notation) class.
The speed difference is a constant factor, not the order of growth: each pass of a Python loop pays the interpreter's per-iteration overhead, while a vectorized operation runs its loop once, in compiled C, underneath a single Python call. That overhead is small per element but adds up — the larger the array, the bigger the gap, even though neither approach's growth rate has changed.
- See [Efficiency](../style.md#efficiency) for why this distinction matters.
+ See [Efficiency](../practices/style.md#efficiency) for why this distinction matters.
### Aggregating an array
diff --git a/docs/libraries/pillow.md b/docs/libraries/pillow.md
index 6170e96..a716449 100644
--- a/docs/libraries/pillow.md
+++ b/docs/libraries/pillow.md
@@ -326,7 +326,7 @@ img.save("shapes.png")
```
??? tip "Drawing with objects"
- Once a drawing gets complicated, it's common to wrap each thing you're drawing in its own class — an object that stores its own position/size/color, and knows how to draw itself given a drawing context. Nothing here is Pillow-specific: it's the same pattern covered in [Classes](../classes.md) — bundling data with the behavior that acts on it — just applied to a shape instead of a snake. A calling function loops over a list of these objects and calls `.draw()` on each, so building a complex image — dozens of randomly placed shapes, say, using the `random` module — is just a loop appending new `Shape` objects rather than dozens of manual `draw_context` calls.
+ Once a drawing gets complicated, it's common to wrap each thing you're drawing in its own class — an object that stores its own position/size/color, and knows how to draw itself given a drawing context. Nothing here is Pillow-specific: it's the same pattern covered in [Classes](../organization/classes.md) — bundling data with the behavior that acts on it — just applied to a shape instead of a snake. A calling function loops over a list of these objects and calls `.draw()` on each, so building a complex image — dozens of randomly placed shapes, say, using the `random` module — is just a loop appending new `Shape` objects rather than dozens of manual `draw_context` calls.
```python-ref
class Shape:
@@ -585,7 +585,7 @@ img.convert("RGB").save("snake.jpg")
## ImageSequence module
-An animated GIF is really a whole stack of images shown one after another. `Image.open()` only gives you the first frame by default — `ImageSequence` lets a [`for` loop](../loops.md) step through every frame in order.
+An animated GIF is really a whole stack of images shown one after another. `Image.open()` only gives you the first frame by default — `ImageSequence` lets a [`for` loop](../flow/loops.md) step through every frame in order.
```python-ref
from PIL import Image, ImageSequence
@@ -619,7 +619,7 @@ for frame in ImageSequence.Iterator(gif):
## Putting it together
-Pillow doesn't need anything special to combine with the rest of Python — a function wrapping one transformation, called from an `if`/`elif` chosen by [user input](../conditionals.md), looped until the user's done, is enough to build a small interactive tool out of the operations above.
+Pillow doesn't need anything special to combine with the rest of Python — a function wrapping one transformation, called from an `if`/`elif` chosen by [user input](../flow/conditionals.md), looped until the user's done, is enough to build a small interactive tool out of the operations above.
```python-ref
def apply_filter(img, choice):
@@ -635,7 +635,7 @@ def apply_filter(img, choice):
### An interactive filter tool
-Combines a function, an `if`/`elif` chain, and a `while` loop — nothing here is Pillow-specific. Each piece here is something covered elsewhere on this site — a [function](../functions.md) wrapping one transformation, an [`if`/`elif` chain](../conditionals.md) picking which one to run, and a [`while` loop](../loops.md#while-loops) that keeps asking until the user's satisfied. Pillow itself only shows up inside `apply_filter`.
+Combines a function, an `if`/`elif` chain, and a `while` loop — nothing here is Pillow-specific. Each piece here is something covered elsewhere on this site — a [function](../organization/functions.md) wrapping one transformation, an [`if`/`elif` chain](../flow/conditionals.md) picking which one to run, and a [`while` loop](../flow/loops.md#while-loops) that keeps asking until the user's satisfied. Pillow itself only shows up inside `apply_filter`.
```python-ref
while True:
diff --git a/docs/libraries/pytest.md b/docs/libraries/pytest.md
index af6f4f4..ad248a0 100644
--- a/docs/libraries/pytest.md
+++ b/docs/libraries/pytest.md
@@ -189,7 +189,7 @@ def test_length_is_positive(species, length_ft):
## Testing for exceptions
-`pytest.raises()` is a context manager that asserts the code inside its `with` block raises a specific exception — a way to test the [`try`/`except`](../errors.md#catch-with-tryexcept) paths in your own code, not just the successful ones.
+`pytest.raises()` is a context manager that asserts the code inside its `with` block raises a specific exception — a way to test the [`try`/`except`](../practices/errors.md#catch-with-tryexcept) paths in your own code, not just the successful ones.
```python-ref
def test_invalid_length_raises():
diff --git a/docs/libraries/re.md b/docs/libraries/re.md
index b9b7e1b..4521dce 100644
--- a/docs/libraries/re.md
+++ b/docs/libraries/re.md
@@ -74,7 +74,7 @@ print(match.group())
Calling `re.search()` (or `.findall()`, `.sub()`, etc.) with a raw pattern string repeats the same compilation work internally on every call, even when the pattern never changes — across a loop of n calls, that's n compilations of the same pattern. Compiling it once with `re.compile()` above the loop and calling `.search()` on the result instead does that work exactly once, however many times the loop runs.
- See [Efficiency](../style.md#efficiency) for why this distinction matters.
+ See [Efficiency](../practices/style.md#efficiency) for why this distinction matters.
??? run "Run a searching example"
All the examples above, combined into one script:
diff --git a/docs/libraries/requests.md b/docs/libraries/requests.md
index 68e6d9a..0e33584 100644
--- a/docs/libraries/requests.md
+++ b/docs/libraries/requests.md
@@ -98,7 +98,7 @@ print("request succeeded")
### Parsing JSON
-`.json()` converts a JSON response body directly into a Python `dict` or `list`. Most web APIs send their data back as JSON — text formatted so it maps directly onto Python's own `dict`/`list` structures, which is why `.json()` needs no extra parsing step. Once converted, the result works exactly like any other [dict](../collections.md#dictionaries) or [list](../collections.md#lists) you'd build by hand.
+`.json()` converts a JSON response body directly into a Python `dict` or `list`. Most web APIs send their data back as JSON — text formatted so it maps directly onto Python's own `dict`/`list` structures, which is why `.json()` needs no extra parsing step. Once converted, the result works exactly like any other [dict](../types/collections.md#dictionaries) or [list](../types/collections.md#lists) you'd build by hand.
```python-ref
response = requests.get("https://jsonplaceholder.typicode.com/posts/1")
@@ -198,7 +198,7 @@ print(response.json())
## Handling request errors
-A network call can fail in ways that have nothing to do with your code — the [Errors](../errors.md#catch-with-tryexcept) page covers `try`/`except` in general; a couple of exceptions are specific to `requests`.
+A network call can fail in ways that have nothing to do with your code — the [Errors](../practices/errors.md#catch-with-tryexcept) page covers `try`/`except` in general; a couple of exceptions are specific to `requests`.
| Exception | Happens when |
|-----------|---------------|
diff --git a/docs/libraries/turtle.md b/docs/libraries/turtle.md
index 249bf33..7f8dd46 100644
--- a/docs/libraries/turtle.md
+++ b/docs/libraries/turtle.md
@@ -204,7 +204,7 @@ A related but separate setting — `speed(n)` controls how fast each individual
`tracer()` also accepts two numbers, `tracer(n, delay)` — show only every `n`-th update, with `delay` milliseconds between them, instead of turning animation off completely. Useful for speeding up something slow and complex without losing the animation altogether.
??? tip "no_animation() block"
- A context manager wrapping the same idea as `tracer(False)`/`tracer(True)` — animation is off for whatever runs inside the block, then back on (and shown) once it exits. The same `with` pattern as [opening a file](../files.md#with), applied to animation instead of a file handle.
+ A context manager wrapping the same idea as `tracer(False)`/`tracer(True)` — animation is off for whatever runs inside the block, then back on (and shown) once it exits. The same `with` pattern as [opening a file](../resources/files.md#with), applied to animation instead of a file handle.
```python-ref
with no_animation():
@@ -383,7 +383,7 @@ x, y = ball # unpack like any other tuple — 3, 5
```
??? tip "Reassigning from inside a function"
- Reassigning a global variable's name from inside a function needs `global`, covered on [Functions](../functions.md#local-vs-global-variables) — a game typically has at least one small function whose only job is reassigning a position or direction this way. *Mutating* something in place instead (`trail.append(...)`, `paddles[1] = paddles[1] + Vec2D(0, 20)`, both from "Many positions at once" below) doesn't need `global`, since the name itself is never reassigned — only reassignment does.
+ Reassigning a global variable's name from inside a function needs `global`, covered on [Functions](../organization/functions.md#local-vs-global-variables) — a game typically has at least one small function whose only job is reassigning a position or direction this way. *Mutating* something in place instead (`trail.append(...)`, `paddles[1] = paddles[1] + Vec2D(0, 20)`, both from "Many positions at once" below) doesn't need `global`, since the name itself is never reassigned — only reassignment does.
```python-ref
aim = Vec2D(0, -10)
@@ -411,7 +411,7 @@ forward(5)
#### Many positions at once { data-card-link="skip" }
-A game's state is rarely just one lone position — a trail that grows over time, or several independent entities tracked at once. Both build on the same list/dict operations covered on [Collections](../collections.md).
+A game's state is rarely just one lone position — a trail that grows over time, or several independent entities tracked at once. Both build on the same list/dict operations covered on [Collections](../types/collections.md).
```python-ref
trail = [Vec2D(10, 0)]
diff --git a/docs/classes.md b/docs/organization/classes.md
similarity index 96%
rename from docs/classes.md
rename to docs/organization/classes.md
index b9e94f8..ced01e4 100644
--- a/docs/classes.md
+++ b/docs/organization/classes.md
@@ -8,7 +8,7 @@ description: >-
-A **class** bundles related data together with the behavior (methods) that acts on it, instead of keeping them separate. A [dictionary](collections.md#dictionaries) can already hold a snake's data as key-value pairs — a class goes one step further, pairing that data with the functions that work on it. Structuring code this way is called **object-oriented programming (OOP)**.
+A **class** bundles related data together with the behavior (methods) that acts on it, instead of keeping them separate. A [dictionary](../types/collections.md#dictionaries) can already hold a snake's data as key-value pairs — a class goes one step further, pairing that data with the functions that work on it. Structuring code this way is called **object-oriented programming (OOP)**.
| Concept | Example | What it is |
|---------|---------|------------|
@@ -142,7 +142,7 @@ For a value every object should share instead of holding its own copy, see [clas
| Plain instance | — | O(n) |
| `__slots__` | — | O(n) (same class, smaller constant) |
- Each instance normally keeps its attributes in a per-object `__dict__`, which costs some [memory](style.md#time-and-space) on top of the attribute values themselves — usually not worth worrying about, but it adds up when a program holds thousands or millions of instances at once. `__slots__` trades that flexibility for a fixed, lighter attribute layout:
+ Each instance normally keeps its attributes in a per-object `__dict__`, which costs some [memory](../practices/style.md#time-and-space) on top of the attribute values themselves — usually not worth worrying about, but it adds up when a program holds thousands or millions of instances at once. `__slots__` trades that flexibility for a fixed, lighter attribute layout:
```python-ref
class Snake:
@@ -155,7 +155,7 @@ For a value every object should share instead of holding its own copy, see [clas
An instance built from this class can no longer get a new attribute added after creation — `ball.venomous = False` raises `AttributeError`, since there's no `__dict__` left for it to go into.
- See [Efficiency](style.md#efficiency) for why this distinction matters.
+ See [Efficiency](../practices/style.md#efficiency) for why this distinction matters.
@@ -215,7 +215,7 @@ print(burmese.kingdom) # "Animalia" — unaffected
```
??? tip "Modify & delete attributes"
- Assign to `object.attribute` to change it after creation — an object is **mutable**, so this changes it in place, the same as [updating an item in a list](collections.md#access-and-update-items). That also means a second variable pointing at the same object sees the change too: `twin = ball` doesn't copy `ball`, it just gives the same object a second name.
+ Assign to `object.attribute` to change it after creation — an object is **mutable**, so this changes it in place, the same as [updating an item in a list](../types/collections.md#access-and-update-items). That also means a second variable pointing at the same object sees the change too: `twin = ball` doesn't copy `ball`, it just gives the same object a second name.
`del object.attribute` removes a single attribute; `del object` removes the object itself.
diff --git a/docs/functions.md b/docs/organization/functions.md
similarity index 94%
rename from docs/functions.md
rename to docs/organization/functions.md
index 4b095fd..14105a1 100644
--- a/docs/functions.md
+++ b/docs/organization/functions.md
@@ -105,7 +105,7 @@ message = describe("ball") # "a ball python" is the return value, so now
A function is first defined. After it's defined, you can [call the function](#calling-a-function) whenever you need to run it.
-The function definition line contains **`def`**, a **function name** (follows the same [naming rules as variables](foundations.md#naming-variables)), **parentheses** holding zero or more **parameters**, and a **colon**.
+The function definition line contains **`def`**, a **function name** (follows the same [naming rules as variables](../start/foundations.md#naming-variables)), **parentheses** holding zero or more **parameters**, and a **colon**.
Under it is an indented **body**: the block of code that runs when the function is [called](#calling-a-function).
@@ -175,7 +175,7 @@ describe("ball") # second parameter is not given, so length_ft is the d
#### *args tuple
-`*args` collects any number of positional arguments into a single [tuple](collections.md#tuples), so a function can accept as many as the caller passes instead of a fixed list of parameters. `*args` is the conventional name, but any name after `*` works.
+`*args` collects any number of positional arguments into a single [tuple](../types/collections.md#tuples), so a function can accept as many as the caller passes instead of a fixed list of parameters. `*args` is the conventional name, but any name after `*` works.
```python-ref
def total_length(*args):
@@ -186,7 +186,7 @@ total_length(5, 12, 8) # 25
#### **kwargs dict
-`**kwargs` collects any number of keyword arguments into a single [dict](collections.md#dictionaries), so a function can accept as many `name=value` pairs as the caller passes instead of a fixed list of parameters. `**kwargs` is the conventional name, but any name after `**` works.
+`**kwargs` collects any number of keyword arguments into a single [dict](../types/collections.md#dictionaries), so a function can accept as many `name=value` pairs as the caller passes instead of a fixed list of parameters. `**kwargs` is the conventional name, but any name after `**` works.
```python-ref
def describe(**details):
@@ -275,7 +275,7 @@ result = find_species("cobra") # None — the function fell through without a
#### Multiple values { data-card-link="skip" }
-`return` followed by several values separated by commas [packs](collections.md#packing-and-unpacking) them into a tuple as a single return value. The caller unpacks that tuple to use the values separately — see [multiple values](#multiple-values_1) under calling a function.
+`return` followed by several values separated by commas [packs](../types/collections.md#packing-and-unpacking) them into a tuple as a single return value. The caller unpacks that tuple to use the values separately — see [multiple values](#multiple-values_1) under calling a function.
```python-ref
def describe(species, length_ft):
@@ -333,7 +333,7 @@ def describe(species):
### Docstrings
-A triple-quoted string as a function's first line documents what it does — most editors show it automatically when you use the function elsewhere. A **docstring** is the same triple-quoted-string trick covered on the [Foundations](foundations.md#multi-line-comments-with) page, but placed as the very first line inside a function specifically to document it. Unlike a regular comment, Python actually stores a docstring (as the function's `__doc__` attribute) rather than discarding it — which is how editors are able to show it in a tooltip when you call the function elsewhere, without you needing to go find the definition.
+A triple-quoted string as a function's first line documents what it does — most editors show it automatically when you use the function elsewhere. A **docstring** is the same triple-quoted-string trick covered on the [Foundations](../start/foundations.md#multi-line-comments-with) page, but placed as the very first line inside a function specifically to document it. Unlike a regular comment, Python actually stores a docstring (as the function's `__doc__` attribute) rather than discarding it — which is how editors are able to show it in a tooltip when you call the function elsewhere, without you needing to go find the definition.
Short, single-line docstrings are common for simple functions:
@@ -436,7 +436,7 @@ message = describe("ball") # "a ball python" — stored, not printed
#### Multiple values { data-card-link="skip" }
-A function that [returns multiple values packed into a tuple](#multiple-values) can have them unpacked straight into multiple variables in one line at the call site. `name, length = describe(...)` unpacks the returned tuple, matching each variable to the tuple's items by position — the same as [unpacking any other tuple](collections.md#packing-and-unpacking). The number of variables on the left has to match the number of values returned.
+A function that [returns multiple values packed into a tuple](#multiple-values) can have them unpacked straight into multiple variables in one line at the call site. `name, length = describe(...)` unpacks the returned tuple, matching each variable to the tuple's items by position — the same as [unpacking any other tuple](../types/collections.md#packing-and-unpacking). The number of variables on the left has to match the number of values returned.
```python-ref
def describe(species, length_ft):
@@ -550,7 +550,7 @@ Every recursive function needs two parts:
```
??? tip "Recursion vs. a loop"
- Anything recursion can do, a loop can do too — recursion is rarely the only option, just sometimes the more natural fit. It reads most naturally for problems already defined in terms of themselves, like a [nested dictionary](collections.md#dictionaries) of arbitrary depth, where the number of levels isn't known ahead of time. For a simple countdown like the one above, a `while` loop is just as clear and doesn't risk a `RecursionError`.
+ Anything recursion can do, a loop can do too — recursion is rarely the only option, just sometimes the more natural fit. It reads most naturally for problems already defined in terms of themselves, like a [nested dictionary](../types/collections.md#dictionaries) of arbitrary depth, where the number of levels isn't known ahead of time. For a simple countdown like the one above, a `while` loop is just as clear and doesn't risk a `RecursionError`.
```python-ref
n = 3
@@ -570,9 +570,9 @@ Every recursive function needs two parts:
Every call a function makes — recursive or not — adds a frame to the call stack and holds that call's local variables until it returns. A recursive function keeps every call's frame alive until the base case is reached, so its space cost is O(n) for n levels of recursion. That's why the `RecursionError` exists — Python caps how deep the stack can grow before it runs out of room.
- A loop reuses the same frame each pass, O(1) [space](style.md#time-and-space).
+ A loop reuses the same frame each pass, O(1) [space](../practices/style.md#time-and-space).
- See [Efficiency](style.md#efficiency) for why this distinction matters.
+ See [Efficiency](../practices/style.md#efficiency) for why this distinction matters.
@@ -865,7 +865,7 @@ next(counter) # 2
### Generator expressions
-Parentheses instead of brackets turn a [list comprehension](collections.md#list-comprehension) into a generator expression — same filtering and transforming syntax, but values are produced lazily instead of built into a list all at once.
+Parentheses instead of brackets turn a [list comprehension](../types/collections.md#list-comprehension) into a generator expression — same filtering and transforming syntax, but values are produced lazily instead of built into a list all at once.
`doubled_list` below is an actual `list` — `[10, 24, 16]`, every value already computed — so it supports indexing, `len()`, and looping over more than once. `doubled_gen` is a `generator` — nothing has been computed yet, and it only supports stepping forward once with `next()` or a `for` loop, the same [list vs. generator](#generator-vs-a-regular-function) tradeoff covered above.
diff --git a/docs/errors.md b/docs/practices/errors.md
similarity index 89%
rename from docs/errors.md
rename to docs/practices/errors.md
index cc69bea..8022176 100644
--- a/docs/errors.md
+++ b/docs/practices/errors.md
@@ -77,16 +77,16 @@ Think about what programming concepts the failing line is using (data type, loop
| Kind of runtime error | Happens when | Check for |
|-------|---------------|-----------|
| **`AssertionError`** | An [`assert`](#assert-a-condition) statement's condition was `False` |
@@ -233,7 +233,7 @@ species = "burmese python" # replaces the old value entirely
a = b = 0
```
- `species, length_ft = "ball python", 4.5` assigns each value to the matching name in order — the same unpacking mechanism covered on the [Collections](collections.md#packing-and-unpacking) page. `a = b = 0` instead points every name at the *same* value, useful for initializing a few counters at once.
+ `species, length_ft = "ball python", 4.5` assigns each value to the matching name in order — the same unpacking mechanism covered on the [Collections](../types/collections.md#packing-and-unpacking) page. `a = b = 0` instead points every name at the *same* value, useful for initializing a few counters at once.
??? run "Run a variables example"
All the examples above, combined into one script:
@@ -298,11 +298,11 @@ print(species, length_ft, "ft")
Commas are usually the easier choice for a quick print. Pass `sep="..."` to change the default single-space separator, like `print(species, length_ft, sep=", ")`.
-Once you're comfortable with the basics here, the [Collections](collections.md#list-operations) page covers printing the contents of a list or dict.
+Once you're comfortable with the basics here, the [Collections](../types/collections.md#list-operations) page covers printing the contents of a list or dict.
### Building a string manually { data-card-link="skip" }
-Come back to this once you've read the [Types](types.md) page.
+Come back to this once you've read the [Types](../types/basics.md) page.
You can also build one string yourself with `+` and print that instead of using commas — but every piece has to already be a string, so a number like `length_ft` needs `str()` first, and you have to add the spaces yourself.
@@ -310,7 +310,7 @@ You can also build one string yourself with `+` and print that instead of using
print(species + " " + str(length_ft) + " ft") # ball python 4.5 ft — same output, more typing
```
-For building a full sentence out of text and variables, an [f-string](types.md#building-strings) is usually clearer than either approach.
+For building a full sentence out of text and variables, an [f-string](../types/basics.md#building-strings) is usually clearer than either approach.
??? run "Run a printing variables example"
All the examples above, combined into one script:
@@ -332,7 +332,7 @@ For building a full sentence out of text and variables, an [f-string](types.md#b
### Variables and types { data-card-link="skip" }
-Come back to this once you've read the [Types](types.md) page.
+Come back to this once you've read the [Types](../types/basics.md) page.
A variable isn't locked to the type of value it first held — `species` can hold a string, then later be reassigned to an `int` or `float`, with no error.
@@ -341,7 +341,7 @@ species = "burmese python" # str
species = 12 # now an int — Python allows this
```
-Other languages fix a variable to one type permanently at creation; Python doesn't. Every value still has its own type — [covered in full here](types.md) — a variable is just a name that can point at any of them, one at a time.
+Other languages fix a variable to one type permanently at creation; Python doesn't. Every value still has its own type — [covered in full here](../types/basics.md) — a variable is just a name that can point at any of them, one at a time.
@@ -349,7 +349,7 @@ Other languages fix a variable to one type permanently at creation; Python doesn
## Expressions and statements
-Every line of Python code is either an **expression** or a **statement**. An expression is anything that evaluates to a value — `2 + 3`, `species`, `species == "burmese"`. A statement is a complete instruction — an assignment, a `print()` call, an `if` statement's condition (covered on the [Conditionals](conditionals.md#if-elif-else) page) — and it's usually built out of one or more expressions.
+Every line of Python code is either an **expression** or a **statement**. An expression is anything that evaluates to a value — `2 + 3`, `species`, `species == "burmese"`. A statement is a complete instruction — an assignment, a `print()` call, an `if` statement's condition (covered on the [Conditionals](../flow/conditionals.md#if-elif-else) page) — and it's usually built out of one or more expressions.
```python-ref
2 + 3 # an expression — evaluates to 5
@@ -443,11 +443,11 @@ print("Hello,", name) # now a usable variable
### Converting input to a number { data-card-link="skip" }
-Come back to this once you've read the [Types](types.md) page.
+Come back to this once you've read the [Types](../types/basics.md) page.
Whatever the person types, `input()` always hands it back as a **string** — even a typed number comes back as text, not a real number.
-To use what the user has entered as a real number, you must convert it with `int()` or `float()`, covered on the [Types](types.md#convert) page. Skipping this step causes an error the moment you try to do math with it — Python won't add a number to a string.
+To use what the user has entered as a real number, you must convert it with `int()` or `float()`, covered on the [Types](../types/basics.md#convert) page. Skipping this step causes an error the moment you try to do math with it — Python won't add a number to a string.
```python-ref
age = input("How old are you? ") # "8" — a string, not the number 8
@@ -539,7 +539,7 @@ Python doesn't have a true multi-line comment symbol — a triple-quoted string
[^triple-quote-string]: It isn't technically a comment — it's a string that Python creates and then immediately discards since nothing uses it. Python just never complains about a statement that does nothing, so the effect is the same as a real comment.
-Placed as the very first line inside a function or a file specifically, this same trick is called a **docstring** and documents what that function or file does. Function docstrings are covered on the [Functions](functions.md#docstrings) page.
+Placed as the very first line inside a function or a file specifically, this same trick is called a **docstring** and documents what that function or file does. Function docstrings are covered on the [Functions](../organization/functions.md#docstrings) page.
Placed as the very first line of a file instead, it becomes a **module docstring** — documenting the file as a whole rather than a single function, and a common place to note who wrote it and when.
@@ -557,7 +557,7 @@ species = "ball python"
length_ft = 4.5
```
-More on docstring conventions on the [Style](style.md#docstrings) page.
+More on docstring conventions on the [Style](../practices/style.md#docstrings) page.
diff --git a/docs/workspace.md b/docs/start/workspace.md
similarity index 96%
rename from docs/workspace.md
rename to docs/start/workspace.md
index 31a77cc..cff8b31 100644
--- a/docs/workspace.md
+++ b/docs/start/workspace.md
@@ -62,7 +62,7 @@ A **code editor** or an **IDE** ("Integrated Development Environment") is a text
- **Running code is easier** — click a Run button from your IDE instead of typing Terminal commands every time
- **Code completion** — the editor suggests function names and variables as you type, saving time and reducing typos
- **Error detection** — it warns you about common mistakes before you run the code
-- **[Debugging](errors.md#debugger-tool)** — pause your code mid-run and inspect variables to track down bugs, instead of only reading output after the fact
+- **[Debugging](../practices/errors.md#debugger-tool)** — pause your code mid-run and inspect variables to track down bugs, instead of only reading output after the fact
Download one of the **free** code editors below. You can always switch later.
@@ -141,7 +141,7 @@ That's it! You've written and run your first Python program. From here, you can
??? tip "Reading error messages"
- When you see red error text, the [Errors](errors.md#reading-a-traceback) page covers how to read it.
+ When you see red error text, the [Errors](../practices/errors.md#reading-a-traceback) page covers how to read it.
@@ -234,7 +234,7 @@ It's good for running Python files that are already finished — either your own
## Virtual environments { data-advanced="true" }
-Sometimes you'll want to install [external libraries](./libraries/index.md) for your project. A **virtual environment** keeps each project's installed libraries in their own separate folder instead of installing them onto your computer.
+Sometimes you'll want to install [external libraries](../libraries/index.md) for your project. A **virtual environment** keeps each project's installed libraries in their own separate folder instead of installing them onto your computer.
**Benefits:**
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
index 1d8ac4e..275317b 100644
--- a/docs/stylesheets/extra.css
+++ b/docs/stylesheets/extra.css
@@ -53,6 +53,12 @@
--pt-shimmer-gold: #D9C48C;
--pt-shimmer-sage: #B9CDAE;
--pt-shimmer-clay: #CBA989;
+
+ /* Active top-nav tab (e.g. "Flow" when conditionals.md is open) — the
+ site's own accent green, deliberately darker than the inactive tabs'
+ --pt-text-secondary below it, so the current page reads as the
+ "heavier," more deliberate mark rather than a lighter highlight. */
+ --pt-nav-active: var(--pt-accent);
}
[data-md-color-scheme="slate"] {
@@ -87,6 +93,8 @@
--pt-shimmer-indigo: #201f3d;
--pt-shimmer-purple: #2a1c3d;
--pt-shimmer-green: #17281f;
+
+ --pt-nav-active: #8FCB93;
}
/* Material re-sets --md-* variables on [data-md-color-scheme] (applied to
@@ -115,6 +123,27 @@
--md-typeset-a-color: var(--pt-accent);
+ /* mkdocs-nested-tabs override hooks (see CLAUDE.md's "Planned extraction"
+ section). The page-link row keeps the standard --pt-ink default (dark
+ ink in light mode, off-white in dark mode) used everywhere else for
+ default header/tab text. The all-caps category-label row above it uses
+ --pt-text-secondary instead — a lower-contrast grey, lighter than full
+ ink in light mode and darker than full white in dark mode — so the two
+ rows read as visually distinct tiers rather than identical text.
+
+ --md-nested-tabs-label-active-color (added in mkdocs-nested-tabs 0.2.0)
+ colors the category label itself when one of its own pages is active —
+ e.g. "Flow" going --pt-nav-active while "Conditionals" is open. The
+ active page link keeps the plugin's default (--md-accent-fg-color, i.e.
+ --pt-accent) rather than a --md-nested-tabs-link-active-color override,
+ so parent and child read as two distinct active tones, not one. Before
+ 0.2.0 added this hook, achieving the same thing needed a :has() rule
+ reaching into the plugin's markup from outside — see git history if
+ that's ever relevant again. */
+ --md-nested-tabs-label-color: var(--pt-text-secondary);
+ --md-nested-tabs-link-color: var(--pt-ink);
+ --md-nested-tabs-label-active-color: var(--pt-nav-active);
+
--md-code-bg-color: var(--pt-panel);
--md-code-fg-color: var(--pt-ink);
@@ -172,9 +201,15 @@ html:focus-within::-webkit-scrollbar-thumb {
color: var(--pt-ink);
}
+/* Soft halo in --pt-panel (a lighter, in-palette tint of --pt-bg rather than
+ an arbitrary white) so the title reads clearly over the busier parts of
+ the header's illustrated background, without needing a solid pill/box
+ behind the text. Two stacked passes build up more coverage than a single
+ blur radius gives on its own. */
.md-header__title,
.md-header__topic {
font-weight: 800;
+ text-shadow: 0 0 3px var(--pt-panel), 0 0 6px var(--pt-panel);
}
/* Below ~45em the header's own title font-size (Material's default) plus
@@ -237,6 +272,25 @@ html:focus-within::-webkit-scrollbar-thumb {
background-repeat: repeat-x;
}
+/* Material's own collapsed/idle search box (shown ≥60em before it's
+ clicked into) defaults to a mostly-transparent black fill
+ (`background-color: #00000042`, ~26% opaque, meant to sit on Material's
+ flat primary-color header). Against this site's much darker slate
+ header — plus the shimmer artwork underneath it — that low opacity
+ barely reads as a distinct control. Raise the opacity so the search box
+ is actually visible as its own element; once expanded/active it already
+ switches to a solid `var(--md-default-bg-color)` fill via Material's own
+ `[data-md-toggle=search]:checked` rule, untouched here. */
+@media screen and (min-width: 60em) {
+ [data-md-color-scheme="slate"] .md-search__form {
+ background-color: rgba(0, 0, 0, 0.6);
+ }
+
+ [data-md-color-scheme="slate"] .md-search__form:hover {
+ background-color: rgba(0, 0, 0, 0.72);
+ }
+}
+
[data-md-color-scheme="slate"] .md-header::before {
content: "";
position: absolute;
@@ -298,6 +352,34 @@ html:focus-within::-webkit-scrollbar-thumb {
mask-repeat: repeat-x;
}
+/* Mobile gets its own header art (same background-strip + mask setup, just
+ a different source file per scheme) rather than scaling the desktop
+ artwork down — a wide illustration loses detail and can read as visual
+ noise at the header's much shorter mobile height. Same 45em breakpoint as
+ the title-sizing rule above, so the art and the title layout switch at the
+ same width. Swapping just the url() here (not duplicating the whole rule
+ block) keeps the background-size/position/repeat and the mask wiring
+ defined once. */
+@media (max-width: 45em) {
+ [data-md-color-scheme="slate"] .md-header {
+ background-image: url("../img/header_dark_mobile.svg");
+ }
+
+ [data-md-color-scheme="slate"] .md-header::before {
+ -webkit-mask-image: url("../img/header_dark_mobile.svg");
+ mask-image: url("../img/header_dark_mobile.svg");
+ }
+
+ [data-md-color-scheme="default"] .md-header {
+ background-image: url("../img/header_light_mobile.svg");
+ }
+
+ [data-md-color-scheme="default"] .md-header::before {
+ -webkit-mask-image: url("../img/header_light_mobile.svg");
+ mask-image: url("../img/header_light_mobile.svg");
+ }
+}
+
@media (prefers-reduced-motion: no-preference) {
[data-md-color-scheme="slate"] .md-header::before,
[data-md-color-scheme="default"] .md-header::before {
@@ -311,12 +393,98 @@ html:focus-within::-webkit-scrollbar-thumb {
100% { background-position: 0% 50%; }
}
-.md-header__button.md-logo img {
- border: 1px solid rgba(103, 58, 183, 0.6);
+/* The logo only ever linked home with no other purpose, so
+ cheatsheet_button.js's "Cheatsheet" pill replaces it in the header's
+ top-left slot instead of sitting alongside it — hidden here, not
+ removed from the DOM, since Material still generates it normally. */
+.md-header__button.md-logo {
+ display: none;
+}
+
+/* "Cheatsheet" link injected by cheatsheet_button.js, in the logo's old
+ spot (leading the site title) — an explicit text shortcut to the
+ homepage (already this site's compact quick-reference dashboard) rather
+ than relying on the logo image alone reading as clickable. Deliberately
+ reuses the Essentials/Advanced toggle's own technique (a currentColor
+ pill fill + --pt-bg text flip when active) rather than hardcoded colors,
+ so it adapts across light/dark mode exactly the way that toggle does —
+ currentColor here is .md-header's own `color: var(--pt-ink)`, inherited,
+ not set locally. */
+.pt-cheatsheet-link {
+ display: inline-flex;
+ align-items: center;
+ margin-right: 0.6rem;
+ padding: 0.15rem 0.6rem;
+ /* Matches .pt-simplify-toggle's own resting track (border + background)
+ below, so the inactive pill reads as the same kind of control rather
+ than plain text — var(--pt-ink) directly instead of currentColor,
+ sidestepping the self-reference risk noted below for the active state. */
+ border: 0.05rem solid var(--pt-ink);
+ border-radius: 1rem;
+ background-color: var(--pt-bg);
+ font-size: 0.6rem;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ color: currentColor;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+/* Material Symbols "dashboard" (viewBox 0 -960 960 960), same mask-image
+ technique as .pt-simplify-option's icons above — an asymmetric mixed-size
+ panel shape, matching the homepage's own grid (its library boxes are
+ genuinely different widths, not a uniform grid — see the "External
+ files and resources" grid-math notes in CLAUDE.md) rather than a plain
+ symmetric grid icon. Fetched from Google's material-design-icons source
+ directly (not hand-drawn) to keep the path data exact. Leads the word
+ (::before, not ::after), matching the icon-then-text order used by the
+ Essentials/Advanced toggle, the theme toggle, and the toast. */
+.pt-cheatsheet-link::before {
+ content: "";
+ display: inline-block;
+ width: 0.7rem;
+ height: 0.7rem;
+ flex: none;
+ margin-right: 0.3rem;
+ background-color: currentColor;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+ -webkit-mask-size: contain;
+ mask-size: contain;
+ -webkit-mask-position: center;
+ mask-position: center;
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M520-600v-240h320v240H520ZM120-440v-400h320v400H120Zm400 320v-400h320v400H520Zm-400 0v-240h320v240H120Zm80-400h160v-240H200v240Zm400 320h160v-240H600v240Zm0-480h160v-80H600v80ZM200-200h160v-80H200v80Zm160-320Zm240-160Zm0 240ZM360-280Z'/%3E%3C/svg%3E");
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M520-600v-240h320v240H520ZM120-440v-400h320v400H120Zm400 320v-400h320v400H520Zm-400 0v-240h320v240H120Zm80-400h160v-240H200v240Zm400 320h160v-240H600v240Zm0-480h160v-80H600v80ZM200-200h160v-80H200v80Zm160-320Zm240-160Zm0 240ZM360-280Z'/%3E%3C/svg%3E");
+}
+
+.pt-cheatsheet-link:hover,
+.pt-cheatsheet-link:focus-visible {
+ text-decoration: underline;
+}
+
+/* Not background-color: currentColor here — the toggle's pill and its text
+ are two separate elements, so its highlight span can safely use
+ currentColor (the *inherited* color) while a different element's text
+ flips to --pt-bg. Collapsed into one element like this, currentColor
+ would resolve against this same rule's own `color: var(--pt-bg)` below
+ it (CSS uses the final computed color, not declaration order) — pill and
+ text would end up identical, text invisible. --pt-ink directly sidesteps
+ that self-reference entirely. */
+.pt-cheatsheet-link--active {
+ background-color: var(--pt-ink);
+ color: var(--pt-bg);
+}
+
+.pt-cheatsheet-link--active:hover,
+.pt-cheatsheet-link--active:focus-visible {
+ text-decoration: none;
}
.md-tabs {
background-color: transparent;
+ /* Extra breathing room above the tab row itself, beyond Material's
+ default (the header title sits right above it otherwise). */
+ padding-top: 0.5rem;
}
/* Make the current page's top-nav tab visually heavier than the rest, so
@@ -326,8 +494,33 @@ html:focus-within::-webkit-scrollbar-thumb {
is no .md-tabs__link--active class. */
.md-tabs__item--active .md-tabs__link {
font-weight: 700;
+ color: var(--pt-nav-active);
+}
+
+/* Inactive tabs: --pt-text-secondary instead of the plain ink they'd
+ otherwise inherit from .md-header — slightly lighter, and matching what
+ .nested-tabs__label already gets by default at desktop width (see
+ --md-nested-tabs-label-color above), so both breakpoints read the same
+ way. Light mode only (dark mode's own ink/secondary pairing is
+ unaffected). :not(.md-tabs__item--active) is required here because the
+ attribute selector makes this MORE specific than the active-tab rule
+ above (.md-tabs__item--active .md-tabs__link, two classes with no
+ attribute) — without it this would always win and overwrite the active
+ tab's color too. */
+[data-md-color-scheme="default"] .md-tabs__item:not(.md-tabs__item--active) .md-tabs__link {
+ color: var(--pt-text-secondary);
}
+/* The desktop version of this same active-parent color (above the
+ mkdocs-nested-tabs plugin's 76.234375em breakpoint, where .md-tabs is
+ hidden and replaced by that plugin's own .nested-tabs row — a separate
+ DOM tree the rule above never reaches) doesn't need a rule here at all:
+ --md-nested-tabs-label-active-color, set above, is a first-class hook the
+ plugin's own CSS applies via .nested-tabs__label--active as of 0.2.0.
+ Before that version, the plugin never marked a flat category's own label
+ active (only the child page link), so this used to need a :has() selector
+ reaching into the plugin's markup from outside — see git history. */
+
/* Material redefines --md-typeset-a-color to --md-primary-fg-color somewhere
deeper in its own cascade (our light-cream header color), which makes body
links and the active-page nav label unreadable. Override color directly
@@ -701,6 +894,36 @@ input:checked + .md-consent__settings {
}
}
+/* Same extra top breathing room as .md-tabs above, applied to the
+ mkdocs-nested-tabs plugin's own row (nested-tabs.css sets
+ .nested-tabs__list's padding to 0.5rem 0 — override padding-top only, so
+ the bottom padding it sets stays as-is). extra.css loads after
+ nested-tabs.css (see mkdocs.yml's extra_css / plugin injection order),
+ so this wins without needing extra specificity. */
+@media screen and (min-width: 76.234375em) {
+ .nested-tabs__list {
+ padding-top: 1.3rem;
+ }
+}
+
+/* The header nav row (now the mkdocs-nested-tabs plugin's own .nested-tabs
+ — see CLAUDE.md's "Planned extraction" section) covers category/page
+ navigation on desktop, so the left nav sidebar (site tree: categories +
+ sibling pages) is redundant there — hide it above the same breakpoint the
+ plugin uses. mkdocs.yml no longer sets toc.integrate, so this sidebar only
+ ever held the site tree, never the current page's own heading TOC — that
+ renders separately as the standard right-hand .md-sidebar--secondary
+ column (unaffected by this rule), same as it did before the site's earlier
+ migration to toc.integrate (see the nav history notes in CLAUDE.md).
+ Desktop-only and guarded by min-width on purpose: .md-sidebar--primary is
+ the exact same DOM element Material reuses as the off-canvas mobile
+ hamburger drawer, so an unscoped display:none here would silently break
+ mobile nav entirely (this happened once already). */
+@media screen and (min-width: 76.234375em) {
+ .md-sidebar--primary {
+ display: none;
+ }
+}
/* :visited alone (0,2,1) outranks .md-button's two plain classes (0,2,0),
so a visited link would otherwise fall through to the ".md-typeset
a:visited" accent-on-accent color above and vanish into the button's own
@@ -907,18 +1130,24 @@ input:checked + .md-consent__settings {
/* Icon for each option (Material Symbols "psychiatry" / "park", viewBox
0 -960 960 960), same mask-image technique as .pt-theme-option's
- sun/moon. On desktop it trails the "Essentials"/"Advanced" word — an
- ::after, not ::before, so it renders after the label text in normal
- document order without any extra markup or flex re-ordering; the
- option button itself is a flex row (see .pt-simplify-option above), so
+ sun/moon. On desktop it leads the "Essentials"/"Advanced" word — a
+ ::before, same as .pt-theme-option's own icon and the toast's
+ .pt-mode-icon (see below), so all three read consistently as
+ "icon, then text" — matching Material's own "back to top" button
+ (`.md-top`: icon SVG, then the "Back to top" label). The option
+ button itself is a flex row (see .pt-simplify-option above), so
the icon centers vertically against the text the same way .pt-theme-
option's icon-only button centers its own icon — no vertical-align
fudging needed, and it can't drift out of sync between the two variants.
Below ~45em (matches this file's other mobile breakpoints) the words
get tight, so the label is visually clipped and the icon alone
represents the option — it stays in the DOM (not aria-hidden) so the
- button's accessible name is unchanged for screen readers either way. */
-.pt-simplify-option[data-mode]::after,
+ button's accessible name is unchanged for screen readers either way.
+ Sized smaller (0.7rem) than the toast's own .pt-mode-icon (1.2rem,
+ matching Material's "back to top" icon) — this icon sits inside the
+ 1.2rem-tall toggle track itself, so it stays the toggle's own icon
+ size rather than the toast's. */
+.pt-simplify-option[data-mode]::before,
.pt-mode-icon {
content: "";
display: inline-block;
@@ -934,25 +1163,32 @@ input:checked + .md-consent__settings {
mask-position: center;
}
-.pt-simplify-option[data-mode]::after {
- margin-left: 0.3rem;
+.pt-simplify-option[data-mode]::before {
+ margin-right: 0.3rem;
}
-.pt-simplify-option[data-mode="simplified"]::after,
+/* Toast-only override: matches the size of Material's own "back to top"
+ icon (`.md-icon svg`, 1.2rem), per the shared rule's comment above. */
+.pt-mode-icon {
+ width: 1.2rem;
+ height: 1.2rem;
+}
+
+.pt-simplify-option[data-mode="simplified"]::before,
.pt-mode-icon[data-mode="simplified"] {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M450-130v-309h-20q-64 0-120.5-24.5T209-533q-44-45-66.5-104T120-760v-80h78.32Q260-840 317-815.5 374-791 419-746q33 34 54.5 76t30.5 89q7.65-11.9 16.82-22.95Q530-615 540-626q45-45 102-69.5T761.67-720H840v80q0 64-23.98 123T748-413q-45 45-101.56 69T528-320h-18v190h-60Zm1-370q0-61-20-113.5t-55-89q-35-36.5-86-57T180-780q0 63 18.5 115.5T252-575q42 45 90.5 60T451-500Zm59 120q60 0 111-19.5t86-56q35-36.5 54-89T780-660q-60 0-111 20.5T583-583q-43 45-58 94t-15 109Zm0 0Zm-59-120Z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M450-130v-309h-20q-64 0-120.5-24.5T209-533q-44-45-66.5-104T120-760v-80h78.32Q260-840 317-815.5 374-791 419-746q33 34 54.5 76t30.5 89q7.65-11.9 16.82-22.95Q530-615 540-626q45-45 102-69.5T761.67-720H840v80q0 64-23.98 123T748-413q-45 45-101.56 69T528-320h-18v190h-60Zm1-370q0-61-20-113.5t-55-89q-35-36.5-86-57T180-780q0 63 18.5 115.5T252-575q42 45 90.5 60T451-500Zm59 120q60 0 111-19.5t86-56q35-36.5 54-89T780-660q-60 0-111 20.5T583-583q-43 45-58 94t-15 109Zm0 0Zm-59-120Z'/%3E%3C/svg%3E");
}
-.pt-simplify-option[data-mode="advanced"]::after,
+.pt-simplify-option[data-mode="advanced"]::before,
.pt-mode-icon[data-mode="advanced"] {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M538-80H423v-149H120l189-274h-95l266-377 266 377h-94l188 274H538v149ZM236-289h189-90 290-89 189-489Zm0 0h489L536-563h89L480-769 335-563h90L236-289Z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M538-80H423v-149H120l189-274h-95l266-377 266 377h-94l188 274H538v149ZM236-289h189-90 290-89 189-489Zm0 0h489L536-563h89L480-769 335-563h90L236-289Z'/%3E%3C/svg%3E");
}
@media (max-width: 45em) {
- .pt-simplify-option[data-mode]::after {
- margin-left: 0;
+ .pt-simplify-option[data-mode]::before {
+ margin-right: 0;
}
.pt-simplify-toggle:has(.pt-simplify-option[data-mode]) .pt-simplify-option {
@@ -1033,31 +1269,32 @@ input:checked + .md-consent__settings {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q8 0 17 .5t23 1.5q-36 32-56 79t-20 99q0 90 63 153t153 63q52 0 99-18.5t79-51.5q1 12 1.5 19.5t.5 14.5q0 150-105 255T480-120Zm0-60q109 0 190-67.5T771-406q-25 11-53.67 16.5Q688.67-384 660-384q-114.69 0-195.34-80.66Q384-545.31 384-660q0-24 5-51.5t18-62.5q-98 27-162.5 109.5T180-480q0 125 87.5 212.5T480-180Zm-4-297Z'/%3E%3C/svg%3E");
}
-/* Disappearing confirmation toast for the Essentials/Advanced toggle
- (docs/javascripts/essentials_toggle.js's showToast). Sits just below the
- header — right where the toggle that triggered it lives — rather than
- at the bottom of the screen, so it's immediately next to the control the
- reader just clicked. `top` itself is set inline by showToast() (the
- header's own height varies: ~48px on mobile, taller on desktop/tablet
- with the tab bar, and Material can also hide/reveal it on scroll), only
- the offset-from-header gap lives here. Centered horizontally on both
- desktop and mobile. Colors are inverted (ink background, bg-colored
- text) same as the toggle's own active-option highlight, so it reads as
- "this site's accent chip," not a generic OS notification. */
+/* Disappearing confirmation toast for the Essentials/Advanced and
+ light/dark toggles (docs/javascripts/essentials_toggle.js's showToast).
+ Sits just below the header — right where the toggle that triggered it
+ lives — rather than at the bottom of the screen, so it's immediately
+ next to the control the reader just clicked. `top` itself is set inline
+ by showToast() (the header's own height varies: ~48px on mobile, taller
+ on desktop/tablet with the tab bar, and Material can also hide/reveal it
+ on scroll), only the offset-from-header gap lives here. Centered
+ horizontally on both desktop and mobile. Shape/colors match Material's
+ own "back to top" pill (`.md-top`) — same rounded-pill radius, shadow,
+ bg/fg pairing, font size, and padding — so it reads as a native site
+ chrome element rather than a one-off custom popup. */
.pt-toast {
position: fixed;
z-index: 1000;
left: 50%;
transform: translate(-50%, 0);
max-width: min(90vw, 22rem);
- padding: 0.6rem 1rem;
- border-radius: 0.4rem;
- background-color: var(--pt-ink);
- color: var(--pt-bg);
- font-size: 0.75rem;
+ padding: 0.4rem 0.8rem;
+ border-radius: 1.6rem;
+ background-color: var(--md-default-bg-color);
+ color: var(--md-default-fg-color--light);
+ font-size: 0.7rem;
line-height: 1.4;
text-align: center;
- box-shadow: 0 0.2rem 0.6rem rgba(0, 0, 0, 0.25);
+ box-shadow: var(--md-shadow-z2);
opacity: 0;
/* none while hidden, so an invisible toast (opacity alone doesn't take
it out of hit-testing) doesn't block clicks on whatever's under where
diff --git a/docs/types.md b/docs/types/basics.md
similarity index 98%
rename from docs/types.md
rename to docs/types/basics.md
index 7edfdd3..c093200 100644
--- a/docs/types.md
+++ b/docs/types/basics.md
@@ -444,11 +444,11 @@ Strings use the same index and slice syntax as lists. `0` is the first character
`.join()` on a list of the same pieces builds the result once, at O(n) — collect the pieces in a list through the loop, then join them after.
- See [Efficiency](style.md#efficiency) for why this distinction matters.
+ See [Efficiency](../practices/style.md#efficiency) for why this distinction matters.
-- **`print()`'s `sep` and `end` arguments** also take a string — `sep` replaces the space Python puts between multiple printed values (already covered on [Foundations](foundations.md#print-function)), and `end` replaces the newline `print()` adds after the last one, so the *next* `print()` call continues on the same line instead of starting a new one.
+- **`print()`'s `sep` and `end` arguments** also take a string — `sep` replaces the space Python puts between multiple printed values (already covered on [Foundations](../start/foundations.md#print-function)), and `end` replaces the newline `print()` adds after the last one, so the *next* `print()` call continues on the same line instead of starting a new one.
```python-ref
print("a", "b", sep="-") # "a-b"
@@ -736,7 +736,7 @@ if name: # runs — name isn't empty
## Booleans
-A boolean (`bool`) holds one of exactly two values, **`True`** or **`False`** — often used to represent yes/no, on/off, or the result of a comparison. They are used in [if statements](conditionals.md#if-elif-else) and [while loops](loops.md#while-loops).
+A boolean (`bool`) holds one of exactly two values, **`True`** or **`False`** — often used to represent yes/no, on/off, or the result of a comparison. They are used in [if statements](../flow/conditionals.md#if-elif-else) and [while loops](../flow/loops.md#while-loops).
```python-ref
venomous = False
diff --git a/docs/collections.md b/docs/types/collections.md
similarity index 93%
rename from docs/collections.md
rename to docs/types/collections.md
index 340c727..5654eb6 100644
--- a/docs/collections.md
+++ b/docs/types/collections.md
@@ -8,7 +8,7 @@ description: >-
-A **collection** is a single object that groups multiple values (like [basic types](types.md)) together and so they can be stored in one variable together and worked with as a unit.
+A **collection** is a single object that groups multiple values (like [basic types](basics.md)) together and so they can be stored in one variable together and worked with as a unit.
@@ -81,7 +81,7 @@ class diagram panel
- **Index with `list[index]`** to return the item at that index (position number) of the list.
- To **update** the item at that index, set it equal to something else **`list[index] = new_item`**. This works because a list is **mutable** — updating an item changes it in place instead of building a new one, the same way [an object's attributes](classes.md#defining-a-class) can be changed after it's created.
+ To **update** the item at that index, set it equal to something else **`list[index] = new_item`**. This works because a list is **mutable** — updating an item changes it in place instead of building a new one, the same way [an object's attributes](../organization/classes.md#defining-a-class) can be changed after it's created.
*Run the below example, and change the indexes to see how they work:*
@@ -126,7 +126,7 @@ class diagram panel
species[::2] = ["carpet", "anaconda"] # ["carpet", "rock", "anaconda", "blood"]
```
-### [Loop](loops.md#loop-through-a-collection) through a list
+### [Loop](../flow/loops.md#loop-through-a-collection) through a list
- Lists make it simple to loop directly over the items. The loop runs once for every item in the list, and on each pass the new loop variable, *(i.e. `specie`)* is set to the next item in the list.
@@ -335,7 +335,7 @@ class diagram panel
[s.title() for s in species] # ["Burmese", "Rock", "Ball", "Blood"]
```
- Swapping the brackets for parentheses turns this into a [generator expression](functions.md#generator-expressions) instead — same syntax, but it produces items one at a time rather than building the whole list up front. Use a list comprehension when the result needs indexing, `len()`, or looping over more than once; use a generator expression when it's only read once, or the full result would be too large to hold in memory as a list.
+ Swapping the brackets for parentheses turns this into a [generator expression](../organization/functions.md#generator-expressions) instead — same syntax, but it produces items one at a time rather than building the whole list up front. Use a list comprehension when the result needs indexing, `len()`, or looping over more than once; use a generator expression when it's only read once, or the full result would be too large to hold in memory as a list.
### Going further { data-card-link="skip" }
@@ -439,12 +439,12 @@ class diagram panel
??? tip "Extending lists with `collections.deque`"
A list can already add or remove items from the end cheaply, but doing the same at the
*front* — `species.insert(0, item)` or `species.pop(0)` — means Python has to shift every
- other item over. The [`collections`](libraries/collections.md) library's
- [`deque`](libraries/collections.md#deque) adds fast `appendleft()`/`popleft()` methods for
+ other item over. The [`collections`](../libraries/collections.md) library's
+ [`deque`](../libraries/collections.md#deque) adds fast `appendleft()`/`popleft()` methods for
exactly that case. Switch to it when items are being added or removed from both ends
often, like a queue of items processed in the order they arrive — not for a list that's
mostly read or only changed at the end, where a plain list is simpler and already fast.
- See the [collections library page](libraries/collections.md) for the rest of `deque`'s
+ See the [collections library page](../libraries/collections.md) for the rest of `deque`'s
methods (`rotate()`, `maxlen=`, and more) and for the other list-adjacent tools it adds.
@@ -455,9 +455,9 @@ class diagram panel
| `append()` / `pop()` | O(1) | O(1) |
| `insert(0, x)` / `pop(0)` | O(n) | O(1) |
- `append()` and `pop()` (from the end) run in constant [time](style.md#time-and-space) — one step no matter how long the list already is. `insert(0, item)` and `pop(0)` run in linear time, since Python has to shift every remaining item over.
+ `append()` and `pop()` (from the end) run in constant [time](../practices/style.md#time-and-space) — one step no matter how long the list already is. `insert(0, item)` and `pop(0)` run in linear time, since Python has to shift every remaining item over.
- See [Efficiency](style.md#efficiency) for why this distinction matters.
+ See [Efficiency](../practices/style.md#efficiency) for why this distinction matters.
??? efficiency "For efficiency, sorted() copies the list; sort() doesn't"
| | Time | Space |
@@ -469,7 +469,7 @@ class diagram panel
Reach for `sort()` when the original order doesn't need to survive; `sorted()` when it does.
- See [Efficiency](style.md#efficiency) for why this distinction matters.
+ See [Efficiency](../practices/style.md#efficiency) for why this distinction matters.
@@ -546,9 +546,9 @@ flowchart LR
| `if key in snake: snake[key]` |
O(1) (two lookups) | — |
| `snake.get(key)` |
O(1) (one lookup) | — |
- `if key in snake: value = snake[key]` does two hash lookups — one to check membership, one to fetch the value. `snake.get(key)` does the same job in one. Both are O(1), so this isn't a [Big O](style.md#big-o-notation) difference, just avoided repeated work — worth reaching for out of habit once it's familiar, not worth restructuring existing code to chase.
+ `if key in snake: value = snake[key]` does two hash lookups — one to check membership, one to fetch the value. `snake.get(key)` does the same job in one. Both are O(1), so this isn't a [Big O](../practices/style.md#big-o-notation) difference, just avoided repeated work — worth reaching for out of habit once it's familiar, not worth restructuring existing code to chase.
- See [Efficiency](style.md#efficiency) for why this distinction matters.
+ See [Efficiency](../practices/style.md#efficiency) for why this distinction matters.
@@ -713,7 +713,7 @@ flowchart LR
Looking up a key with `dict[key]` or `.get()` is O(1) — Python computes where to look directly, the same cost regardless of how many keys the dict holds. Storing the same data as a list of `(key, value)` tuples instead and searching for a match by hand is O(n) — worst case, checking every pair before finding it or coming up empty. That's the main reason to reach for a dict instead of a list when data needs to be looked up by a key.
- See [Efficiency](style.md#efficiency) for why this distinction matters.
+ See [Efficiency](../practices/style.md#efficiency) for why this distinction matters.
@@ -798,22 +798,22 @@ flowchart LR
??? tip "Extending dicts with `collections`"
A plain dict can tally counts or group items, but both take extra setup code: checking
whether a key exists before incrementing it, or before appending to a list under it. The
- [`collections`](libraries/collections.md) library adds several dicts that handle cases
+ [`collections`](../libraries/collections.md) library adds several dicts that handle cases
like these automatically.
- - [`Counter`](libraries/collections.md#counter) counts items in a sequence directly —
+ - [`Counter`](../libraries/collections.md#counter) counts items in a sequence directly —
reach for it as soon as a dict's job is "how many times does each item show up."
- - [`defaultdict`](libraries/collections.md#defaultdict) supplies an empty value (a list,
+ - [`defaultdict`](../libraries/collections.md#defaultdict) supplies an empty value (a list,
a set, `0`) the first time a new key is used, so grouping items under keys that aren't
known ahead of time doesn't need an `if key not in dict` check before every write.
- - [`OrderedDict`](libraries/collections.md#ordereddict) is worth reaching for only when
+ - [`OrderedDict`](../libraries/collections.md#ordereddict) is worth reaching for only when
order itself needs to be compared or reordered — a plain dict already remembers
insertion order, but its `==` ignores that order, and it has no `move_to_end()`.
- - [`ChainMap`](libraries/collections.md#chainmap) layers several dicts together — like a
+ - [`ChainMap`](../libraries/collections.md#chainmap) layers several dicts together — like a
set of overrides checked before a set of defaults — without copying or merging them
into a new dict.
- See the [collections library page](libraries/collections.md) for the full method list on
+ See the [collections library page](../libraries/collections.md) for the full method list on
each of these.
@@ -870,7 +870,7 @@ The **negative index** starts counting down from the end instead, starting at `-
### Loop through a tuple
-- The [loop](loops.md#loop-through-a-collection) runs once for every item in the tuple, and on each pass the loop variable, *(i.e. `specie`)* is set to the next item in the tuple.
+- The [loop](../flow/loops.md#loop-through-a-collection) runs once for every item in the tuple, and on each pass the loop variable, *(i.e. `specie`)* is set to the next item in the tuple.
```python-ref
for specie in species:
@@ -943,7 +943,7 @@ The **negative index** starts counting down from the end instead, starting at `-
a, b, c, d = species # a="burmese" b="rock" c="ball" d="blood"
```
- A [`match` statement](conditionals.md#unpacking-a-tuple) can do this same unpacking while also branching on the tuple's shape or specific values.
+ A [`match` statement](../flow/conditionals.md#unpacking-a-tuple) can do this same unpacking while also branching on the tuple's shape or specific values.
```python-ref
snake = (12, "ball")
@@ -1080,13 +1080,13 @@ The **negative index** starts counting down from the end instead, starting at `-
??? tip "Extending tuples with `collections.namedtuple`"
A plain tuple's items can only be accessed by position — `snake[1]` doesn't say what
that value actually means without checking back how the tuple was built. The
- [`collections`](libraries/collections.md) library's
- [`namedtuple`](libraries/collections.md#namedtuple) builds a tuple type with named fields,
+ [`collections`](../libraries/collections.md) library's
+ [`namedtuple`](../libraries/collections.md#namedtuple) builds a tuple type with named fields,
so the same value reads as `snake.length_ft`. Switch to it once a tuple's positions start
needing a mental lookup table to remember, or once several tuples share the same shape
throughout a program — a single `namedtuple` definition documents that shape once instead
of repeating a comment at every literal.
- See the [collections library page](libraries/collections.md) for `namedtuple`'s other
+ See the [collections library page](../libraries/collections.md) for `namedtuple`'s other
methods (`_asdict()`, `_replace()`, default field values) and the rest of the module.
@@ -1124,7 +1124,7 @@ block-beta
### Loop through a set
-The [loop](loops.md#loop-through-a-collection) runs once for every item in the set, in no guaranteed order, and on each pass the loop variable, *(i.e. `specie`)* is set to the next item.
+The [loop](../flow/loops.md#loop-through-a-collection) runs once for every item in the set, in no guaranteed order, and on each pass the loop variable, *(i.e. `specie`)* is set to the next item.
```python-ref
for specie in species:
@@ -1339,7 +1339,7 @@ These check a relationship between two sets and hand back a `bool`, rather than
Checking `in` on a list or tuple is O(n) — worst case, Python has to look at every item before it can say no. A set (and a dict, checking its keys) looks a value up directly instead of scanning, so `in` on either is O(1) on average, regardless of size. That's the "far faster" mentioned above, named precisely — it's also the reason converting a list to a set is a common move before doing a lot of membership checks against it.
- See [Efficiency](style.md#efficiency) for why this distinction matters.
+ See [Efficiency](../practices/style.md#efficiency) for why this distinction matters.
diff --git a/mkdocs.yml b/mkdocs.yml
index 2c364c1..be0409c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -7,20 +7,33 @@ site_description: >-
hooks:
- hooks/thanks_url.py
+plugins:
+ # Setting `plugins:` at all replaces MkDocs' implicit default list, which
+ # otherwise includes `search` automatically — has to be listed explicitly
+ # here or the site silently loses its search feature.
+ - search
+ - nested-tabs
+
nav:
- All: index.md
- - Workspace: workspace.md
- - Foundations: foundations.md
- - Types: types.md
- - Collections: collections.md
- - Conditionals: conditionals.md
- - Loops: loops.md
- - Functions: functions.md
- - Classes: classes.md
- - Modules: modules.md
- - Files: files.md
- - Style: style.md
- - Errors: errors.md
+ - Start:
+ - Workspace: start/workspace.md
+ - Foundations: start/foundations.md
+ - Types:
+ - Basics: types/basics.md
+ - Collections: types/collections.md
+ - Flow:
+ - Conditionals: flow/conditionals.md
+ - Loops: flow/loops.md
+ - Organization:
+ - Functions: organization/functions.md
+ - Classes: organization/classes.md
+ - Resources:
+ - Modules: resources/modules.md
+ - Files: resources/files.md
+ - Practices:
+ - Style: practices/style.md
+ - Errors: practices/errors.md
- Libraries:
- All: libraries/index.md
- Utilities:
@@ -73,15 +86,14 @@ theme:
icon: material/weather-night
name: Switch to dark mode
features:
- - navigation.expand
- navigation.footer
- navigation.instant
- navigation.instant.prefetch
+ - navigation.sections
- navigation.tabs
- navigation.tabs.sticky
- navigation.top
- navigation.tracking
- - toc.integrate
- toc.follow
- content.code.copy
- content.tooltips
@@ -143,6 +155,7 @@ extra_javascript:
- javascripts/copyright_year.js
- javascripts/external_links.js
- javascripts/homepage_header_title.js
+ - javascripts/cheatsheet_button.js
- javascripts/essentials_toggle.js
- javascripts/a11y_patches.js
- https://unpkg.com/mermaid@11/dist/mermaid.min.js
diff --git a/requirements.txt b/requirements.txt
index 0b6b41c..72a82d9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,8 @@
mkdocs
mkdocs-material
+# Extracted nested-tabs header row — see CLAUDE.md's "Planned extraction"
+# section. Published to PyPI; was a local editable install before 2026-09-23.
+mkdocs-nested-tabs==0.2.0
pytest
playwright
pytest-playwright
diff --git a/tests/test_accessibility_browser.py b/tests/test_accessibility_browser.py
index 827b892..4c8a7a3 100644
--- a/tests/test_accessibility_browser.py
+++ b/tests/test_accessibility_browser.py
@@ -20,12 +20,12 @@
# FAQ tabs), a content page with a wide comparison table, a page built from numbered
# walkthroughs and tabbed OS instructions, one dense with admonitions, and a library page
# full of images.
-PAGES = ["/", "/types/", "/workspace/", "/collections/", "/libraries/pillow/"]
+PAGES = ["/", "/types/basics/", "/start/workspace/", "/types/collections/", "/libraries/pillow/"]
# The pages whose palette does the most work — card grid, wide truth tables — re-checked
# with the dark scheme active. (Material lists `slate` first, so dark is already the
# default the PAGES run above scans; this forces the *other* direction explicitly too.)
-LIGHT_MODE_PAGES = ["/", "/collections/"]
+LIGHT_MODE_PAGES = ["/", "/types/collections/"]
MOBILE_VIEWPORT = {"width": 375, "height": 812}
# Between Material's own tab-bar breakpoint (~1220px) and extra.css's override that pulls
@@ -75,7 +75,7 @@ def test_homepage_has_no_axe_violations_in_dark_mode(page, site_url):
)
-@pytest.mark.parametrize("path", ["/", "/collections/"])
+@pytest.mark.parametrize("path", ["/", "/types/collections/"])
def test_page_has_no_axe_violations_on_mobile(page, site_url, path):
page.set_viewport_size(MOBILE_VIEWPORT)
page.goto(f"{site_url}{path}")
@@ -89,7 +89,7 @@ def test_page_has_no_axe_violations_on_mobile(page, site_url, path):
def test_mobile_nav_drawer_has_no_axe_violations(page, site_url):
"""The hamburger drawer is a different DOM subtree than the desktop tab nav."""
page.set_viewport_size(MOBILE_VIEWPORT)
- page.goto(f"{site_url}/collections/")
+ page.goto(f"{site_url}/types/collections/")
page.evaluate(
"""() => {
const drawer = document.getElementById('__drawer');
@@ -106,7 +106,7 @@ def test_mobile_nav_drawer_has_no_axe_violations(page, site_url):
def test_tablet_width_has_no_axe_violations(page, site_url):
page.set_viewport_size(TABLET_VIEWPORT)
- page.goto(f"{site_url}/types/")
+ page.goto(f"{site_url}/types/basics/")
violations = run_axe(page)
assert not violations, (
f"axe-core violations at {TABLET_VIEWPORT['width']}px (custom tab-bar breakpoint):\n"
diff --git a/tests/test_accessibility_keyboard.py b/tests/test_accessibility_keyboard.py
index 8bc21ac..2935acb 100644
--- a/tests/test_accessibility_keyboard.py
+++ b/tests/test_accessibility_keyboard.py
@@ -10,7 +10,7 @@
import pytest
-CHECK_PAGES = ["/", "/types/", "/workspace/"]
+CHECK_PAGES = ["/", "/types/basics/", "/start/workspace/"]
@pytest.mark.parametrize("path", CHECK_PAGES)
@@ -32,7 +32,7 @@ def test_first_tab_reaches_the_skip_link(page, site_url, path):
def test_skip_link_moves_past_the_navigation(page, site_url):
- page.goto(f"{site_url}/types/")
+ page.goto(f"{site_url}/types/basics/")
page.keyboard.press("Tab") # focus skip link
page.keyboard.press("Enter") # activate it
moved = page.evaluate(
@@ -95,9 +95,9 @@ def test_palette_toggle_is_keyboard_reachable(page, site_url):
# (`.md-button` is only used on 404.md and is already covered statically by
# test_accessibility.py's `.md-button:focus-visible` check, so it's not repeated here.)
REPO_STYLED_CONTROLS = [
- ("/foundations/", ".md-content a[href]"),
- ("/foundations/", ".pyodide-runner__run-btn"),
- ("/foundations/", "details.run > summary"),
+ ("/start/foundations/", ".md-content a[href]"),
+ ("/start/foundations/", ".pyodide-runner__run-btn"),
+ ("/start/foundations/", "details.run > summary"),
("/", ".grid.cards a[href]"),
]
diff --git a/tests/test_accessibility_runnable.py b/tests/test_accessibility_runnable.py
index e3b7418..69ca753 100644
--- a/tests/test_accessibility_runnable.py
+++ b/tests/test_accessibility_runnable.py
@@ -13,7 +13,7 @@
from conftest import format_violations, run_axe
# A core content page that carries several ```python runnable blocks.
-RUNNABLE_PAGE = "/foundations/"
+RUNNABLE_PAGE = "/start/foundations/"
def _open(page, site_url):
diff --git a/tests/test_essentials_toggle.py b/tests/test_essentials_toggle.py
index 9752dd6..e1fde94 100644
--- a/tests/test_essentials_toggle.py
+++ b/tests/test_essentials_toggle.py
@@ -45,7 +45,7 @@ def test_simplified_state_carries_to_content_page_heading_and_toc(page, site_url
"""functions.md's own '## Decorators { data-advanced="true" }' heading (and its
integrated-TOC entry) should hide too — carried over from the homepage's marker via
localStorage, with no need to visit the homepage first in this same test."""
- page.goto(f"{site_url}/functions/?simplified=true")
+ page.goto(f"{site_url}/organization/functions/?simplified=true")
result = page.evaluate(
"""() => {
@@ -68,7 +68,7 @@ def test_admonition_inside_a_hidden_section_is_actually_hidden(page, site_url):
on an admonition inside a hidden section didn't actually hide it — it stayed on
screen as a bordered box even though its heading was gone. Fixed with a blanket
`[hidden] { display: none !important }` in extra.css."""
- page.goto(f"{site_url}/collections/?simplified=true")
+ page.goto(f"{site_url}/types/collections/?simplified=true")
hidden_and_shown = page.evaluate(
"""() => [...document.querySelectorAll('.md-typeset details')]
@@ -88,7 +88,7 @@ def test_pfg_section_wrapper_is_hidden_with_its_heading(page, site_url):
heading and its flow siblings, which sit *inside* that wrapper — the wrapper
itself was never touched, so it stayed on screen as an empty bordered card
once everything inside it was hidden."""
- page.goto(f"{site_url}/collections/?simplified=true")
+ page.goto(f"{site_url}/types/collections/?simplified=true")
result = page.evaluate(
"""() => {
@@ -111,7 +111,7 @@ def test_link_to_hidden_section_recovers_to_advanced(page, site_url):
while the "Tuples" heading itself is hidden by data-advanced — clicking that visible
link should flip the toggle back to Advanced and reveal the section, rather than
landing on a hidden target and doing nothing."""
- page.goto(f"{site_url}/collections/?simplified=true")
+ page.goto(f"{site_url}/types/collections/?simplified=true")
tuples_link = page.locator('table a[href$="#tuples"]')
assert tuples_link.count() > 0, "expected the cheat-sheet table's #tuples link to exist"
@@ -144,9 +144,13 @@ def test_link_to_hidden_section_recovers_to_advanced(page, site_url):
def test_link_recovery_ignores_toc_links_to_visible_sections(page, site_url):
"""Sanity check the recovery handler isn't overly broad: clicking an ordinary link to
a section that's already visible shouldn't touch Simplify state at all."""
- page.goto(f"{site_url}/collections/?simplified=true")
+ page.goto(f"{site_url}/types/collections/?simplified=true")
- lists_link = page.locator('a.md-nav__link[href$="#lists"]')
+ # Material renders a page's TOC twice: once for real in the secondary
+ # (right-hand) sidebar, and once inert (visibility:collapse) inside the
+ # primary nav's copy of the current page's entry — scope to the visible
+ # one so .first doesn't land on the inert copy and time out.
+ lists_link = page.locator('.md-sidebar--secondary a.md-nav__link[href$="#lists"]')
assert lists_link.count() > 0
lists_link.first.click()
diff --git a/tests/test_structure.py b/tests/test_structure.py
index ba470ea..38c1ef1 100644
--- a/tests/test_structure.py
+++ b/tests/test_structure.py
@@ -196,7 +196,7 @@ def test_python_is_capitalized_in_prose(path):
# pages that are the clearest, most literal examples of the documented convention. Extending it
# to more pages would need either a markup convention to mark "this block follows the cheat
# sheet rule" or a per-page editorial pass — worth doing, but a separate task from this suite.
-CHECKED_PAGES_FOR_PYTHON_REF_COMMENTS = {"types.md", "collections.md"}
+CHECKED_PAGES_FOR_PYTHON_REF_COMMENTS = {"types/basics.md", "types/collections.md"}
_CONTROL_FLOW_RE = re.compile(
r"^(if |elif |else|while |for |def |class |try|except|finally|with |return|import |from |@|match |case )"