Skip to content

Roadmap

Level: reference · for anyone adding a page

Status of every page, and the order things are planned in. A stub has a permanent URL and an outline but no runnable example yet; see CONTRIBUTING for what it takes to graduate one.

Companion page, added 2026-09-07. This one tracks the pages that exist; TODO.md ranks what to write next, built from the questions Adam asked while reading the Python docs — each one routed to the page that answers it, half-answers it, or does not exist yet. Nothing on that page has been through the gates that back a lesson, and it says so at the top.

The practice track, and the shape it was given

Added 2026-09-08: KATAS.md, nine katas and a gate. Three pages here explain bytes and the concept still would not stick, so the missing thing was repetition rather than a fourth exposition. Two shape decisions were made, and both are load-bearing enough to record here rather than only in a commit message.

A kata lives on the page for the topic it teaches, under ## Practice, with the answer folded into a <details markdown="1"> block — never in a katas/ folder of its own and never with its number in its own heading. Folders are permanent URLs; a sequence is the thing that gets reordered. So the ordering lives in one table in KATAS.md, the same reasoning that keeps sidebar order in NAV_ORDER rather than in filenames. The practice order there is deliberately not the sidebar order: it puts the four bytes katas first.

Every answer is printed by a program. The solution is examples/<stem>_kata_py.py beside the lesson's own example, pasted into the fold with <!-- output:<stem>_kata_py -->, so it runs in CI on Ubuntu, macOS and the 3.11 floor with everything else. tools/check_katas.py holds ten rules — including two this library needs and the sibling encodings library does not, because every example here is Python: the answer key's stem must say _kata (or it is the lesson's own output pasted twice), and its file must live under the page's own folder (or it is a fold copied from the page next door, which fills in perfectly and answers the wrong kata). --selftest breaks the gate fourteen ways — nine on a page, five on the index — and fails if any of them goes unreported.

An Anki deck follows, and it is not a rendering of these katas — a kata is a paragraph of prediction, a card is one fact in ten seconds, and mechanically converting one into the other produces cards nobody reviews twice. It waits on two stubs: Python::Bytes cards that cannot say what open() does without an encoding= are teaching half the subject. KATAS.md has the full reasoning and the trigger.

Chapter 1 — Text and bytes

Lesson Status Notes
str is not bytes written
String literals written, 2026-09-07 nine prefixes and the eight combinations that do not exist, five spellings of one code point, \x fixed at two digits against greedy octal in the same grammar, and the unrecognised escape that is kept — which is why '\bfoo\b' as a regex matches nothing and raises nothing; the Rust and C columns are a dated table, not examples (rustc 1.98.0, clang 21.0.0) — three answers to "where does an escape stop", and Rust's raw string is the stronger one
Encode and decode written covers the errors= policies including surrogateescape
Making a bytes object written, 2026-09-07 the four-job constructor; the other-language table is dated and measured locally, not an example — CI here is Python-only
bytearray is the mutable one written, 2026-09-07 the mutable half of the binary pair, and why there is no literal; the six-language table was run locally (rustc 1.98.0, go1.25.5, Swift 6.3.3, Node 20.20.2, clang 21.0.0) and is dated, not an example — CI here is Python-only. Java, C# and ABAP are prose
Opening a file written, 2026-09-08 prompted by perlfaq5 ↗. open() as four decisions of which only one is the encoding: truncation at open() rather than at the first write, a tell() cookie of 39 digits for a 6-byte file, stderr overtaking stdout into a pipe (shown against a real pty), five line counts from one 33-byte file, and os.replace not carrying the old file's permissions. The locale-independent problem was solved by printing sys.flags.utf8_mode and sys.flags.isolated rather than a codec name — locale.getencoding() and io.DEFAULT_BUFFER_SIZE are in a dated table because they differ by platform and by release. Key is byte-identical on macOS 3.14 and python:3.11/3.12/3.13/3.14-slim; the Rust and C rows are a dated table, not examples. One of the two stubs the Anki deck was waiting on; stdin, stdout and pipes is the other
Counting characters written
Slicing is not indexing written, 2026-09-08 the invariant s[:n] + s[n:] == s tested over 187 values of n across seven strings plus 10**30 rather than asserted, slice.indices() as the clamping rule that makes it true, and s[::-1] measured on a decomposed é, a flag and a ZWJ family — it reverses code points, so it moves a combining mark onto the wrong letter. Carries K9. The Rust and ABAP contrasts are prose and a link, not examples — Rust's range is bytes and panics on a char boundary, ABAP's s+3(5) is an offset and a length and is checked rather than clamped
Is it a letter? written, 2026-09-06 the twelve is* predicates; the Rust column is a dated table, not an example — is_alphabetic is the Alphabetic property where isalpha is the L* categories
Lowercasing is not folding written, 2026-09-08 the conversion half of Is it a letter? — 297 code points where casefold() is not lower(), the 102 that uppercase to more than one character and the single one that lowercases to two, final sigma making s.lower() context-dependent so ''.join(c.lower() for c in s) is a different function, and Turkish as the tailoring Python declines to take because there is no parameter to take it with. str.title() gets O'Brien right and Don'T wrong; string.capwords() does the reverse — the backlog's hook was backwards. The key is byte-identical on macOS 3.14 and python:3.11/3.12/3.13/3.14-slim, and the counts are stable from Unicode 14.0.0 to 16.0.0. The docs-citation finding and the Rust column are dated tables, not examples
repr is not str written, 2026-09-07 the documented isprintable() rule checked over the whole code space with zero disagreements, string.printable.isprintable() being False by design, and str(b'Zoot!'); the Rust column is a dated table, not an example — Rust's Debug escapes exactly what Python calls non-printable on all seven samples, and its is_printable is private. Section 8 (2026-09-10) calls the real sys.displayhook, which is what >>> runs, to show why ascii('café') is quoted twice at the prompt — key byte-identical on macOS 3.14 and python:3.11/3.12/3.13/3.14-slim
What ends a line written, 2026-09-07 ten boundaries scanned out of the whole code space, and the Bidi_Class B rule that predicts them; the cross-language ladder is a dated table, not an example — the three Java rows were measured 2026-09-10 on OpenJDK 25.0.4.1, each over the whole code space, and the .NET 6+ row is the only one left from a spec, since the .NET here is 5
strip is a set, not a prefix written, 2026-09-07 the three differences from removeprefix (repeat/once, set/string, both silent), the file-extension case where one filename in four survives, and the 29 code points strip() removes with no argument; the Rust column is a dated table, not an example — trim_start_matches takes a pattern, so the accident is not available, and trim() disagrees with strip() on U+001CU+001F
Four ways to find it written, 2026-09-08 four searches sorted by how they fail, not by what they find: find's truth value is inverted (0 falsy, -1 truthy) and -1 is a valid index, so a miss slices out the last character in silence; partition/rpartition fail in mirror image and the mirror is deliberate; start/end are slice notation, so end cuts a straddling match; partition('') raises, so it is not "the one that never raises". The backlog hook came out half wrong once measured — find(x) > -1 is correct, and the two shapes that actually fail are if s.find(x): and > 0. list.index and range.index were reworded in 3.14, so the key prints exception types there and not text. The Rust contrast is prose citing the sibling library's own recorded output, not a ported example. Key byte-identical on macOS 3.14 and python:3.11/3.12/3.13/3.14-slim
translate is a table, keyed by ordinal written, 2026-09-08 the table is a dict of ints on both sides, values of any length, None to delete, and any __getitem__ as a table — plus the one-pass property that chained .replace() lacks (html.escape in the stdlib needs a comment saying which line goes first) and the silent no-op of passing a str as a table. bytes.translate is documented here as a different method: 256-byte table, separate delete argument. The key is byte-identical on python:3.11/3.12/3.13/3.14-slim; the tr and Rust columns are dated tables, not examples (GNU coreutils 9.11, BSD tr, rustc 1.98.0) — Rust std has no equivalent at all
The format mini-language written, 2026-09-07 four doors onto one nine-slot grammar, the mix of numbering styles that raises, -0 from a number that is not negative and the z option that fixes it, the n type that mutates process-global locale state, and % as a separate language kept alive by bytes; the Rust column is a dated table, not an example — same spec string, same bytes, and format! is a macro so runtime templates do not compile
Padding is not alignment written, 2026-09-08 five methods and the five specs that duplicate them, the width argument that is a floor rather than a ceiling, center and ^ disagreeing by one space on odd widths, format('-42', '05') being '-4200', and the tab column that only LF and CR reset where splitlines() knows eight boundaries. The cell counts come from unicodedata.east_asian_width rather than from a terminal, so the key is byte-identical on macOS 3.14 and on python:3.11/3.12/3.13/3.14-slim despite four revisions of the Unicode database; Rust, C# and ABAP are prose and links, not examples
bin() is not the bits written, 2026-09-10 prompted by a screenshot of ascii(), bin(), bytes() and bytes.fromhex() at the prompt, and a Rust port of it whose {:#b} printed -9 as 32 bits. bin(), oct() and hex() checked equal to format(n, '#b'/'#o'/'#x') over -1000..1000; a negative prints sign and magnitude because an int has no width (-9 >> 1000 is still -1); a mask or to_bytes is the only way to the bits, and format(-9, '08b') is eight characters and not eight bits; int(s, 0) round-trips the sign but not a width; the docs' own bit_length() is a set strip that is right by construction. Carries K12. The Rust and ABAP halves are a dated table and prose, not examples. Key byte-identical on macOS 3.14 and python:3.11/3.12/3.13/3.14-slim
Comparison has a mode written, 2026-09-08 the word .NET forces and Python never says; the four résumé spellings, and the ASCII needle whose offset moves because a letter elsewhere was written as two code points; lower() against casefold() over the whole code space (297 disagreements, 172 of them folding upward to a Cherokee capital) and re.IGNORECASE as a third answer agreeing with neither; locale.strxfrm raising on an embedded NUL rather than ignoring it the way .NET's culture-sensitive comparison is documented to. The .NET and ABAP halves are prose, marked not machine-checked; the key is byte-identical on macOS 3.14 and python:3.11/3.12/3.13/3.14-slim
Normalization stub
Sorting is not comparing written
Standard in, standard out, and pipes stub the example has to run under a pipe to show the effect. The other stub the Anki deck is waiting on. Rescoped 2026-09-10 to the call half, BrokenPipeError and sys.stdout.buffer; its encoding questions went to the sibling, which had already measured them (see Open questions)
Filenames are not text written, 2026-09-06 the example touches no disk on purpose; APFS refuses invalid UTF-8 and is normalization-insensitive, not normalizing — both measured against Linux, both in a dated table rather than an answer key
What kind of file is this? written, 2026-09-07 five APIs, five questions; imghdr and sndhdr were removed in 3.13 so the byte-reading answer is now hand-rolled. The version split and the PermissionError that pathlib swallows are dated tables, not keys — the key is byte-identical on macOS 3.14 and python:3.11/3.12/3.13/3.14-slim
The string module written, 2026-09-08 the leftovers drawer: twelve public names, the nine constants sorted back into the ASCII chart they came from, capwords against title on where a word begins, and the measured reach of a user-supplied format string — '{0.__init__.__globals__[NAME]}' walks from any object to a module global, which is the argument for Template. The answer key is byte-identical on macOS 3.14 and python:3.11/3.12/3.13/3.14-slim; the Rust and C columns are a dated table, not examples — Rust's is_ascii_whitespace is five characters, not six, because it omits U+000B
The codecs registry written, 2026-09-07 the three things in codecs that are not a second spelling of str.encode: the _is_text_encoding flag behind the non-text codecs, the stateful incremental pair, and the ninth error handler. The chunked-decode bug and its silent errors='replace' version are both in the key; so is the encoder twin, where a per-chunk utf-16 encode leaves a U+FEFF in the data with no exception anywhere. The base64/zlib split is keyed on the round-trip check only, because what a decoder does with the malformed chunked form changes at 3.13 — the key itself is byte-identical on python:3.11/3.12/3.13/3.14-slim

Chapter 2 — Projects and environments

Lesson Status Notes
pyproject.toml written the standard tables, the [tool.X] rooms, workspaces, and tomllib
-c is not the prompt written, 2026-09-10 prompted by a real fish session: python3 -c 'chr(0x20AC)' printed nothing, and the same line unquoted never reached Python. The mechanism is the compile mode — 'exec' for -c, a file and a pipe, 'single' only at >>> — shown three ways, including python3 -i reading a pipe, which is the prompt with nobody typing and is therefore recordable. Also sys.path[0] == '' letting a json.py in the current folder shadow the stdlib (and -P/-I removing it), the ; that cannot precede a compound statement, and exit statuses. Two version splits are kept out of the key: 3.14 dedents the -c program, and 3.13 began quoting the source line in -c tracebacks. The shell half is dated tables, not examples — fish 4.3.2, zsh 5.9 and bash 3.2.57 each reject the unquoted line for a different reason, and fish alone treats \\ and \' as escapes inside single quotes. The Rust, C, Perl, Ruby and Node rows on who warns about a dropped value are dated too. Carries K13. Key byte-identical on macOS 3.14 and python:3.11/3.12/3.13/3.14-slim

Planned, and deliberately not yet folders: the virtual environment, sys.path and how an import finds a file, the lockfile, and PEP 723 ↗ inline script metadata. See the chapter page.

Chapters after this one

Not yet folders, deliberately — a directory of empty stubs is clutter, and every folder name is a permanent URL. Named here so the shape is visible:

  • The data model__len__, __eq__, __hash__, and why == and is are different questions
  • Sequences and iteration — iterators, generators, and the difference between lazy and eager
  • Functions — arguments, closures, decorators, and the mutable-default trap
  • Errors — exceptions as control flow, and what except Exception costs you
  • The standard library worth knowingpathlib, dataclasses, collections, itertools, functools
  • Testing — and why a recorded-output check like this library's own is not a substitute for one

Open questions

  • Where the boundary with the encodings library ↗ sits. Its 04_Python chapter has five stubs that overlap this one. Settled for one of them on 2026-09-08, and the phrasing generalises: the sibling owns the codec, this library owns the call. Opening a file keeps mode, buffering, the tell() cookie, line counting, replace-by-rename and the cross-language table; its sibling ↗ keeps which encoding the default resolves to, UTF-8 mode, PEP 686, sys.stdout.encoding on the way out, and EncodingWarning. Each now names the split at the top and links the other, and the loser of each half is a real page pointing at the winner rather than a deletion — a folder name is a permanent URL. The earlier guess in this bullet, that the sibling's stubs should simply point here, turned out to be half right: it holds for the call, and reverses for the codec. The remaining four stubs are still open, and normalization in particular may go the other way. Applied a second time on 2026-09-10, to a stub of this library's own: stdin, stdout and pipes had opened with encoding questions — an emoji raising under cron, sys.stdout.encoding differing down a pipe — that the sibling's A pipe is not a terminal ↗ had already measured and answered the other way. They went there; the stub keeps BrokenPipeError and sys.stdout.buffer. One question went over unanswered — PYTHONIOENCODING against sys.stdout.reconfigure(encoding=...) — and is recorded in the sibling's backlog.
  • Whether a lesson may ever name a third-party library. The rule is stdlib-only examples; sorting names PyICU in prose because omitting the correct production answer would be worse. That precedent should stay rare.