Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ Quick cheatsheet for basic Python.

**This is a casual and unpolished personal project, started in Aug '26.**

I wrote and built this from scratch, not as a complete Python language reference, but as a visualization of my mental model of how Python works.
I wrote and built this from scratch, not as a complete Python language reference, but as a **visualization of my mental model** of how Python works.

It started as a few quick-reference explanations on loops and lists for high-school intro-Python students working on their first independent projects, and evolved from there. I couldn't find a resource my students would consistently use that had:
It started as a few quick-reference explanations on loops and lists for high-school intro-Python students working on their first independent projects, and evolved from there. I couldn't find a resource my students would consistently use that had:

- simple explanations for beginners without technical jargon
- no advanced topics that intimidate or overwhelm beginners
Expand Down Expand Up @@ -123,6 +123,16 @@ A two-option [switch](docs/javascripts/essentials_toggle.js) that lets a reader

Each marking is independent — there's no shared list of "advanced" topics to keep in sync, just the attribute at each spot in the Markdown. State persists in `localStorage` and applies on every page (also settable via a `?simplified=true`/`false` URL param, for sharing a pre-set link). If a visible link points at a heading that's currently hidden (e.g. collections.md's cheat-sheet table linking to `#tuples`), following it flips the toggle back to Advanced and reveals the target instead of landing on nothing.

Some examples of content that is hidden while in "Essentials" mode, while a student is first learning to program:

- Collection types a beginner can often get by without (tuples, sets)
- OOP features past a basic class (method decorators, multiple inheritance, polymorphism, encapsulation, operator overloading, dataclasses, abstract base classes). OOP can already be a challenging topic, and they should first have a strong understanding of it before adding these features.
- Function features (type hints, positional-only/keyword-only parameters, recursion, decorators, generators)
- File-handling edge cases (seek and tell, the `"x"` create mode)
- Styling suggestions that aren't critical (file order, constants, quote style, indentation, comments, the truthy-check and `enumerate()` idioms)
- Workspace/tooling topics as most students are using an IDE (using the terminal, virtual environments)
- Efficiency, awareness of space and time resources, Big O notation

## Theme

### Custom CSS
Expand Down
1 change: 1 addition & 0 deletions STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ Pick the existing type that matches the branch, don't invent new ones without a
| `??? info` | Defining a term/concept adjacent to the page but not the topic itself. |
| `??? failure` | The negative counterpart to a `success` branch — "this didn't work, here's what to do about it" (e.g. workspace.md's "download Python here" branch when `python --version` doesn't show 3.x.x). |
| `??? ai` | Opinion/meta content specifically about learning with or around AI (e.g. index.md's FAQ tabs on whether/how to use AI while learning) — not used for teaching content about Python itself. |
| `??? efficiency` | A runtime/space aside naming the cost behind a choice already shown in prose (e.g. list vs. set membership, `sort()` vs. `sorted()`) — usually a Big O difference, occasionally a constant-factor one (`.get()` vs. two hash lookups, vectorized NumPy vs. a Python loop) where it's still worth flagging but doesn't change the O(...) class. Wrap it in `<div data-advanced="true" markdown="block">` on a page that participates in the Essentials/Advanced toggle (skip it on a page that doesn't, like the library reference pages), and close with a link to [style.md's "Efficiency"](docs/style.md#efficiency) section. Formalizes a tradeoff the surrounding prose already states in plain language; doesn't introduce the tradeoff for the first time. |
| `!!! example` | An always-open side-by-side comparison the reader is meant to see without a click, not a branch — e.g. "how to loop each type," showing every collection type's loop pattern in one visible table. |

Default to collapsed (`???`), not always-open (`!!!`) — an always-open admonition competes with
Expand Down
25 changes: 25 additions & 0 deletions docs/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,31 @@ print(burmese.species) # "burmese" — a separate copy, not shared

For a value every object should share instead of holding its own copy, see [class attributes](#class-attributes) below.

<div data-advanced="true" markdown="block">

??? efficiency "For efficiency, use __slots__ when creating many instances"
| | Time | Space (n instances) |
|---|---|---|
| Plain instance | — | <span class="pt-bigo pt-bigo--ok">O(n)</span> |
| `__slots__` | — | <span class="pt-bigo pt-bigo--ok">O(n)</span> (same class, smaller constant) |

Each instance normally keeps its attributes in a per-object `__dict__`, which costs some [memory](style.md#time-and-space) on top of the attribute values themselves — usually not worth worrying about, but it adds up when a program holds thousands or millions of instances at once. `__slots__` trades that flexibility for a fixed, lighter attribute layout:

```python-ref
class Snake:
__slots__ = ("species", "length_ft") # only these attributes are allowed, no __dict__

def __init__(self, species, length_ft):
self.species = species
self.length_ft = length_ft
```

An instance built from this class can no longer get a new attribute added after creation — `ball.venomous = False` raises `AttributeError`, since there's no `__dict__` left for it to go into.

See [Efficiency](style.md#efficiency) for why this distinction matters.

</div>

### Class attributes

A class attribute is set directly in the class body, outside `__init__` — shared by every object built from that class, unlike an [instance attribute](#instance-attributes), which is a separate copy per object. Assigning to `object.attribute` always creates (or updates) an instance attribute, even if a class attribute of the same name exists — it doesn't change the shared value, just shadows it for that one object.
Expand Down
68 changes: 68 additions & 0 deletions docs/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,32 @@ class diagram panel
See the [collections library page](libraries/collections.md) for the rest of `deque`'s
methods (`rotate()`, `maxlen=`, and more) and for the other list-adjacent tools it adds.

<div data-advanced="true" markdown="block">

??? efficiency "For efficiency, use append()/pop() instead of insert(0, x)/pop(0)"
| | Time | Space |
|---|---|---|
| `append()` / `pop()` | <span class="pt-bigo pt-bigo--good">O(1)</span> | <span class="pt-bigo pt-bigo--good">O(1)</span> |
| `insert(0, x)` / `pop(0)` | <span class="pt-bigo pt-bigo--ok">O(n)</span> | <span class="pt-bigo pt-bigo--good">O(1)</span> |

`append()` and `pop()` (from the end) run in constant [time](style.md#time-and-space) — one step no matter how long the list already is. `insert(0, item)` and `pop(0)` run in linear time, since Python has to shift every remaining item over.

See [Efficiency](style.md#efficiency) for why this distinction matters.

??? efficiency "For efficiency, sorted() copies the list; sort() doesn't"
| | Time | Space |
|---|---|---|
| `sort()` | <span class="pt-bigo pt-bigo--ok">O(n log n)</span> | <span class="pt-bigo pt-bigo--good">O(1)</span> |
| `sorted()` | <span class="pt-bigo pt-bigo--ok">O(n log n)</span> | <span class="pt-bigo pt-bigo--ok">O(n)</span> |

`sort()` rearranges the list **in place**, while `sorted()` builds and returns an entirely new one, so both copies sit in memory at once until the original is no longer needed.

Reach for `sort()` when the original order doesn't need to survive; `sorted()` when it does.

See [Efficiency](style.md#efficiency) for why this distinction matters.

</div>

</div>

<div class="pfg-section" markdown="block">
Expand Down Expand Up @@ -508,6 +534,20 @@ flowchart LR
snake.get("weight_lbs", 0) # 0 — key is missing, so the default is returned instead of None
```

<div data-advanced="true" markdown="block">

??? efficiency "For efficiency, use .get() instead of checking in first"
| | Time | Space |
|---|---|---|
| `if key in snake: snake[key]` | <span class="pt-bigo pt-bigo--good">O(1)</span> (two lookups) | — |
| `snake.get(key)` | <span class="pt-bigo pt-bigo--good">O(1)</span> (one lookup) | — |

`if key in snake: value = snake[key]` does two hash lookups — one to check membership, one to fetch the value. `snake.get(key)` does the same job in one. Both are O(1), so this isn't a [Big O](style.md#big-o-notation) difference, just avoided repeated work — worth reaching for out of habit once it's familiar, not worth restructuring existing code to chase.

See [Efficiency](style.md#efficiency) for why this distinction matters.

</div>

### Loop through a dictionary

- Looping **directly** over a dictionary gives you its keys, one at a time — the loop runs once for every key in the dictionary, and on each pass the loop variable, *(i.e. `key`)* is set to the next key.
Expand Down Expand Up @@ -659,6 +699,20 @@ flowchart LR
snakes["burmese"]["length_ft"] # 16
```

<div data-advanced="true" markdown="block">

??? efficiency "For efficiency, use a dict instead of a list to look up by key"
| | Time | Space |
|---|---|---|
| Dict `dict[key]` / `.get()` | <span class="pt-bigo pt-bigo--good">O(1)</span> | — |
| List of `(key, value)` tuples, searched by hand | <span class="pt-bigo pt-bigo--ok">O(n)</span> | — |

Looking up a key with `dict[key]` or `.get()` is O(1) — Python computes where to look directly, the same cost regardless of how many keys the dict holds. Storing the same data as a list of `(key, value)` tuples instead and searching for a match by hand is O(n) — worst case, checking every pair before finding it or coming up empty. That's the main reason to reach for a dict instead of a list when data needs to be looked up by a key.

See [Efficiency](style.md#efficiency) for why this distinction matters.

</div>

### Going further { data-card-link="skip" }

??? run "Practice with dictionaries"
Expand Down Expand Up @@ -1271,6 +1325,20 @@ These check a relationship between two sets and hand back a `bool`, rather than
list(set(species)) # ["burmese", "ball", "boa"] — order not guaranteed
```

<div data-advanced="true" markdown="block">

??? efficiency "For efficiency, use a set instead of a list for membership checks"
| | Time | Space |
|---|---|---|
| List/tuple `in` | <span class="pt-bigo pt-bigo--ok">O(n)</span> | — |
| Set/dict `in` | <span class="pt-bigo pt-bigo--good">O(1)</span> average | — |

Checking `in` on a list or tuple is O(n) — worst case, Python has to look at every item before it can say no. A set (and a dict, checking its keys) looks a value up directly instead of scanning, so `in` on either is O(1) on average, regardless of size. That's the "far faster" mentioned above, named precisely — it's also the reason converting a list to a set is a common move before doing a lot of membership checks against it.

See [Efficiency](style.md#efficiency) for why this distinction matters.

</div>

### Going further { data-card-link="skip" }

??? run "Practice with sets"
Expand Down
14 changes: 14 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,20 @@ finally:
print(f"found it: {length} ft") # runs only if try succeeded
```

<div data-advanced="true" markdown="block">

??? efficiency "For efficiency, use try/except when success is the common case"
| | Time | Space |
|---|---|---|
| `try` succeeds | <span class="pt-bigo pt-bigo--good">O(1)</span> | — |
| `try` fails | <span class="pt-bigo pt-bigo--good">O(1)</span> (larger constant, same class) | — |

A `try` block that succeeds costs almost nothing — Python doesn't pay for exception handling until an exception is actually raised. When one is raised, unwinding to the matching `except` has real overhead, more than an `if` check would. That makes `try`/`except` (checking after — sometimes called EAFP, "easier to ask forgiveness than permission") cheap for something expected to usually succeed, like the `float()` conversion above, and comparatively expensive as a substitute for an `if` check on something that fails often — checking first (LBYL, "look before you leap") avoids paying for exceptions that are more the rule than the exception.

See [Efficiency](style.md#efficiency) for why this distinction matters.

</div>

??? run "Run a try/except example"
A case where try/except is the right tool — converting a value that might not be a valid number:

Expand Down
16 changes: 16 additions & 0 deletions docs/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,22 @@ with open("notes.txt", "r") as file:
print(line.strip())
```

<div data-advanced="true" markdown="block">

??? efficiency "For efficiency, loop over a file instead of reading it all at once"
| | Time | Space |
|---|---|---|
| `.read()` / `.readlines()` | <span class="pt-bigo pt-bigo--ok">O(n)</span> | <span class="pt-bigo pt-bigo--ok">O(n)</span> |
| Loop over the file, line by line | <span class="pt-bigo pt-bigo--ok">O(n)</span> | <span class="pt-bigo pt-bigo--good">O(1)</span> |

`.read()`/`.readlines()` holds the entire file's contents in memory at once (O(n) [space](style.md#time-and-space)). Looping over the file object or calling `.readline()` repeatedly needs only enough memory for the current line, O(1) space regardless of file size.

For a small file it doesn't matter; for a file too large to comfortably fit in memory, it's the difference between the program running and it not.

See [Efficiency](style.md#efficiency) for why this distinction matters.

</div>

#### Seek and tell { data-advanced="true" }

`.tell()` returns the current position in the file, as a character count from the start. `.seek(position)` moves back to a given position, letting you re-read part of a file without closing and reopening it.
Expand Down
16 changes: 16 additions & 0 deletions docs/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,22 @@ Every recursive function needs two parts:
print("liftoff")
```

<div data-advanced="true" markdown="block">

??? efficiency "For efficiency, use a loop instead of recursion to save memory"
| | Time | Space |
|---|---|---|
| Loop | <span class="pt-bigo pt-bigo--ok">O(n)</span> | <span class="pt-bigo pt-bigo--good">O(1)</span> |
| Recursion, n levels deep | <span class="pt-bigo pt-bigo--ok">O(n)</span> calls | <span class="pt-bigo pt-bigo--ok">O(n)</span> stack |

Every call a function makes — recursive or not — adds a frame to the call stack and holds that call's local variables until it returns. A recursive function keeps every call's frame alive until the base case is reached, so its space cost is O(n) for n levels of recursion. That's why the `RecursionError` exists — Python caps how deep the stack can grow before it runs out of room.

A loop reuses the same frame each pass, O(1) [space](style.md#time-and-space).

See [Efficiency](style.md#efficiency) for why this distinction matters.

</div>

??? run "Run a recursion example"
All the examples above, combined into one script:

Expand Down
14 changes: 10 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,10 +539,6 @@ hide:

Readable Python code, and polished UI.

[**`checklist`**](style.md#script-style-checklist)

[**`linter`**](style.md#linter-tool)

[**`PEP 8`**](style.md#pep-8-style-guide):
[`blank lines`](style.md#blank-lines)
[`docstrings`](style.md#docstrings)
Expand All @@ -556,13 +552,23 @@ hide:
[`quote style`](style.md#quote-style)
{: data-advanced="true" }

[**`Linters, formatters`**](style.md#linters-and-formatters)

[**`Pythonic patterns`**](style.md#pythonic-patterns):
[`mutable defaults`](style.md#mutable-default-arguments)
[`is None`](style.md#is-none-instead-of-none)

[`truthy checks`](style.md#truthy-checks)
[`enumerate()`](style.md#enumerate-instead-of-range)
{: data-advanced="true" }

[**`Efficiency`**](style.md#efficiency)
[`big O`](style.md#big-o-notation)
[`common optimizations`](style.md#common-optimizations)
[`time`](style.md#time-and-space)
[`space`](style.md#time-and-space)
{: data-advanced="true" }

[**`Polished UX`**](style.md#polished-ux):
[`input validation`](style.md#input-validation)
[`menus`](style.md#menus)
Expand Down
12 changes: 12 additions & 0 deletions docs/libraries/numpy.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ lengths_m = lengths_ft * 0.3048
print(lengths_m)
```

??? efficiency "For efficiency, use vectorized operations instead of a Python loop"
| | Time | Space |
|---|---|---|
| Python `for` loop | <span class="pt-bigo pt-bigo--ok">O(n)</span> | — |
| Vectorized | <span class="pt-bigo pt-bigo--ok">O(n)</span> (same class, smaller constant) | — |

A Python `for` loop over a list and a vectorized NumPy operation both touch every element once — O(n) either way, the same [Big O](../style.md#big-o-notation) class.

The speed difference is a constant factor, not the order of growth: each pass of a Python loop pays the interpreter's per-iteration overhead, while a vectorized operation runs its loop once, in compiled C, underneath a single Python call. That overhead is small per element but adds up — the larger the array, the bigger the gap, even though neither approach's growth rate has changed.

See [Efficiency](../style.md#efficiency) for why this distinction matters.

### Aggregating an array

Collapses an entire array down to a single summary number. `.mean()`, `.max()`, `.min()`, and `.sum()` — the same idea as Python's built-in `sum()` and `max()`, but computed directly on the array without converting it back to a list first.
Expand Down
10 changes: 10 additions & 0 deletions docs/libraries/re.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ print(match.group())
length_pattern.search(note).group() # "12ft"
```

??? efficiency "For efficiency, compile the pattern once with re.compile()"
| | Compilation cost (over a loop of n calls) |
|---|---|
| Recompiled every pass | <span class="pt-bigo pt-bigo--ok">O(n)</span> |
| Compiled once, reused | <span class="pt-bigo pt-bigo--good">O(1)</span> |

Calling `re.search()` (or `.findall()`, `.sub()`, etc.) with a raw pattern string repeats the same compilation work internally on every call, even when the pattern never changes — across a loop of n calls, that's n compilations of the same pattern. Compiling it once with `re.compile()` above the loop and calling `.search()` on the result instead does that work exactly once, however many times the loop runs.

See [Efficiency](../style.md#efficiency) for why this distinction matters.

??? run "Run a searching example"
All the examples above, combined into one script:

Expand Down
Loading
Loading