From f20c8b53e6e84ca324640c4dbc83799368a4a81c Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Fri, 11 Sep 2026 18:53:19 -0700 Subject: [PATCH 01/10] rewrite errors page --- docs/errors.md | 413 +++++++++++++++++++++++++++++-------------------- 1 file changed, 243 insertions(+), 170 deletions(-) diff --git a/docs/errors.md b/docs/errors.md index c772cb2..0bad72d 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -6,33 +6,145 @@ description: >- # :material-bug-outline:{ .lg .middle } Errors -Your code had an error or didn't do what you expected — here are tools for figuring out why. +**Errors** occur when a line of code is impossible to run, so the program stops and displays an error message with information on what went wrong and where. + +**"Bugs"** are the general term for *any* mistake or error in your code, like logic errors. + +They are part of programming, and will happen constantly. Once you know what kind of error you have, you can check for common causes, and try one of these ways of fixing it: + +
+ +| | [Read traceback/error](#reading-a-traceback) | [`try`/`except`](#catch-with-tryexcept) | [Debugging strategies](#debugging-strategies) | [Debugger tool](#debugger-tool) | [Testing](#detect-errors-with-testing) | +|----------|:---:|:---:|:---:|:---:|:---:| +| [Syntax errors](#syntax-errors) | :material-check:{ .pt-icon-success } | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | +| [Runtime errors](#runtime-errors) | :material-check:{ .pt-icon-success } | :material-check:{ .pt-icon-success } | :material-check:{ .pt-icon-success } | :material-check:{ .pt-icon-success } | :material-check:{ .pt-icon-success } | +| [Logic errors](#logic-errors) | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | :material-check:{ .pt-icon-success } | :material-check:{ .pt-icon-success } | :material-check:{ .pt-icon-success } | + +
+ +## Kinds of errors: + +
+ +### Syntax errors { .pt-fake-h2 } + +The code doesn't follow Python's grammar rules, so it can't read or run the file. These errors must be fixed directly. + +
+ +| | [Read error message](#reading-a-syntax-error-message) | [`try`/`except`](#catch-with-tryexcept) | [Debugging strategies](#debugging-strategies) | [Debugger tool](#debugger-tool) | [Testing](#detect-errors-with-testing) | +|---|:---:|:---:|:---:|:---:|:---:| +| Ways to fix syntax errors | :material-check:{ .pt-icon-success }
Points to where Python couldn't understand | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | + +
+ +
+ +| Kind of syntax error | Happens when | Check for | +|-------|---------------|-----------| +| **`IndentationError`** | Incorrect indentation |
  • Code pasted from somewhere else, with indentation that doesn't match the rest of the file.
| +| **`SyntaxError`** | The code isn't valid Python |
  • A missing colon after `if`, `for`, `while`, `def`, `class` etc.
  • An unclosed parenthesis, bracket, or quote.
  • A line that ends before the expression on it is complete, like a trailing `+`.
  • Copying old Python 2 code, like `print "hello"` without parentheses — Python 3 requires `print("hello")`.
  • Curly "smart quotes" (`'` `'` `"` `"`) instead of straight ones — copying code out of a Word doc, PDF, or a web page can silently swap them in; Python only recognizes straight quotes. Retype them if this happens.
  • A non-breaking space instead of a regular one, from that same kind of copy-paste — also has to be retyped.
  • Writing `if x = 5:` instead of `if x == 5:`
  • Naming a variable after a reserved keyword, like `class` or `pass`.
| +| **`TabError`** | Tabs and spaces mixed in the same block of indentation |
  • Pick one (spaces is Python's convention) and set your editor to use it everywhere — many editors can auto-convert existing tabs to spaces.
  • Code pasted in from a source using the other kind of indentation than the rest of the file.
| + +
+ +
+ +
+ +### Runtime errors { .pt-fake-h2 } + +A **runtime error** happens once a program is already running — the code is grammatically correct, but unable to execute. + +Think about what programming concepts the failing line is using (data type, loop, conditional, etc), and revisit that page on this site to confirm you're applying it correctly. + +
+ +| | [Read traceback message](#reading-a-traceback) | [`try`/`except`](#catch-with-tryexcept) | [Debugging strategies](#debugging-strategies) | [Debugger tool](#debugger-tool) | [Testing](#detect-errors-with-testing) | +|---|:---:|:---:|:---:|:---:|:---:| +| Ways to fix runtime errors | :material-check:{ .pt-icon-success }
Tells you exactly where it broke | :material-check:{ .pt-icon-success }
Use when the failure is expected and outside your control | :material-check:{ .pt-icon-success }
Figure out why it failed | :material-check:{ .pt-icon-success }
Step through the code to see exactly what's happening | :material-check:{ .pt-icon-success }
Lock in the fix to prevent it from happening again | + +
+ +
+ +| Kind of runtime error | Happens when | Check for | +|-------|---------------|-----------| +| **`AttributeError`** | Calling a method or attribute that doesn't exist on that object |
  • Typo in a method name.
  • A method that is being called on the wrong type: `"ball".append(...)` fails since `.append()` can only be applied to `list` not `str`.
  • A variable holds `None` instead of the object you meant to call a method on — often a function returned None instead of an expected value.
  • Reassigning a variable to the result of an in-place list method like `.append()` or `.sort()` — those return `None`, not the changed list.
  • A local file named the same as a library you import, see [file naming rules](workspace.md#step-2-write-and-run-a-python-file).
| +| **`FileNotFoundError`** | Trying to open a file that doesn't exist at that path |
  • A typo in the filename, path, extension, or case. Open your file browser (Finder/ File Explorer) and check directly.
  • The path is relative to your current working directory
  • Meant to create a file but opened in read mode `"r"` instead of write mode `"w"` which creates the file if it doesn't exist.
| +| **`ImportError`** | Importing a name that doesn't exist in a module that *was* found |
  • The module itself was found, but `from module import name` is asking for something that doesn't exist inside it — a typo in `name`.
  • The name exists, but in a different module than the one you're importing it from.
  • A local file named the same as a library you import, see [file naming rules](workspace.md#step-2-write-and-run-a-python-file).
  • Two of your own files importing from each other — restructure so one of them doesn't need to import the other, often by moving the shared piece into a third file.
| +| **`IndexError`** | Looking up an index that doesn't exist — in a list, tuple, or string |
  • Off-by-one — `len(items)` is one past the last valid index; the last item is `items[len(items) - 1]`.
  • The sequence is shorter than you assumed.
  • Modifying a list while looping over it, so its length changes mid-loop.
| +| **`KeyError`** | Looking up a dict key that doesn't exist |
  • The key doesn't exist — check spelling and capitalization against how the [dict](collections.md#dictionaries) was actually built.
  • Use `.get(key)` instead of `[key]` when a missing key is expected, so you get `None` back instead of a crash.
| +| **`ModuleNotFoundError`** | Importing a module that can't be found|
  • The library isn't installed — `pip install` it into the same environment you're running from.
  • A typo in the module name.
| +| **`NameError`** | Using a variable that hasn't been assigned yet |
  • Misspelled or mis-cased — Python is case-sensitive, `species` and `Species` are different names.
  • Used before the line that assigns it — Python reads top to bottom, so the assignment has to come first.
  • Assigned inside a function, but used outside it — see [local vs global variables](functions.md#local-vs-global-variables).
| +| **`RecursionError`** | A function calls itself too many times without ever reaching a base case |
  • No [base case](functions.md#recursion) — the function has no condition that ever stops it from calling itself.
  • A base case exists, but is unreachable — check that each recursive call actually moves closer to it (the argument shrinks, or grows, toward it every time).
  • The recursion is correct, but genuinely deep — Python's default limit is around 1000 calls. Rewriting as a loop is usually the better fix.
| +| **`TypeError`** | Using a value the wrong way for its type, or calling a function with the wrong number of arguments |
  • Combining a `str` with a number, like `"length: " + 4.5` — convert first with [`str()`](types.md#convert_2).
  • A variable is `None` where you expected a real value — often a function that fell through without hitting a [`return`](functions.md#return-values).
  • Called a function without all of its required arguments.
  • Missing parentheses on a call — `len` (the function object) instead of `len(species)` (the result), then trying to use it like a number or string.
  • Trying to change a character in a string directly (`name[0] = "X"`) — strings are immutable; build a new string instead.
  • Sorting, or comparing with `<` or `>`, a [list](collections.md#lists) of mixed, incompatible types, like `[3, "burmese"]`.
  • Using a mutable collection (`list`, `dict`, or `set`) as a [dict](collections.md#dictionaries) key or a [set](collections.md#sets) item.
| +| **`UnboundLocalError`** | A local variable used before it's assigned |
  • Assigned to that name *later* in the function — Python then treats it as local for the whole function body, so reading it earlier fails instead of falling back to a variable of the same name outside. See [local vs global variables](functions.md#local-vs-global-variables).
  • If you actually meant to change the outer variable, add `global name` (or rename the local one).
| +| **`ValueError`** | The argument is the right *type*, but not a valid *value* for what's being done with it |
  • Converting a string to a number, but its text doesn't actually look like one — `int("four")` or `float("")`.
  • Unpacking the wrong number of values, like `a, b = 1, 2, 3`.
| +| **`ZeroDivisionError`** | Dividing by zero |
  • The divisor could be a variable, not a literal `0` (often a count or length that turned out empty).
  • Guard with `if divisor != 0:` before dividing, if zero is a value you actually expect sometimes.
| + +
+ +
-## Reading errors +### Logic errors { .pt-fake-h2 } + +A logic error is a bug Python doesn't notice, it finishes running but gives you an **unexpected result** because the reasoning itself was **inaccurate**. + +Think about what programming concepts you are using (data types, loops, conditionals, etc.) and revist those pages on this site to confirm you're applying them correctly. + +
+ +| | [Read traceback/error](#reading-a-traceback) | [`try`/`except`](#catch-with-tryexcept) | [Debugging strategies](#debugging-strategies) | [Debugger tool](#debugger-tool) | [Testing](#detect-errors-with-testing) | +|---|:---:|:---:|:---:|:---:|:---:| +| Ways to fix logic errors? | :material-close:{ .pt-icon-fail }
No error message is shown | :material-close:{ .pt-icon-fail }
No error is raised | :material-check:{ .pt-icon-success }
Helps you find exactly where the code's behavior diverges from what you expected | :material-check:{ .pt-icon-success }
Especially useful, since there's no error message to point you anywhere | :material-check:{ .pt-icon-success }
State your expected output, so the mistake gets caught automatically next time | -An **error** is Python's way of telling you it couldn't do what your code asked — a typo it can't parse, a variable that doesn't exist, dividing by zero, and so on. When Python hits one, it stops the program right there. That's not a sign you've broken something unrecoverable — it's Python pointing at the exact spot to look. +
+ +
-Errors are part of programming, and will happen constantly. +## Fixing errors: -### How to read a traceback +
+ +### Reading a syntax error message { .pt-fake-h2 } Red text instead of your expected output? Here's how to read it. ```python-ref -Traceback (most recent call last): - File "hello.py", line 2, in -NameError: name 'name' is not defined + File "hello.py", line 1 + if True + ^ +SyntaxError: expected ':' ``` -Errors in Python show up as a **traceback** — don't be intimidated by the wall of text. Read it from the **bottom up**: +The code never ran, but Python still points at the problem. Read it from the **bottom up**: -- The **last line** tells you the type of error and a short description (e.g. `NameError: name 'name' is not defined`) — this is usually the most useful part. -- The line just above it tells you the **file and line number** where the error happened, so you know exactly where to look in your code. +- The **last line** names the problem (`SyntaxError: expected ':'`) — this is usually the most useful part. +- The line above it points at the **file and line number**, with a `^` marking roughly where Python gave up. Fix the issue there, save, and run again. Errors are a normal part of writing code — even experienced programmers see them constantly. +That pointer isn't always exactly where the mistake is — an unclosed bracket or quote, for example, can get reported many lines later, once Python finally runs out of file without finding the closing character. See [Isolate the problem](#isolate-the-problem) for narrowing down a case like that. + +
+ +
+ +### Reading a traceback { .pt-fake-h2 } + +A runtime error follows the same bottom-up pattern — but since the program actually started running, Python can show a full **traceback**: don't be intimidated by the wall of text. + +```python-ref +Traceback (most recent call last): + File "hello.py", line 2, in +NameError: name 'name' is not defined +``` + +The **last line** and the **file and line** above it still matter most, same as before. + **Longer tracebacks** show one `File` line per function call involved — your code calling a function, which calls another function, and so on. Keep reading bottom to top: the first `File` line naming *your own file* (not a library you imported) is almost always the one worth looking at — the frames above it are usually just the library code that was doing what your code asked, not the actual source of the bug. ```python-ref @@ -47,138 +159,91 @@ IndexError: list index out of range
-## Handling errors +### Catch with try/except { .pt-fake-h2 } -`try`/`except` lets your program handle an error instead of crashing. +`try`/`except` lets your program handle [runtime errors](#runtime-errors) and then continue without crashing. ```python-ref try: - [run this block of code first] -except [specific error]: - [if the above block failed due to the specific error named in the except, then run this code] + [run this block of code first] # only the line(s) that could cause the error +except [error name]: # i.e. KeyError, ValueError, etc + [if the try block caused the specified error, then continue and run this code] ``` +#### When to use it { .pt-fake-h3 } + !!! success "Handle it with try/except" - - A failure that's expected and outside your control, even when the code is correct - - Catch the specific exception type you expect, not a bare `except:` - - Keep the `try` block small — only the line that can actually fail + - The failure is genuinely outside your control — a file that might not exist, a network call, user input you can't fully validate ahead of time + - The failure is an expected, normal outcome — not a mistake + - You have real alternative logic to run instead, like a fallback value or a retry — not just silencing the error !!! danger "Fix the code instead" - - Typos, bugs, incorrect logic - - Catching several unrelated exception types just to make errors stop — often a sign one of them is actually a bug + - You don't know what is causing the error + - It is in your control to fix the error + - Just wanting to make errors stop — often a sign there is a bug -An **exception** is Python's formal name for the type of error that was raised — `KeyError`, `ValueError`, and so on are all exceptions, and it's the technical term for what `except` actually matches against. Matching `except` to a specific exception type (rather than catching everything) means your program only handles the failure you actually expected, and still crashes loudly on a genuine bug — which is usually what you want while learning. +#### Catching multiple exceptions { .pt-fake-h3 } -```python-ref -lengths = {"ball python": 4.5, "burmese python": 12} +List several exception types in one `except` to handle them the same way. Separate `except` blocks work too, if each error type needs different handling — Python checks them top to bottom and runs the first one that matches. +```python-ref try: - print(lengths["reticulated python"]) -except KeyError: - print("no length on record for that species") + length = float(lengths[species]) +except (KeyError, TypeError): # handle these the same way + print("couldn't look up that species") +except ValueError: # separate for different handling + print("length on record isn't a number") ``` -### Common exception types +#### Optional else and finally blocks { .pt-fake-h3 } -The name in the traceback's last line tells you which of these went wrong. +`else` runs only if `try` succeeded, but it won't trigger the `except` block. This isn't commonly used. -**Runtime errors** — valid Python that fails only once that specific line actually executes. These are the ones `except` can catch. +`finally` always runs, and is for cleanup that has to happen either way, like closing a file. -| Exception | Happens when | -|-----------|---------------| -| `NameError` | Using a variable that hasn't been assigned yet | -| `TypeError` | Using a value the wrong way for its type, or calling a function with the wrong number of arguments | -| `ValueError` | The type is right, but the value doesn't make sense — `int("banana")`, or unpacking the wrong number of values (`a, b = 1, 2, 3`) | -| `KeyError` | Looking up a dict key that doesn't exist | -| `IndexError` | Looking up a list index that doesn't exist | -| `ZeroDivisionError` | Dividing by zero | -| `AttributeError` | Calling a method that doesn't exist on that object — often a typo, or calling a method on `None` | -| `FileNotFoundError` | Trying to open a file that doesn't exist at that path | -| `ImportError` (or `ModuleNotFoundError`) | Importing something that doesn't exist, or a library that isn't installed | -**Parse-time errors** — Python can't even finish reading the file, so nothing runs at all. `except` can't catch this — it has to be fixed. - -| Exception | Happens when | -|-----------|---------------| -| `SyntaxError` | The code isn't valid Python at all — a missing colon, mismatched parentheses | -| `IndentationError` | A specific kind of `SyntaxError` for inconsistent or incorrect indentation | - -### Going further { data-card-link="skip" } - -??? tip "Catching multiple exceptions" - List several exception types in one `except` to handle them the same way. Separate `except` blocks work too, if each error type needs different handling — Python checks them top to bottom and runs the first one that matches. - - ```python-ref - try: - print(lengths[species]) - except (KeyError, TypeError): - print("couldn't look up that species") - ``` - -??? tip "else and finally" - `else` runs only if `try` succeeded; `finally` always runs, whether it succeeded or not. `else` is a good place for code that should only run after a successful `try`, without risking it accidentally triggering the `except` block itself. `finally` is for cleanup that has to happen either way, like closing a file — it runs even if the `try` block succeeded, failed, or the `except` block itself raised a new error. - - ```python-ref - try: - length = lengths[species] # attempted first - except KeyError: - print("no length on record") # runs only on a KeyError - else: - print(f"found it: {length} ft") # runs only if try succeeded - finally: - print("lookup attempt finished") # always runs - ``` +```python-ref +try: + length = lengths[species] # attempted first +except KeyError: + print("no length on record") # runs only on a KeyError +else: + print(f"found it: {length} ft") # runs only if try succeeded +finally: + print("lookup attempt finished") # always runs +``` ??? run "Run a try/except example" - All the examples above, combined into one script: + A case where try/except is the right tool — converting a value that might not be a valid number: ```python - lengths = {"ball python": 4.5, "burmese python": 12} + raw_length = "n/a" - try: - print(lengths["reticulated python"]) - except KeyError: - print("no length on record for that species") - - lengths = {"ball python": 4.5, "burmese python": 12} - species = "reticulated python" + print("trying to read the length") try: - print(lengths[species]) - except KeyError: - print("no length on record for that species") - except TypeError: - print("species should be text, not a number") - - lengths = {"ball python": 4.5, "burmese python": 12} - species = "ball python" - - try: - length = lengths[species] # attempted first - except KeyError: - print("no length on record") # runs only if the try block raised a KeyError - else: - print(f"found it: {length} ft") # runs only if the try block succeeded - finally: - print("lookup attempt finished") # always runs, no matter what happened above + length = float(raw_length) + print(f"length: {length} ft") + except ValueError: + print(f"couldn't read '{raw_length}' as a number") ```
-## Debugging strategies +### Debugging strategies { .pt-fake-h2 } -The traceback tells you exactly where Python broke. When that's not enough — or the code runs to completion but the output is just wrong, so there's no traceback at all — these general techniques help close the gap between what you think the code does and what it's actually doing. +These general techniques help close the gap between what you think the code does and what it's actually doing. -### Read it out loud +#### Read it out loud { .pt-fake-h3 } ```python-ref for length in lengths: # "for each length in lengths" — but a dict hands back its keys if length > 6: # so `length` is actually a species name here, not a number ``` -Read your code line by line, out loud, saying in plain English what each line does and why — to a rubber duck, a pet, or just the room. This is often called **rubber duck debugging**: putting each line into words forces you to state assumptions you'd otherwise skim past while reading silently. Say "for each length in lengths" out loud and the mismatch jumps out — looping directly over a dict hands back its keys, not its values, so `length` here is actually a species name like `"ball python"`. That's exactly what raises the error below: you can't compare a string to `6`. +Read your code line by line, out loud, saying in plain English what each line does and why. This is often called **rubber duck debugging**: putting each line into words forces you to state assumptions you'd otherwise skim past while reading silently. Say "for each length in lengths" out loud and the mismatch jumps out — looping directly over a dict hands back its keys, not its values, so `length` here is actually a species name like `"ball python"`. That's exactly what raises the error below: you can't compare a string to `6`. ```python-ref Traceback (most recent call last): @@ -188,7 +253,7 @@ TypeError: '>' not supported between instances of 'str' and 'int' The fix follows straight from the narration — loop over `lengths.values()` instead. -### Print debugging +#### Print debugging { .pt-fake-h3 } ```python-ref print(type(length), length) # confirm what a value actually is, not what you assumed it was @@ -196,11 +261,11 @@ print(type(length), length) # confirm what a value actually is, not what you a Sprinkle `print()` calls between the lines you suspect, showing a variable's value (and [`type()`](types.md), if you're not sure) at that exact point in the run. This narrows down *where* your assumption about the code stopped matching reality — especially useful when nothing crashes and you're just staring at a wrong final answer, so there's no traceback pointing anywhere. Delete the `print()` calls once you've found the problem — they're a diagnostic, not part of the program. -### Isolate the problem +#### Isolate the problem { .pt-fake-h3 } Comment out or delete code until the smallest version that still shows the bug is left, then add pieces back one at a time until it reappears — whatever you just added back is the culprit. Especially useful in a long script, where the traceback's line number is buried inside a function calling a function calling a function — cutting the problem down to a few lines removes everything that isn't actually relevant. -### Flag it with TODO/FIXME +#### Flag as TODO/FIXME { .pt-fake-h3 } ```python-ref # TODO: handle the case where length_ft is negative @@ -211,97 +276,105 @@ Not every problem gets fixed the moment you spot it — sometimes you're mid-deb Some editors collect every `TODO`/`FIXME` in a project into one scannable list — PyCharm has a built-in TODO tool window (**View → Tool Windows → TODO**, or ++alt+6++), VS Code needs an extension like [Todo Tree](https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.todo-tree), and Thonny/IDLE have no built-in equivalent (it still works as a plain comment, just without the aggregated list). -### Going further { data-card-link="skip" } - -??? run "Run a debugging strategies example" - The `lengths` bug from above, worked through with the techniques above: - - ```python - lengths = {"ball python": 4.5, "burmese python": 12, "boa": 8} - # TODO: also count venomous snakes separately - - long_snakes = 0 - for length in lengths.values(): # fixed after reading the buggy version aloud — .values(), not the dict itself - print(type(length), length) # print debugging: confirm each value really is a length - if length > 6: - long_snakes += 1 - - print("total:", long_snakes) # total: 2 - ``` -
-## Using a debugger +### Debugger tool { .pt-fake-h2 } A **debugger** is a tool built into most code editors that lets you pause a running program and look around, instead of only seeing what it printed after the fact. Pause your code mid-run to inspect what's happening and inspect variables — instead of only reading `print()` outputs at the end. -### Step 0: Set "breakpoints" +0. **Set breakpoints.** A **breakpoint** marks a specific line where you want the program to pause while debugging, so you can inspect it. You can set as many as you want — set these *before* you start running. Click in the margin next to a line number to set one; click the same spot again to remove it — the red dot toggles off. +1. **Run in debug mode.** Look for a **"Debug"** button instead of the regular Run button. Your program will run normally until it hits the *first* breakpoint, then pauses there. +2. **Use the controls at a breakpoint.** Once paused, these controls move you through your code: -A **breakpoint** marks a specific line where you want the program to pause while debugging, so you can inspect it. You can set as many breakpoints as you want. Set these *before* you start running. + | Control | What it does | Use it when | + |---------|---------------|-------------| + | **Inspect variables** | Shows the current value of every variable while paused | You want to watch exactly when a variable becomes wrong, instead of guessing | + | **Step Into** | Jumps inside the [function](functions.md) being called, so you can watch it run line by line | You want to see exactly what a function does | + | **Step Over** | Runs the current line, then pauses on the next one, without entering any function it calls | You trust the function works and don't need to see inside it | + | **Step Out** | Finishes the current function, then pauses back where it was called from | You stepped into a function but have seen enough and want to jump back out | + | **Continue/Resume** (▶) | Runs until the next breakpoint, or finishes if there are none left | You're done inspecting the current pause point and want to jump ahead | + | **Stop debugging** | Ends the debug session entirely | You're done, instead of stepping or continuing all the way through | -Click in the margin next to a line number to set one. To remove it, click the same spot again — the red dot toggles off. + ??? tip "Where debugging controls are in each editor" + Where to find the debugger, and what it calls things, varies by editor. -### Step 1: Run in debug mode + === "Thonny" -Your program will run normally until it hits the *first* breakpoint, then it pauses there. + - **Debug button:** Bug icon in the main toolbar + - **Step controls:** Inline in the main toolbar + - **Stop button:** Same toolbar + - **Where output shows:** Same Shell panel as a normal run + - **Inspecting variables:** Always-visible Variables panel -Look for a **"Debug"** button instead of the regular Run button. + You don't need to set any breakpoints — Thonny's debugger pauses at every step by default, which is great for watching exactly how a program runs the first time. -### Step 2: What you can do at a breakpoint + === "VS Code" -When it gets to a breakpoint it will pause, and you can use these controls to move through your code: + - **Debug button:** "Run and Debug" in the sidebar, or the dropdown next to the Run button + - **Step controls:** A floating toolbar + - **Stop button:** Red square, same floating toolbar + - **Where output shows:** Separate "Debug Console" panel + - **Inspecting variables:** Variables section in the Run and Debug sidebar -| Control | What it does | Use it when | -|---------|---------------|-------------| -| **Inspect variables** | Shows the current value of every variable while paused | You want to watch exactly when a variable becomes wrong, instead of guessing | -| **Step Into** | Jumps inside the [function](functions.md) being called, so you can watch it run line by line | You want to see exactly what a function does | -| **Step Over** | Runs the current line, then pauses on the next one, without entering any function it calls | You trust the function works and don't need to see inside it | -| **Step Out** | Finishes the current function, then pauses back where it was called from | You stepped into a function but have seen enough and want to jump back out | -| **Continue/Resume** (▶) | Runs until the next breakpoint, or finishes if there are none left | You're done inspecting the current pause point and want to jump ahead | -| **Stop debugging** | Ends the debug session entirely | You're done, instead of stepping or continuing all the way through | + === "IDLE" -### Going further { data-card-link="skip" } + - **Debug button:** Debug menu in the Shell window (turn on before running) + - **Step controls:** A separate popup window + - **Stop button:** "Quit" button, same popup window + - **Where output shows:** Same Shell window as a normal run + - **Inspecting variables:** Same popup Debug Control window -??? tip "Where debugging controls are in each editor" - Where to find the debugger, and what it calls things, varies by editor. + Most basic of the four. - === "Thonny" + === "PyCharm" + + - **Debug button:** Bug icon next to the Run button + - **Step controls:** The bottom Debug tool window + - **Stop button:** Red square, same tool window + - **Where output shows:** Same "Debug" tool window + - **Inspecting variables:** Same tool window, or hover over a variable in the editor + +
- - **Debug button:** Bug icon in the main toolbar - - **Step controls:** Inline in the main toolbar - - **Stop button:** Same toolbar - - **Where output shows:** Same Shell panel as a normal run - - **Inspecting variables:** Always-visible Variables panel +
- You don't need to set any breakpoints — Thonny's debugger pauses at every step by default, which is great for watching exactly how a program runs the first time. +### Detect errors with testing { .pt-fake-h2 } - === "VS Code" +You're here because something broke. A **test** is a small script that checks your code's behavior automatically, so the same mistake gets caught the moment it's introduced — instead of the next time someone happens to run into it by hand. - - **Debug button:** "Run and Debug" in the sidebar, or the dropdown next to the Run button - - **Step controls:** A floating toolbar - - **Stop button:** Red square, same floating toolbar - - **Where output shows:** Separate "Debug Console" panel - - **Inspecting variables:** Variables section in the Run and Debug sidebar +```python-ref +def get_length(species, lengths): + return lengths.get(species) - === "IDLE" +def test_missing_species_returns_none(): + lengths = {"ball python": 4.5, "burmese python": 12} + assert get_length("reticulated python", lengths) is None +``` - - **Debug button:** Debug menu in the Shell window (turn on before running) - - **Step controls:** A separate popup window - - **Stop button:** "Quit" button, same popup window - - **Where output shows:** Same Shell window as a normal run - - **Inspecting variables:** Same popup Debug Control window +[pytest](libraries/pytest.md) is the standard tool for this in Python — a function named `test_*` is one check, a plain `assert` states what should be true, and running the file reports exactly which checks passed and which failed, the same way `python` reports which line of your code raised an error. - Most basic of the four. +Tests are especially good at catching [logic errors](#logic-errors) — the one category on this page with no traceback at all, where the only way to notice something's wrong is comparing the actual output against what you expected. A test makes that exact comparison every time, automatically, instead of relying on you to notice by eye. - === "PyCharm" +They're just as useful for [runtime errors](#runtime-errors) — a test can exercise an edge case you wouldn't normally hit by hand (an empty input, a missing key, a zero divisor), and `pytest.raises()` even lets you assert that a specific exception *should* fire, so you catch both "this crashes when it shouldn't" and "this doesn't crash when it should." [Syntax errors](#syntax-errors) are the one category tests can't help with — the file has to actually parse before pytest can even import it to run anything. - - **Debug button:** Bug icon next to the Run button - - **Step controls:** The bottom Debug tool window - - **Stop button:** Red square, same tool window - - **Where output shows:** Same "Debug" tool window - - **Inspecting variables:** Same tool window, or hover over a variable in the editor +??? run "Run a test example" + ```python + import pytest + + with open("test_lengths.py", "w") as file: + file.write( + "def get_length(species, lengths):\n" + " return lengths.get(species)\n" + "\n" + "def test_missing_species_returns_none():\n" + " lengths = {\"ball python\": 4.5, \"burmese python\": 12}\n" + " assert get_length(\"reticulated python\", lengths) is None\n" + ) + + pytest.main(["-v", "test_lengths.py"]) + ```
From 72623b10e5a81bf0129cc20399e17fbccb7a03b6 Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Fri, 11 Sep 2026 19:20:17 -0700 Subject: [PATCH 02/10] final errors rewrite --- docs/errors.md | 70 ++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/docs/errors.md b/docs/errors.md index 0bad72d..c3c5c6a 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -34,7 +34,7 @@ The code doesn't follow Python's grammar rules, so it can't read or run the file | | [Read error message](#reading-a-syntax-error-message) | [`try`/`except`](#catch-with-tryexcept) | [Debugging strategies](#debugging-strategies) | [Debugger tool](#debugger-tool) | [Testing](#detect-errors-with-testing) | |---|:---:|:---:|:---:|:---:|:---:| -| Ways to fix syntax errors | :material-check:{ .pt-icon-success }
Points to where Python couldn't understand | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | +| Ways to fix syntax errors | :material-check:{ .pt-icon-success }
Points to what Python couldn't read | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } | :material-close:{ .pt-icon-fail } |
@@ -238,43 +238,56 @@ These general techniques help close the gap between what you think the code does #### Read it out loud { .pt-fake-h3 } -```python-ref -for length in lengths: # "for each length in lengths" — but a dict hands back its keys - if length > 6: # so `length` is actually a species name here, not a number -``` - -Read your code line by line, out loud, saying in plain English what each line does and why. This is often called **rubber duck debugging**: putting each line into words forces you to state assumptions you'd otherwise skim past while reading silently. Say "for each length in lengths" out loud and the mismatch jumps out — looping directly over a dict hands back its keys, not its values, so `length` here is actually a species name like `"ball python"`. That's exactly what raises the error below: you can't compare a string to `6`. +Read your code line by line, out loud, saying in plain English what each line does and why. This is often called **rubber duck debugging**: putting each line into words forces you to state assumptions you'd otherwise skim past while reading silently. ```python-ref -Traceback (most recent call last): - File "lengths.py", line 5, in -TypeError: '>' not supported between instances of 'str' and 'int' +if length < 1 and length > 20: # "If length is under 1 and length is over 20..." + print("that length doesn't look right") # "impossible condition, should use `or` instead of `and`!" ``` -The fix follows straight from the narration — loop over `lengths.values()` instead. - #### Print debugging { .pt-fake-h3 } ```python-ref print(type(length), length) # confirm what a value actually is, not what you assumed it was ``` -Sprinkle `print()` calls between the lines you suspect, showing a variable's value (and [`type()`](types.md), if you're not sure) at that exact point in the run. This narrows down *where* your assumption about the code stopped matching reality — especially useful when nothing crashes and you're just staring at a wrong final answer, so there's no traceback pointing anywhere. Delete the `print()` calls once you've found the problem — they're a diagnostic, not part of the program. +Sprinkle `print()` calls between the lines you suspect, showing a variable's value (and [`type()`](types.md), if you're not sure) at that exact point in the run. This narrows down *where* your assumption about the code stopped matching reality — especially useful when nothing crashes and you're just staring at a wrong final answer, so there's no traceback pointing anywhere. Delete the `print()` calls once you've found the problem. #### Isolate the problem { .pt-fake-h3 } -Comment out or delete code until the smallest version that still shows the bug is left, then add pieces back one at a time until it reappears — whatever you just added back is the culprit. Especially useful in a long script, where the traceback's line number is buried inside a function calling a function calling a function — cutting the problem down to a few lines removes everything that isn't actually relevant. +Comment out or delete sections of code until you find the smallest version that still shows the problem. Especially useful for syntax errors you can't obviously spot, since the pointer Python gives you isn't always exactly where the mistake is. #### Flag as TODO/FIXME { .pt-fake-h3 } ```python-ref # TODO: handle the case where length_ft is negative length_ft = 4.5 + +# FIXME: math incorrect +def to_inches(length_ft): + return length_ft * 10 ``` -Not every problem gets fixed the moment you spot it — sometimes you're mid-debugging something else and don't want to lose track of it. Marking a comment `TODO` flags a placeholder for "come back to this," not a fix in itself. `FIXME` is the same idea for something you know is actively broken rather than just unfinished. +Not every problem gets fixed the moment you spot it — sometimes you're mid-debugging something else and don't want to lose track of it. Marking a comment `TODO` creates a reminder for yourself to "come back to this." `FIXME` is the same idea for something you know is actively broken rather than just unfinished. + +??? tip "Collecting TODO/FIXME comments in each editor" + Some editors collect every `TODO`/`FIXME` in a project into one scannable list. + + === "PyCharm" + + Built-in TODO tool window (**View → Tool Windows → TODO**, or ++alt+6++) collects every `TODO`/`FIXME` in the project into one scannable list. -Some editors collect every `TODO`/`FIXME` in a project into one scannable list — PyCharm has a built-in TODO tool window (**View → Tool Windows → TODO**, or ++alt+6++), VS Code needs an extension like [Todo Tree](https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.todo-tree), and Thonny/IDLE have no built-in equivalent (it still works as a plain comment, just without the aggregated list). + === "VS Code" + + No built-in aggregator, but an extension like [Todo Tree](https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.todo-tree) adds one. + + === "Thonny" + + No built-in equivalent — it still works as a plain comment, just without an aggregated list. + + === "IDLE" + + No built-in equivalent — it still works as a plain comment, just without an aggregated list. @@ -342,7 +355,7 @@ A **debugger** is a tool built into most code editors that lets you pause a runn ### Detect errors with testing { .pt-fake-h2 } -You're here because something broke. A **test** is a small script that checks your code's behavior automatically, so the same mistake gets caught the moment it's introduced — instead of the next time someone happens to run into it by hand. +A **test** is a small script that checks your code's behavior automatically, so the mistake gets caught the moment it's introduced. ```python-ref def get_length(species, lengths): @@ -353,28 +366,11 @@ def test_missing_species_returns_none(): assert get_length("reticulated python", lengths) is None ``` -[pytest](libraries/pytest.md) is the standard tool for this in Python — a function named `test_*` is one check, a plain `assert` states what should be true, and running the file reports exactly which checks passed and which failed, the same way `python` reports which line of your code raised an error. - -Tests are especially good at catching [logic errors](#logic-errors) — the one category on this page with no traceback at all, where the only way to notice something's wrong is comparing the actual output against what you expected. A test makes that exact comparison every time, automatically, instead of relying on you to notice by eye. +[pytest](libraries/pytest.md) is the standard tool for this in Python — a function starting with `test_` is one check, and inside it `assert` states what should be true. Running the file reports exactly which checks passed and which failed, the same way `python` reports which line of your code raised an error. -They're just as useful for [runtime errors](#runtime-errors) — a test can exercise an edge case you wouldn't normally hit by hand (an empty input, a missing key, a zero divisor), and `pytest.raises()` even lets you assert that a specific exception *should* fire, so you catch both "this crashes when it shouldn't" and "this doesn't crash when it should." [Syntax errors](#syntax-errors) are the one category tests can't help with — the file has to actually parse before pytest can even import it to run anything. +Tests are especially good at catching [logic errors](#logic-errors) — where the only way to notice something's wrong is comparing the actual output against what you expected. A test does that comparison automatically, instead of relying on you to notice by eye. -??? run "Run a test example" - ```python - import pytest - - with open("test_lengths.py", "w") as file: - file.write( - "def get_length(species, lengths):\n" - " return lengths.get(species)\n" - "\n" - "def test_missing_species_returns_none():\n" - " lengths = {\"ball python\": 4.5, \"burmese python\": 12}\n" - " assert get_length(\"reticulated python\", lengths) is None\n" - ) - - pytest.main(["-v", "test_lengths.py"]) - ``` +They're also useful for [runtime errors](#runtime-errors) — a test can exercise an edge case you wouldn't normally hit every time (an empty input, a missing key, a zero divisor), and `pytest.raises()` even lets you assert that a specific exception *should* fire, so you catch both "this crashes when it shouldn't" and "this doesn't crash when it should." From b526b1497299e0154e89e940471fff4ff92631d7 Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Fri, 11 Sep 2026 19:20:32 -0700 Subject: [PATCH 03/10] Create test_typos.py --- tests/test_typos.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_typos.py diff --git a/tests/test_typos.py b/tests/test_typos.py new file mode 100644 index 0000000..1a2603f --- /dev/null +++ b/tests/test_typos.py @@ -0,0 +1,33 @@ +"""Typo check over the site's prose sources, via codespell. + +codespell flags known misspellings (e.g. "teh", "recieve", "langauge") rather than +unrecognized words, so it doesn't choke on the site's snake species names, Python +jargon, or Pyodide/mkdocs terminology the way a dictionary-based spellchecker would. + +If a future false positive does show up (a real word codespell's dictionary treats as +a typo), add it to IGNORE_WORDS below rather than editing the file to dodge it. +""" + +import subprocess +import sys + +from conftest import REPO_ROOT + +# Words codespell's dictionary flags that are intentional here — add to this set +# (lowercase) rather than rewording a page to avoid a false positive. +IGNORE_WORDS: set[str] = set() + +# Prose sources to check: page content plus the repo's own editorial docs. +CHECK_PATHS = ["docs", "includes", "STRUCTURE.md", "README.md"] + + +def test_no_typos(): + args = [sys.executable, "-m", "codespell_lib", *CHECK_PATHS] + if IGNORE_WORDS: + args += ["-L", ",".join(sorted(IGNORE_WORDS))] + proc = subprocess.run(args, cwd=REPO_ROOT, capture_output=True, text=True) + # codespell exits 0 for no findings, >0 (non-usage-error) for findings. + assert proc.returncode == 0, ( + "codespell found possible typos (fix the text, or add a genuine false " + "positive to IGNORE_WORDS in tests/test_typos.py):\n" + proc.stdout + proc.stderr + ) From 36c1ce8109074d72d8579dd7caa7be4a94ebd724 Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Fri, 11 Sep 2026 19:23:24 -0700 Subject: [PATCH 04/10] add common error warnings --- docs/collections.md | 14 ++++++++++++++ docs/functions.md | 22 ++++++++++++++++++++++ docs/loops.md | 15 +++++++++++++++ docs/types.md | 14 ++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/docs/collections.md b/docs/collections.md index b704815..5cb12dd 100644 --- a/docs/collections.md +++ b/docs/collections.md @@ -333,6 +333,20 @@ class diagram panel ### Going further { data-card-link="skip" } +??? warning "In-place list methods return None" + `append()`, `insert()`, `extend()`, `sort()`, `reverse()`, and `remove()` all change the list directly and return `None` — not the changed list. Reassigning the variable to one of their results replaces the list itself with `None`, and the next call on it raises `AttributeError: 'NoneType' object has no attribute '...'`. + + ```python-ref + species = species.append("carpet") # species is now None, not the updated list + species.sort() # AttributeError: 'NoneType' object has no attribute 'sort' + ``` + + Call the method on its own line instead — the list was already changed in place, nothing to reassign. + + ```python-ref + species.append("carpet") # correct — no assignment needed + ``` + ??? run "Practice with lists" Each box below is fully editable — write your answer, then click Run. diff --git a/docs/functions.md b/docs/functions.md index d658703..613f60c 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -121,6 +121,28 @@ describe(species="ball", venomous=True) # length_ft still uses its default pass # placeholder — does nothing, but prevents a syntax error ``` +??? warning "Mutable default argument" + A default value is only ever created **once**, when the function is defined — not fresh on every call. For a list or dict default, that means every call sharing that default is silently reading and writing the *same* object, so it keeps growing across calls instead of starting empty each time. + + ```python-ref + def add_sighting(species, log=[]): # log=[] is created once, not per call + log.append(species) + return log + + add_sighting("ball") # ["ball"] + add_sighting("burmese") # ["ball", "burmese"] — the same list, not a fresh one + ``` + + Default to `None` instead, and create the list inside the function body: + + ```python-ref + def add_sighting(species, log=None): + if log is None: + log = [] + log.append(species) + return log + ``` + ??? run "Run a function example" All the examples above, combined into one script: diff --git a/docs/loops.md b/docs/loops.md index 6f55202..c176064 100644 --- a/docs/loops.md +++ b/docs/loops.md @@ -323,6 +323,21 @@ for s in reversed(species): ### Going further { data-card-link="skip" } +??? warning "Modifying a list while looping over it" + Adding to or removing from a list while a `for` loop is walking over it shifts every item after the change into a different position — the loop keeps advancing by index, so it silently skips over whatever slid into the spot it already passed. + + ```python-ref + species = ["ball", "burmese", "boa"] + + for specie in species: + if specie == "burmese": + species.remove(specie) # "boa" slides into "burmese"'s spot... + + species # ["ball", "boa"] — looks right here, but skips items in longer lists + ``` + + Loop over a copy instead, so the list being changed and the list being walked aren't the same object — `for specie in species.copy():` (equivalently `species[:]` or `list(species)`). + ??? tip "Loop two collections at the same time with zip()" `zip()` pairs up items from two (or more) iterables by position — the first item from each, then the second from each, and so on — stopping as soon as the shortest one runs out. Works with any iterable, mixed types included — list, tuple, string, dict (its keys, by default), even a `range()`. Because it is based on order, using an unordered collection like `set` or plain `dict` can produce pairings in an unpredictable order. diff --git a/docs/types.md b/docs/types.md index c34f77f..aa2c587 100644 --- a/docs/types.md +++ b/docs/types.md @@ -309,6 +309,8 @@ if weight: # runs — weight isn't 0.0 This is a property of floating-point math in virtually every programming language, not a Python bug. If you need exact decimal arithmetic, use the `decimal` library instead of `float`. + It also means two floats that *should* be equal can compare unequal with `==` — `0.1 + 0.2 == 0.3` is `False`. Round both sides first (`round(x, 2) == round(y, 2)`), or check they're close enough instead of exactly equal. + ??? run "Practice with floats" ```python @@ -1131,6 +1133,18 @@ venomous = None ### Going further { data-card-link="skip" } +??? warning "is vs ==" + `is` checks whether two variables point to the *exact same object* in memory, not whether their values are equal — that's what makes it correct for `None` (there's ever only one), but wrong for almost everything else. Small integers and short strings happen to work with `is` too, because Python reuses those specific objects internally, which makes the mistake easy to miss until it silently breaks on a larger number or a value built some other way. + + ```python-ref + a = 1000 + b = 1000 + a == b # True — same value + a is b # usually False — different objects, even though the value matches + ``` + + Use `==`/`!=` to compare values, and reserve `is`/`is not` for `None`. + ??? run "Practice with None" ```python From 2044c43f7c69950ec97e336a3abd41595bb45a8b Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Fri, 11 Sep 2026 19:24:01 -0700 Subject: [PATCH 05/10] update outdated references --- docs/libraries/requests.md | 2 +- docs/workspace.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/libraries/requests.md b/docs/libraries/requests.md index bc3ac3c..740e4d9 100644 --- a/docs/libraries/requests.md +++ b/docs/libraries/requests.md @@ -138,7 +138,7 @@ print(len(comments)) ## Handling request errors -A network call can fail in ways that have nothing to do with your code — the [Errors](../errors.md#handling-errors) 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](../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/workspace.md b/docs/workspace.md index 5536d7a..9485755 100644 --- a/docs/workspace.md +++ b/docs/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#using-a-debugger)** — pause your code mid-run and inspect variables to track down bugs, instead of only reading output after the fact +- **[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 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-errors) page covers how to read it. + When you see red error text, the [Errors](errors.md#reading-a-traceback) page covers how to read it. From 77e2ea74d05a69d148bdc78180fc20ef13a23dd0 Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Fri, 11 Sep 2026 19:28:09 -0700 Subject: [PATCH 06/10] fix typos found in new test --- docs/collections.md | 4 ++-- docs/errors.md | 2 +- docs/libraries/collections.md | 2 +- docs/types.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/collections.md b/docs/collections.md index 5cb12dd..9677bb6 100644 --- a/docs/collections.md +++ b/docs/collections.md @@ -92,7 +92,7 @@ class diagram panel print(species) # ["burmese", "carpet", "ball", "blood"] ``` -- **Acccess a range of multiple items at once:** +- **Access a range of multiple items at once:** - **Slice with `list[start:end]`** to return a new list containing items from the `start` index up to (but not including) the `end` index. @@ -499,7 +499,7 @@ flowchart LR snake["species"] # "ball" ``` -- **`get()`** does the same thing, but returns `None` if the key is not in the dict. You can provide an optinal default value to fall back on that will be returned if they key is not in the dict. +- **`get()`** does the same thing, but returns `None` if the key is not in the dict. You can provide an optional default value to fall back on that will be returned if the key is not in the dict. ```python-ref snake.get("species") # "ball" diff --git a/docs/errors.md b/docs/errors.md index c3c5c6a..e43fd1e 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -93,7 +93,7 @@ Think about what programming concepts the failing line is using (data type, loop A logic error is a bug Python doesn't notice, it finishes running but gives you an **unexpected result** because the reasoning itself was **inaccurate**. -Think about what programming concepts you are using (data types, loops, conditionals, etc.) and revist those pages on this site to confirm you're applying them correctly. +Think about what programming concepts you are using (data types, loops, conditionals, etc.) and revisit those pages on this site to confirm you're applying them correctly.
diff --git a/docs/libraries/collections.md b/docs/libraries/collections.md index c5b4c1d..4f41f04 100644 --- a/docs/libraries/collections.md +++ b/docs/libraries/collections.md @@ -14,7 +14,7 @@ description: >- `from collections import ...`. For the built-in `list`, `dict`, `tuple`, and `set` types themselves, see [Collections](../collections.md). -The **`collections`** module adds specialized containers with added functionaility on top of the +The **`collections`** module adds specialized containers with added functionality on top of the built-in [`str`](../types.md#strings) [`list`](../collections.md#lists) [`dict`](../collections.md#dictionaries) [`tuple`](../collections.md#tuples) and [`set`](../collections.md#sets).
diff --git a/docs/types.md b/docs/types.md index aa2c587..b063f6b 100644 --- a/docs/types.md +++ b/docs/types.md @@ -449,7 +449,7 @@ Strings use the same index and slice syntax as lists. `0` is the first character `{ value : [align] [sign] [width] [thousand separator ,] [.precision] [type] }` - - **align** — `<` left, `>` right, or `^` center, aligns *within* the width, so it requires a specificed width too. + - **align** — `<` left, `>` right, or `^` center, aligns *within* the width, so it requires a specified width too. ```python-ref f"{length:<6}" # "5 " — left-aligned in 6 characters From 4cd3eb1190475719b38ea724ef66b4a52a7260e8 Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Fri, 11 Sep 2026 19:28:22 -0700 Subject: [PATCH 07/10] new glossary terms --- includes/glossary.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/includes/glossary.md b/includes/glossary.md index fa13ca8..29a2599 100644 --- a/includes/glossary.md +++ b/includes/glossary.md @@ -45,3 +45,7 @@ *[Hashable]: Can be used as a dict key or set member because it never changes after creation — most immutable types qualify, like int, float, str, bool, None, and tuple *[queue]: A line of items processed in the order they arrive — the first one added is the first one handled *[Queue]: A line of items processed in the order they arrive — the first one added is the first one handled +*[bug]: A mistake in your code that makes it do the wrong thing, whether or not Python actually notices and raises an error +*[Bug]: A mistake in your code that makes it do the wrong thing, whether or not Python actually notices and raises an error +*[syntax]: The grammatical rules for what counts as validly structured code, checked before any of it runs — independent of whether the logic is actually correct +*[Syntax]: The grammatical rules for what counts as validly structured code, checked before any of it runs — independent of whether the logic is actually correct From 8995d4a0f8179680c4c781d912c1e8a2d32a5235 Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Fri, 11 Sep 2026 19:29:22 -0700 Subject: [PATCH 08/10] new pytest page --- docs/index.md | 68 +++++---- docs/javascripts/pyodide_runner.js | 6 +- docs/libraries/index.md | 54 ++++--- docs/libraries/pytest.md | 218 +++++++++++++++++++++++++++++ docs/style.md | 4 +- docs/stylesheets/extra.css | 30 +++- mkdocs.yml | 2 + requirements.txt | 1 + 8 files changed, 336 insertions(+), 47 deletions(-) create mode 100644 docs/libraries/pytest.md diff --git a/docs/index.md b/docs/index.md index c01d4a8..702ea4b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -102,7 +102,7 @@ hide: Repeat a block of code multiple times. - [**`for`**](loops.md#for-loops): [`enumerate`](loops.md#loop-with-index-and-value) [`loop a set number of times`](loops.md#loop-a- certain-number-of-times) [`loop through a collection`](loops.md#loop-through-a-collection) [`range`](loops.md#iterable-range) [`reversed`](loops.md#loop-in-reverse) [`zip`](loops.md#loop-with-index-and-value) + [**`for`**](loops.md#for-loops): [`enumerate`](loops.md#loop-with-index-and-value) [`loop a set number of times`](loops.md#loop-a-certain-number-of-times) [`loop through a collection`](loops.md#loop-through-a-collection) [`range`](loops.md#iterable-range) [`reversed`](loops.md#loop-in-reverse) [`zip`](loops.md#loop-with-index-and-value) [**`while`**](loops.md#while-loops): [`and`](loops.md#logical-operators) [`boolean expressions`](loops.md#boolean-expressions) [`counter and flag names`](loops.md#counter-and-flag-names) [`flag`](loops.md#using-a-flag) [`not`](loops.md#logical-operators) [`or`](loops.md#logical-operators) [`sentinel`](loops.md#sentinel) @@ -124,7 +124,7 @@ hide: [**`def`**](functions.md#defining-a-function): [`default parameter values`](functions.md#default-parameter-values) [`docstrings`](functions.md#docstrings) [`keyword arguments`](functions.md#keyword-arguments) [`return`](functions.md#return-values) - [**`*args`**](functions.md#args), [**`**kwargs`**](functions.md#kwargs) + [**`flexible arguments`**](functions.md#flexible-arguments): [`*args`](functions.md#args) [`**kwargs`](functions.md#kwargs) [**`scope`**](functions.md#scope): [`local vs global`](functions.md#local-vs-global-variables) @@ -196,13 +196,11 @@ hide: How to understand, manage, and fix errors. - [**`try, except`**](errors.md#handling-errors): [`exception types`](errors.md#common-exception-types) + [**`kinds of errors`**](errors.md#kinds-of-errors): [`syntax errors`](errors.md#syntax-errors) [`runtime errors`](errors.md#runtime-errors) [`logic errors`](errors.md#logic-errors) - [**`tracebacks`**](errors.md#reading-errors): [`how to read a traceback`](errors.md#how-to-read-a-traceback) + [**`fixing errors`**](errors.md#fixing-errors): [`tracebacks`](errors.md#reading-a-traceback) [`reading a syntax error message`](errors.md#reading-a-syntax-error-message) [`try, except`](errors.md#catch-with-tryexcept) [`debugging strategies`](errors.md#debugging-strategies) [`isolate the problem`](errors.md#isolate-the-problem) [`print debugging`](errors.md#print-debugging) [`rubber duck debugging`](errors.md#read-it-out-loud) [`debugger tool`](errors.md#debugger-tool) [`TODO, FIXME`](errors.md#flag-as-todofixme) [`testing`](errors.md#detect-errors-with-testing) - [**`debugger`**](errors.md#using-a-debugger): [`run in debug mode`](errors.md#step-1-run-in-debug-mode) [`set breakpoints`](errors.md#step-0-set-breakpoints) [`what you can do at a breakpoint`](errors.md#step-2-what-you-can-do-at-a-breakpoint) - - [**`debugging strategies`**](errors.md#debugging-strategies): [`isolate the problem`](errors.md#isolate-the-problem) [`print debugging`](errors.md#print-debugging) [`rubber duck debugging`](errors.md#read-it-out-loud) [`TODO, FIXME`](errors.md#flag-it-with-todofixme) + [**`detect errors with testing`**](errors.md#detect-errors-with-testing)
@@ -252,31 +250,23 @@ hide:
-#### Desktop UIs { .pt-homepage-heading } +#### Testing { .pt-homepage-heading }
-- :material-application-outline:{ .lg .middle } [__Tkinter__](libraries/tkinter.md) [:material-language-python:](libraries/tkinter.md){ .pt-lib-badge .pt-lib-badge--builtin title="Built-in — included with Python" } - - Creating desktop applications: text, buttons, dropdowns, forms, output, etc. - - [**`Tk`**](libraries/tkinter.md#creating-a-window) - - [**`Button`**](libraries/tkinter.md#widgets): [`Button`](libraries/tkinter.md#button) [`Entry`](libraries/tkinter.md#entry) [`Label`](libraries/tkinter.md#label) +- :material-test-tube:{ .lg .middle } [__pytest__](libraries/pytest.md) [:material-download-outline:](libraries/pytest.md){ .pt-lib-badge .pt-lib-badge--third-party title="Third-party — install separately with pip" } - [**`pack`**](libraries/tkinter.md#layout-managers): [`grid`](libraries/tkinter.md#grid) [`pack`](libraries/tkinter.md#pack) - - [**`configure`**](libraries/tkinter.md#configuring-widgets): [`reading and changing options`](libraries/tkinter.md#reading-and-changing-options) + Writing and running tests: assertions, fixtures, and parametrizing. - [**`command`**](libraries/tkinter.md#handling-events): [`binding events`](libraries/tkinter.md#binding-events) [`command callbacks`](libraries/tkinter.md#command-callbacks) + [**`writing and running a test`**](libraries/pytest.md#writing-and-running-a-test): [`from the command line`](libraries/pytest.md#from-the-command-line) - [**`Style`**](libraries/tkinter.md#styling-with-ttk): [`customizing a style`](libraries/tkinter.md#customizing-a-style) + [**`reading a failure`**](libraries/pytest.md#reading-a-failure) - [**`messagebox`**](libraries/tkinter.md#dialogs): [`file dialogs`](libraries/tkinter.md#file-dialogs) [`message boxes`](libraries/tkinter.md#message-boxes) + [**`fixtures`**](libraries/pytest.md#fixtures) - [**`winfo_width`**](libraries/tkinter.md#introspecting-widgets): [`winfo methods`](libraries/tkinter.md#winfo-methods) + [**`parametrizing tests`**](libraries/pytest.md#parametrizing-tests) - [**`putting it together`**](libraries/tkinter.md#putting-it-together): [`a simple form`](libraries/tkinter.md#a-simple-form) + [**`testing for exceptions`**](libraries/pytest.md#testing-for-exceptions)
@@ -389,6 +379,36 @@ hide: +
+#### Desktop UIs { .pt-homepage-heading } + +
+ +- :material-application-outline:{ .lg .middle } [__Tkinter__](libraries/tkinter.md) [:material-language-python:](libraries/tkinter.md){ .pt-lib-badge .pt-lib-badge--builtin title="Built-in — included with Python" } + + Creating desktop applications: text, buttons, dropdowns, forms, output, etc. + + [**`Tk`**](libraries/tkinter.md#creating-a-window) + + [**`Button`**](libraries/tkinter.md#widgets): [`Button`](libraries/tkinter.md#button) [`Entry`](libraries/tkinter.md#entry) [`Label`](libraries/tkinter.md#label) + + [**`pack`**](libraries/tkinter.md#layout-managers): [`grid`](libraries/tkinter.md#grid) [`pack`](libraries/tkinter.md#pack) + + [**`configure`**](libraries/tkinter.md#configuring-widgets): [`reading and changing options`](libraries/tkinter.md#reading-and-changing-options) + + [**`command`**](libraries/tkinter.md#handling-events): [`binding events`](libraries/tkinter.md#binding-events) [`command callbacks`](libraries/tkinter.md#command-callbacks) + + [**`Style`**](libraries/tkinter.md#styling-with-ttk): [`customizing a style`](libraries/tkinter.md#customizing-a-style) + + [**`messagebox`**](libraries/tkinter.md#dialogs): [`file dialogs`](libraries/tkinter.md#file-dialogs) [`message boxes`](libraries/tkinter.md#message-boxes) + + [**`winfo_width`**](libraries/tkinter.md#introspecting-widgets): [`winfo methods`](libraries/tkinter.md#winfo-methods) + + [**`putting it together`**](libraries/tkinter.md#putting-it-together): [`a simple form`](libraries/tkinter.md#a-simple-form) + +
+
+
#### Computer vision { .pt-homepage-heading } @@ -430,7 +450,7 @@ hide: | | Learn to do it yourself | Have AI do it for you | |---|---|---| | **Writing & struggling with code** | :material-check:{ .pt-icon-success } **Productive struggle** is what builds understanding
  • You can solve the problem again on your own
  • Adapt the answer, and catch when it's wrong
| :material-close:{ .pt-icon-fail } Being handed the answer skips [the friction that builds understanding](https://bjorklab.psych.ucla.edu/wp-content/uploads/sites/13/2016/04/EBjork_RBjork_2011.pdf)
  • If you skip that struggle, you *won't develop the knowledge* to solve the problem again, adapt the answer, or recognize when it's wrong.
| - | **Reading & verifying code** | :material-check:{ .pt-icon-success } Understanding programming fundamentals makes AI more useful — you can read code you didn't write, and check it before you trust it
  • Spot mistakes
  • Understand *why* a solution works
  • Communicate your problem to AI more effectively
| :material-close:{ .pt-icon-fail } Inefficient communciation with AI if you don't fully understand what's going on, and AI code can look correct while being **wrong and insecure**
  • You can't tell *why* a solution works
  • Researchers are already documenting this skill gap in [students who rely on AI code generation](https://dl.acm.org/doi/10.1145/3617367) before they've [built their own foundation](https://dl.acm.org/doi/10.1145/3624720)
  • One [Stanford study](https://dl.acm.org/doi/10.1145/3576915.3623157) found developers using AI wrote *less* secure code — but were *more* confident it was secure
| + | **Reading & verifying code** | :material-check:{ .pt-icon-success } Understanding programming fundamentals makes AI more useful — you can read code you didn't write, and check it before you trust it
  • Spot mistakes
  • Understand *why* a solution works
  • Communicate your problem to AI more effectively
| :material-close:{ .pt-icon-fail } Inefficient communication with AI if you don't fully understand what's going on, and AI code can look correct while being **wrong and insecure**
  • You can't tell *why* a solution works
  • Researchers are already documenting this skill gap in [students who rely on AI code generation](https://dl.acm.org/doi/10.1145/3617367) before they've [built their own foundation](https://dl.acm.org/doi/10.1145/3624720)
  • One [Stanford study](https://dl.acm.org/doi/10.1145/3576915.3623157) found developers using AI wrote *less* secure code — but were *more* confident it was secure
| | **On the job** | :material-check:{ .pt-icon-success } Employers are still hiring for understanding, not prompting
  • Code review, debugging, and interviews all test whether you can reason about code
  • And judge whether it's correct
  • Learning to program on your own turns AI into a tool you can direct and verify, instead of one you're assuming got it right
| :material-close:{ .pt-icon-fail } AI will likely be available at work too — but it isn't what's being tested |
diff --git a/docs/javascripts/pyodide_runner.js b/docs/javascripts/pyodide_runner.js index 1c86ea9..6f61b90 100644 --- a/docs/javascripts/pyodide_runner.js +++ b/docs/javascripts/pyodide_runner.js @@ -111,10 +111,10 @@ const pyodide = await loadPyodideRuntime(); // Pure-stdlib code runs as-is, but third-party packages (numpy, - // pandas, ...) ship as separate Pyodide wheels that must be fetched - // before the `import` inside the snippet will succeed. + // pandas, pytest, ...) ship as separate Pyodide wheels that must be + // fetched before the `import` inside the snippet will succeed. const source = codeBlock.textContent; - const neededPackages = ["numpy", "pandas"].filter((pkg) => + const neededPackages = ["numpy", "pandas", "pytest"].filter((pkg) => new RegExp(`\\bimport\\s+${pkg}\\b|\\bfrom\\s+${pkg}\\b`).test(source) ); if (neededPackages.length) { diff --git a/docs/libraries/index.md b/docs/libraries/index.md index bf3f92c..5429db9 100644 --- a/docs/libraries/index.md +++ b/docs/libraries/index.md @@ -54,31 +54,23 @@ Libraries allow us to apply Python to real tasks. These are a few popular ones,
-#### Desktop UIs { .pt-homepage-heading } +#### Testing { .pt-homepage-heading }
-- :material-application-outline:{ .lg .middle } [__Tkinter__](tkinter.md) [:material-language-python:](tkinter.md){ .pt-lib-badge .pt-lib-badge--builtin title="Built-in — included with Python" } - - Creating desktop applications: text, buttons, dropdowns, forms, output, etc. - - [**`Tk`**](tkinter.md#creating-a-window) +- :material-test-tube:{ .lg .middle } [__pytest__](pytest.md) [:material-download-outline:](pytest.md){ .pt-lib-badge .pt-lib-badge--third-party title="Third-party — install separately with pip" } - [**`Button`**](tkinter.md#widgets): [`Button`](tkinter.md#button) [`Entry`](tkinter.md#entry) [`Label`](tkinter.md#label) + Writing and running tests: assertions, fixtures, and parametrizing. - [**`pack`**](tkinter.md#layout-managers): [`grid`](tkinter.md#grid) [`pack`](tkinter.md#pack) + [**`writing and running a test`**](pytest.md#writing-and-running-a-test): [`from the command line`](pytest.md#from-the-command-line) - [**`configure`**](tkinter.md#configuring-widgets): [`reading and changing options`](tkinter.md#reading-and-changing-options) + [**`reading a failure`**](pytest.md#reading-a-failure) - [**`command`**](tkinter.md#handling-events): [`binding events`](tkinter.md#binding-events) [`command callbacks`](tkinter.md#command-callbacks) + [**`fixtures`**](pytest.md#fixtures) - [**`Style`**](tkinter.md#styling-with-ttk): [`customizing a style`](tkinter.md#customizing-a-style) + [**`parametrizing tests`**](pytest.md#parametrizing-tests) - [**`messagebox`**](tkinter.md#dialogs): [`file dialogs`](tkinter.md#file-dialogs) [`message boxes`](tkinter.md#message-boxes) - - [**`winfo_width`**](tkinter.md#introspecting-widgets): [`winfo methods`](tkinter.md#winfo-methods) - - [**`putting it together`**](tkinter.md#putting-it-together): [`a simple form`](tkinter.md#a-simple-form) + [**`testing for exceptions`**](pytest.md#testing-for-exceptions)
@@ -191,6 +183,36 @@ Libraries allow us to apply Python to real tasks. These are a few popular ones, +
+#### Desktop UIs { .pt-homepage-heading } + +
+ +- :material-application-outline:{ .lg .middle } [__Tkinter__](tkinter.md) [:material-language-python:](tkinter.md){ .pt-lib-badge .pt-lib-badge--builtin title="Built-in — included with Python" } + + Creating desktop applications: text, buttons, dropdowns, forms, output, etc. + + [**`Tk`**](tkinter.md#creating-a-window) + + [**`Button`**](tkinter.md#widgets): [`Button`](tkinter.md#button) [`Entry`](tkinter.md#entry) [`Label`](tkinter.md#label) + + [**`pack`**](tkinter.md#layout-managers): [`grid`](tkinter.md#grid) [`pack`](tkinter.md#pack) + + [**`configure`**](tkinter.md#configuring-widgets): [`reading and changing options`](tkinter.md#reading-and-changing-options) + + [**`command`**](tkinter.md#handling-events): [`binding events`](tkinter.md#binding-events) [`command callbacks`](tkinter.md#command-callbacks) + + [**`Style`**](tkinter.md#styling-with-ttk): [`customizing a style`](tkinter.md#customizing-a-style) + + [**`messagebox`**](tkinter.md#dialogs): [`file dialogs`](tkinter.md#file-dialogs) [`message boxes`](tkinter.md#message-boxes) + + [**`winfo_width`**](tkinter.md#introspecting-widgets): [`winfo methods`](tkinter.md#winfo-methods) + + [**`putting it together`**](tkinter.md#putting-it-together): [`a simple form`](tkinter.md#a-simple-form) + +
+
+
#### Computer vision { .pt-homepage-heading } diff --git a/docs/libraries/pytest.md b/docs/libraries/pytest.md new file mode 100644 index 0000000..6538a38 --- /dev/null +++ b/docs/libraries/pytest.md @@ -0,0 +1,218 @@ +--- +description: >- + Writing and running tests in Python with pytest: assertions, fixtures, parametrizing, + and testing for exceptions, with runnable examples. +--- + +# :material-test-tube:{ .lg .middle } pytest library + +[Official documentation :material-open-in-new:](https://docs.pytest.org/en/stable/){ target="_blank" } + +pytest is an open-source project maintained by volunteer contributors. + +**pytest** is Python's most widely used testing framework — it finds test functions in a project, runs each one, and reports which passed or failed. It's a third-party package, not part of the standard library, but it's largely replaced the built-in `unittest` module for new projects because a test is just a function with a plain `assert` statement, instead of a class built on a special base and assert methods like `.assertEqual()`. Every example below actually runs in your browser: Pyodide gives each page its own in-memory filesystem, so writing a test file and pointing pytest at it works the same way it would on a real computer. + +
+ +## Install { data-card-link="skip" } + +```bash +pip install pytest +``` + +
+ +
+ +## Import { data-card-link="skip" } + +Most of what pytest does — discovering `test_*` functions and checking plain `assert` statements — needs no import at all. A plain `import pytest` is only needed for its extra tools: fixtures, marks, and `pytest.raises`. + +```python-ref +import pytest +``` + +| Concept | What it is | +|---------|------------| +| Test file | A file named `test_*.py` (or `*_test.py`) that pytest scans for tests | +| Test function | A function named `test_*` inside a test file — each one is a single check | +| Assertion | A plain `assert` statement — pytest reports exactly what failed, no special method needed | +| Fixture | Reusable setup code, shared across test functions with `@pytest.fixture` | +| Marker | A tag like `@pytest.mark.parametrize`, attached to a test to change how it runs | + +
+ +
+ +## Writing and running a test + +A pytest test is an ordinary function, named `test_...`, that makes one or more `assert` statements about the code it's checking. No import, base class, or naming beyond the `test_` prefix is required. + +```python-ref +def test_species_count(): + species = ["ball", "burmese", "boa"] + assert len(species) == 3 +``` + +### From the command line + +Normally you run `pytest` (or `python -m pytest`) from a terminal in the project directory, and it discovers every `test_*.py` file on its own — no need to name each one. This page's sandbox has no terminal, so the examples below call `pytest.main()` directly instead, which does the same discovery-and-run programmatically. + +```python-ref +import pytest + +pytest.main(["-v", "test_snakes.py"]) +``` + +??? run "Run a writing and running tests example" + All the examples above, combined into one script: + + ```python + import pytest + + with open("test_snakes.py", "w") as file: + file.write( + "def test_species_count():\n" + " species = [\"ball\", \"burmese\", \"boa\"]\n" + " assert len(species) == 3\n" + ) + + pytest.main(["-v", "test_snakes.py"]) + ``` + +
+ +
+ +## Reading a failure + +When an `assert` fails, pytest rewrites it behind the scenes to show the actual values it compared, not just that the statement was false — so a failure report reads like a diff, not a generic error. + +```python-ref +def test_species_count(): + species = ["ball", "burmese"] + assert len(species) == 3 # AssertionError: assert 2 == 3 +``` + +??? note "Why plain assert is enough" + Older frameworks like `unittest` need special methods (`.assertEqual()`, `.assertTrue()`, ...) because a bare `assert` normally only reports `AssertionError`, with no detail about *why*. pytest rewrites `assert` statements in test files at import time to capture each operand, so a plain `assert a == b` already shows both values on failure — no special method vocabulary to learn. + +??? run "Run a failing test example" + ```python + import pytest + + with open("test_snakes.py", "w") as file: + file.write( + "def test_species_count():\n" + " species = [\"ball\", \"burmese\"]\n" + " assert len(species) == 3\n" + ) + + pytest.main(["-v", "test_snakes.py"]) + ``` + +
+ +
+ +## Fixtures + +A **fixture** is a function decorated with `@pytest.fixture` that builds some setup data once; any test function that names it as a parameter receives its return value automatically, without calling it directly. + +```python-ref +@pytest.fixture +def snake(): + return {"species": "burmese python", "length_ft": 12, "venomous": False} + +def test_snake_not_venomous(snake): + assert snake["venomous"] is False +``` + +??? run "Run a fixtures example" + ```python + import pytest + + with open("test_snakes.py", "w") as file: + file.write( + "import pytest\n" + "\n" + "@pytest.fixture\n" + "def snake():\n" + " return {\"species\": \"burmese python\", \"length_ft\": 12, \"venomous\": False}\n" + "\n" + "def test_snake_not_venomous(snake):\n" + " assert snake[\"venomous\"] is False\n" + ) + + pytest.main(["-v", "test_snakes.py"]) + ``` + +
+ +
+ +## Parametrizing tests + +`@pytest.mark.parametrize` runs the same test function once per row of arguments, instead of copy-pasting a near-identical test for every case. + +```python-ref +@pytest.mark.parametrize("species,length_ft", [ + ("ball", 4.5), + ("burmese", 12), + ("boa", 8), +]) +def test_length_is_positive(species, length_ft): + assert length_ft > 0 +``` + +??? run "Run a parametrize example" + ```python + import pytest + + with open("test_snakes.py", "w") as file: + file.write( + "import pytest\n" + "\n" + "@pytest.mark.parametrize(\"species,length_ft\", [\n" + " (\"ball\", 4.5),\n" + " (\"burmese\", 12),\n" + " (\"boa\", 8),\n" + "])\n" + "def test_length_is_positive(species, length_ft):\n" + " assert length_ft > 0\n" + ) + + pytest.main(["-v", "test_snakes.py"]) + ``` + +
+ +
+ +## 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. + +```python-ref +def test_invalid_length_raises(): + with pytest.raises(ValueError): + float("not a number") +``` + +??? run "Run a testing for exceptions example" + ```python + import pytest + + with open("test_snakes.py", "w") as file: + file.write( + "import pytest\n" + "\n" + "def test_invalid_length_raises():\n" + " with pytest.raises(ValueError):\n" + " float(\"not a number\")\n" + ) + + pytest.main(["-v", "test_snakes.py"]) + ``` + +
diff --git a/docs/style.md b/docs/style.md index 2cb8f54..bd46fa3 100644 --- a/docs/style.md +++ b/docs/style.md @@ -270,7 +270,7 @@ length_ft = 4.5 # too short # PEP 8 — two spaces before, one after There's no single tool that reliably flags all "unpythonic" code the way PEP 8 has a document to check against. The real habit is asking *"does Python already have a built-in way to do this?"* before writing a manual loop, counter, or flag — an instinct built over time to recognize the built-in pattern. -Other programming langues have different features and patterns, so if code is translated from another langauge into Python it might not be written very clearly. Pythonic code tends to be less buggy and faster. +Other programming languages have different features and patterns, so if code is translated from another language into Python it might not be written very clearly. Pythonic code tends to be less buggy and faster. ### Common patterns @@ -388,7 +388,7 @@ Repeating the same few lines in multiple places is a sign to pull them into thei ### Catch specific exceptions -Catch the exact exception you expect (`except ValueError:`) instead of a bare `except:` — a bare `except` also silently swallows errors you didn't anticipate, including a typo in your own code, and even catches things like a keyboard interrupt (++ctrl+c++) that usually shouldn't be caught at all. Full `try`/`except` mechanics are covered on the [Errors](errors.md#handling-errors) page. +Catch the exact exception you expect (`except ValueError:`) instead of a bare `except:` — a bare `except` also silently swallows errors you didn't anticipate, including a typo in your own code, and even catches things like a keyboard interrupt (++ctrl+c++) that usually shouldn't be caught at all. Full `try`/`except` mechanics are covered on the [Errors](errors.md#catch-with-tryexcept) page. ```python-ref try: diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 773a868..edac12b 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -824,6 +824,22 @@ input:checked + .md-consent__settings { white-space: nowrap; } +.pt-jump-table code { + word-break: keep-all; + overflow-wrap: normal; +} + +.pt-checklist-table th:first-child, +.pt-checklist-table td:first-child { + word-break: keep-all; + overflow-wrap: normal; +} + +.pt-checklist-table code { + word-break: keep-all; + overflow-wrap: normal; +} + .pt-bool-true, .pt-bool-false { display: inline-block; @@ -1020,7 +1036,8 @@ input:checked + .md-consent__settings { margin: 4em 0; } -.md-typeset .pfg-section > h2:first-child { +.md-typeset .pfg-section > h2:first-child, +.md-typeset .pfg-section > h3:first-child { margin-top: 0.7em; } @@ -1029,7 +1046,7 @@ input:checked + .md-consent__settings { color: var(--pt-heading-h3); } -.pt-fake-h2 { +.md-typeset .pt-fake-h2 { font-family: "Cormorant Garamond", serif; font-weight: 700; letter-spacing: -0.01em; @@ -1039,6 +1056,15 @@ input:checked + .md-consent__settings { color: var(--pt-heading-h2); } +.md-typeset .pt-fake-h3 { + font-weight: 700; + letter-spacing: -0.0125em; + font-size: 1.25em; + line-height: 1.5; + margin: 0.875em 0 1em; + color: var(--pt-heading-h3); +} + .pfg-diagram-frame, .pfg-diagram__plate { border: 6px double var(--pt-border-soft); diff --git a/mkdocs.yml b/mkdocs.yml index ad87466..57610b5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,8 @@ nav: - OpenCV: libraries/opencv.md - Desktop UIs: - Tkinter: libraries/tkinter.md + - Testing: + - pytest: libraries/pytest.md theme: name: material diff --git a/requirements.txt b/requirements.txt index a42c257..0b6b41c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ mkdocs-material pytest playwright pytest-playwright +codespell From 493d1c7bfda3623be02bfc6bb20ab5f07956132e Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Fri, 11 Sep 2026 20:16:07 -0700 Subject: [PATCH 09/10] new venv section --- docs/index.md | 2 + docs/workspace.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/docs/index.md b/docs/index.md index 702ea4b..375e3d4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,6 +28,8 @@ hide: [**`Terminal application`**](workspace.md#using-the-terminal-optional) + [**`virtual environments`**](workspace.md#using-a-virtual-environment-optional) + - :material-cube-outline:{ .lg .middle } [__Foundations__](foundations.md) Storing, displaying, and inputting values. diff --git a/docs/workspace.md b/docs/workspace.md index 9485755..5b03abe 100644 --- a/docs/workspace.md +++ b/docs/workspace.md @@ -230,3 +230,105 @@ It's good for running Python files that are already finished — either your own ++ctrl+c++
+
+ +## Using a virtual environment *(optional)* + +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:** + +- **Easy to share your setup** — save the exact libraries a project needs so someone else (or you, on another computer) can recreate it exactly +- **Safe to experiment** — try out a new library and delete it later without affecting anything else on your computer +- **Avoids permission problems** — installs into a folder you own, instead of needing admin access to install onto your whole computer +- **Keeps projects independent** — one project's installed libraries can't conflict with another's + +**To setup and run a virtual environment:** + +0. [Open the terminal](#using-the-terminal-optional) and navigate to your project folder + +1. Create a `venv` folder holding a private copy of Python and its libraries. This only needs to happen the first time you run your project. + + ```bash + python -m venv venv # or use python3, depnding on what you saw in Step 0 above + ``` + +2. Activate it, you need to do this every every time you open a new terminal window: + + === "macOS/Linux" + + ```bash + source venv/bin/activate + ``` + + === "Windows" + + ```bash + venv\Scripts\activate + ``` + + Your terminal prompt now starts with `(venv)`, showing the virtual environment is active. Forgetting to activate the virtual environment first means any commands will run against your system-wide Python instead. + +3. Install the libraries your project needs into the active virtual environment. First, install each library with `pip`: + + ```bash + pip install requests pandas + ``` + +4. Then save the exact versions you just installed to a file, so this same setup can be recreated later without remembering which libraries or versions you used: + + ```bash + pip freeze > requirements.txt + ``` + +5. This creates a `requirements.txt` file listing what you installed, you can open it to check. Alternatively, you can skip steps 4 and 5 by writing `requirements.txt` yourself in your code editor, it is a plain text file with one library per line. + + ``` + requests==2.31.0 + pandas==2.2.0 + ``` + + **Every time after that** — a different computer, a recreated `venv`, someone else running the project — you can now install all of the dependent libraries straight from the requirements file: + + ```bash + pip install -r requirements.txt + ``` + +4. Run your program the same way as before: + + ```bash + python script.py # or python3 + ``` + + No different from [running a file from the terminal](#using-the-terminal-optional) — as + long as the virtual environment is active, `python`/`pip` automatically point at its copy of + Python and its libraries instead of your system-wide one. + +5. Deactivate when you're done: + + ```bash + deactivate + ``` + +!!! danger "Never use `sudo` to fix a permission error" + If `pip install` fails with a permission error, it's almost always because the virtual + environment isn't activated — check for `(venv)` at the start of your prompt and run Step 2 + again. Running `sudo pip install` instead installs directly into your computer's system + Python, which some operating systems (Linux especially) depend on internally — overwriting + or mismatching one of those libraries can break unrelated system tools, sometimes badly + enough to require reinstalling the OS. + +!!! warning "Don't move, rename, or copy the project venv folder to another computer" + The `venv` folder stores absolute file paths pointing back to its own location. Moving or + renaming the project folder — or copying it to a different computer — silently breaks + activation. If that happens, delete the `venv` folder and repeat Step 1 to recreate it; + never move or copy `venv` itself. This is also why `venv` isn't something you back up or + share directly — share `requirements.txt` instead, and let each computer create its own. + +??? tip "Keep the venv folder out of version control" + If your project uses git, add `venv/` to `.gitignore`. It can contain thousands of files, + it's specific to your computer, and anyone else can recreate it in seconds from + `requirements.txt` — committing it just bloats the repository for no benefit. + +
+ From 8690440a61af90551b046eec2d6096c5db69766b Mon Sep 17 00:00:00 2001 From: Luka Sherman Date: Sat, 12 Sep 2026 01:47:53 -0700 Subject: [PATCH 10/10] accessibility patches --- docs/index.md | 2 +- docs/stylesheets/extra.css | 14 ++++++++++++++ docs/workspace.md | 6 +++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/index.md b/docs/index.md index 375e3d4..ed3f954 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,7 +28,7 @@ hide: [**`Terminal application`**](workspace.md#using-the-terminal-optional) - [**`virtual environments`**](workspace.md#using-a-virtual-environment-optional) + [**`virtual environments`**](workspace.md#virtual-environments-optional) - :material-cube-outline:{ .lg .middle } [__Foundations__](foundations.md) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index edac12b..3af33f6 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -336,6 +336,20 @@ input:checked + .md-consent__settings { mask-image: var(--md-admonition-icon--python); } +/* Material's own summary focus ring is gated behind a JS-added `.focus-visible` + class (an old keydown-tracking polyfill it ships), not the native + `:focus-visible` pseudo-class — `.md-typeset summary:not(.focus-visible)` + unconditionally zeroes the outline until that class shows up. Restore a + visible indicator via the native pseudo-class directly, same pattern used + for the run button and card links elsewhere in this file, so keyboard focus + on our custom run/ai/python collapsible admonitions is never silently blank. */ +.md-typeset .run > summary:focus-visible, +.md-typeset .ai > summary:focus-visible, +.md-typeset .python > summary:focus-visible { + outline: 0.15rem solid var(--pt-accent); + outline-offset: 0.15rem; +} + /* ---- Built-in "tip" admonition, darkened ---- Material's default tip color (#00bfa5) is a bright neon teal that clashes with this site's muted cream/ink/green palette — darkened the same way the diff --git a/docs/workspace.md b/docs/workspace.md index 5b03abe..43b05bf 100644 --- a/docs/workspace.md +++ b/docs/workspace.md @@ -232,7 +232,7 @@ It's good for running Python files that are already finished — either your own
-## Using a virtual environment *(optional)* +## Virtual environments *(optional)* 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. @@ -250,10 +250,10 @@ Sometimes you'll want to install [external libraries](./libraries/index.md) for 1. Create a `venv` folder holding a private copy of Python and its libraries. This only needs to happen the first time you run your project. ```bash - python -m venv venv # or use python3, depnding on what you saw in Step 0 above + python -m venv venv # or use python3, depending on what you saw in Step 0 above ``` -2. Activate it, you need to do this every every time you open a new terminal window: +2. Activate it, you need to do this every time you open a new terminal window: === "macOS/Linux"