Skip to content

Latest commit

 

History

History
659 lines (436 loc) · 48.2 KB

PROJECT_BOARD.md

File metadata and controls

659 lines (436 loc) · 48.2 KB

ThinkPython Project Board

Numbered tasks for tracking work. Each task has a permanent number; add new tasks at the end. Update status as work progresses.

Current focus (2026-10-30)

  • Task 1: Chapter 18 Counter example produces nonsense output (#78) — done; published, answered, closed.
  • Task 2: Chapter 17 has_pair exercise gives the wrong outline (#76) — done; published, answered, closed.
  • Task 3: Student notebooks ship with every output stripped (#79) — done; book-wide audit found nothing beyond Chapter 3 (2026-11-06), and keep now tagged in Chapters 4–18.
  • Task 4: Project Gutenberg downloads make CI fail intermittently — done; the texts are hosted in this repo.
  • Task 5: Five chapters are never tested; CI is on deprecated actions and one Python version — done; the outputs check landed 2026-10-14 as check_outputs.py.
  • Task 6: Add dataclass coverage to Chapter 18? (#77) — declined 2026-11-06; answered and closed.
  • Task 7: Reply to the third-party interactive edition of Chapter 3 (#74) — done; answered and closed by Allen.
  • Task 8: Repo hygiene and release tooling — done 2026-10-30.
  • Task 9: Jupyter Book 2.0 migration — deferred (Jan 2026 decision).
  • Task 10: Windows readers cannot run chapters 8, 11 and 12 — done; encoding='utf-8' added, with a remove-print note.
  • Task 11: No Colab link on any chapter page (plan) — done; 18 links, published.
  • Task 12: Page-view counter on the HTML book — live.
  • Task 13: Search engine optimization — done bar two follow-ups: Allen's Search Console submission, and the 6-week / 3-month comparisons (due ~2026-12-01 and ~2027-01-19).
  • Task 14: First edition (Python 2): source, releases — done; 2.0.18 released 2026-10-30.
  • Task 15: Rename the first-edition branch master → v1 — done 2026-10-30.
  • Task 16: Quizzes that LMSs can import (plan) — in progress; questions fixed, published to quizzes/ 2026-11-06; website page live; Moodle and Brightspace untested.
  • Task 17: Teaching materials for instructors (plan) — planning; first step, list what teachers have asked for.
  • Task 18: Archive and mine the old Software Design materials (plan) — planning; sources surveyed (most on an external drive), 418 questions cataloged; every chapter mined into a verified private bank (ThinkPythonTeaching/bank/), 10 questions added to the public quizzes; feeds Task 17.

No issues or PRs are open (2026-11-06), in this repo, ThinkPythonSolutions or ThinkPython2.

How this repo fits together

ThinkPythonSolutions/soln/*.ipynb is the canonical source: it holds the prose, the code, and the executed outputs. Everything else is generated from it by a prep_notebooks.py + build.sh pair:

Target Built by What the prep step does
chapters/ (student notebooks, and ThinkPythonNotebooks.zip) chapters/build.sh Replaces solution source with # Solution goes here, clears tags, clears all outputs
blank/ blank/build.sh Clears the source of every code cell except those tagged keep, clears all outputs
jb/ → GitHub Pages jb/build.sh Blanks solution source, strips %%expect, adds MyST labels; keeps outputs

So a content fix belongs in ThinkPythonSolutions/soln/, and reaches readers only after the relevant build.sh is re-run. The published HTML and the downloadable notebooks are prepped differently, which is the direct cause of Task 3.

Editing a chapter

Edit through jupytext rather than the notebook JSON, and keep the .md afterward:

cd ThinkPythonSolutions/soln
jupytext --to md chapNN.ipynb          # re-export first, so the md is fresh
#  ... edit chapNN.md ...
jupytext --update --to ipynb chapNN.md # --update preserves stored outputs

--update merges into the existing notebook; plain --to ipynb would discard the outputs the website renders.

The round-trip was verified once on this repo — byte-identical, with tags (solution, remove-cell, keep) and %%add_method_to magics surviving intact. Treat that as settled; there is no need to re-check it after each conversion.

The .md stays on disk, untracked, as a recovery copy: an open Jupyter session saving over a chapter will silently revert edits back to HEAD, and re-applying from the markdown is far cheaper than redoing the work. Two cautions: always re-export before editing, or a stale .md pushed back with --update will clobber edits made in Jupyter; and close the Jupyter tab for a chapter before editing it here.


Task 1: Chapter 18 Counter example produces nonsense output

Status: Done 2026-09-19 (5295441) — published, answered and closed.

Reported by: @alchemistcai in #78, 2026-08-16

Location: ThinkPythonSolutions/soln/chap18.ipynb, cells 39–48 (In[21], In[22], In[26] in the published numbering)

Confirmed: The Counter section introduces counter = Counter('banana'), then rebinds counter = Counter(t) where t = (1, 1, 1, 2, 2, 3) to demonstrate that a Counter accepts any sequence. Several cells later, the + operator is demonstrated with counter2 = Counter('bans'); counter + counter2. Because counter is still the integer version, the stored output is:

Counter({1: 3, 2: 2, 3: 1, 'b': 1, 'a': 1, 'n': 1, 's': 1})

The surrounding prose says the + operator "contains the keys from both and the sums of the counts" — a point the reader cannot see in a result that mixes integers with letters and sums nothing. The example clearly intends Counter('banana') + Counter('bans'), which would give Counter({'a': 4, 'n': 3, 'b': 2, 's': 1}) and actually show a sum.

Cause: an extra cell added to test an example and never removed. It has been deleted (25 lines out of soln/chap18.ipynb); counter is now Counter('banana') at the point of the + demonstration.

Resolved

The notebook has since been re-executed, so source and stored output now agree. Cell 47 reads:

counter2 = Counter('bans')
counter + counter2

and now shows Counter({'a': 4, 'n': 3, 'b': 2, 's': 1}), the summed counts the prose describes, in place of the old Counter({1: 3, 2: 2, 3: 1, 'b': 1, 'a': 1, 'n': 1, 's': 1}). Verified live in the published chap18.html.

Scope

  • Remove the stray test cell that rebound counter
  • Re-execute soln/chap18.ipynb so the stored output shows the summed counts
  • Check the subtraction / union / intersection follow-ons in the same section for the same stale-binding problem
  • Rebuild chapters/, blank/, jb/
  • Reply on #78 and close

Task 2: Chapter 17 has_pair exercise gives the wrong outline

Status: Done 2026-09-19 (5295441) — published, answered and closed.

Reported by: @alchemistcai in #76, 2026-06-23

Location: chapters/chap17.ipynb cells 189–196 (In.91 in the published numbering); source in ThinkPythonSolutions/soln/chap17.ipynb

Confirmed: In soln/, the exercise is three cells — a check_sets stub, the check_sets body (tagged as a solution), and has_pair (tagged as a solution) — followed by three test cells that call pair.has_pair(), bad_hand.has_pair(), good_hand.has_pair().

The prep step blanks the two solution cells, so the student notebook hands the reader this scaffolding:

%%add_method_to PokerHand

    def check_sets(self, *need_list):
        return True

and then immediately asks them to run pair.has_pair(). has_pair is never mentioned as something to write, and the one outline they are given is for a different method. The reader's reading is correct.

Resolution: The outline now reads def has_pair(self): return True, matching the tests below it. check_sets is an implementation detail of the author's solution, so the reader meets it only if they read that solution — and it now carries a docstring explaining the *need_list convention, which is not self-evident:

def check_sets(self, *need_list):
    """Checks whether this hand contains sets of cards with the same rank.

    Each argument is the size of a set we need, largest first.
    So check_sets(2) checks for a pair, check_sets(2, 2) checks for
    two pairs, and check_sets(3, 2) checks for a full house.

    need_list: sizes of the sets we need, in decreasing order
    """

The "largest first" requirement is load-bearing and was previously undocumented: need_list is zipped against the rank counts sorted descending, so check_sets(3, 2) recognizes a full house but check_sets(2, 3) does not.

Scope

  • Decide the intended shape: the outline now gives has_pair(self)
  • Fix the scaffolding cell in soln/chap17.ipynb
  • Document check_sets so the solution is readable on its own
  • Sweep the rest of Chapter 17's %%add_method_to exercises for the same stub/test mismatch (has_full_house uses the same pattern)
  • Rebuild and publish
  • Reply on #76 and close

Task 3: Student notebooks ship with every output stripped

Status: Done. Chapter 3 fixed 2026-09-19 (5295441) — published, answered and closed. Audit and keep tagging finished 2026-11-06.

Reported by: @goekce in #79, 2026-09-18

Context: A reader following Chapter 3 in the downloadable notebooks hit this prose:

Here's an example of a rectangle with width 5 and height 4, made up of the string 'H'.

...with nothing after it. The rendered rectangle is visible at allendowney.github.io/ThinkPython/chap03.html, so the website and the notebooks disagree about what the reader gets.

Root cause: The two prep scripts treat outputs differently. jb/prep_notebooks.py blanks the source of solution cells and leaves outputs alone, so the site still renders what the code printed. chapters/prep_notebooks.py does this to every cell unconditionally:

# remove output
if 'outputs' in cell:
    cell['outputs'] = []

Result: 0 of 1370 code cells across chapters/chap*.ipynb have any stored output. In the Chapter 3 case the rectangle('H', 5, 4) call survives (it is tagged keep), but the function that defines rectangle is # Solution goes here and the call has no stored result — so the reader sees a promise of an example, a blank, and a call to a function that does not exist yet.

This is a policy question, not just a bug: blank notebooks are the point, and pre-filling outputs would spoil exercises. But worked examples the prose refers to are not exercises, and today there is no way to mark one.

The keep tag: what it actually does

Update 2026-11-06: much of what follows is out of date. Task 8 merged the prep scripts into prep_notebooks.py, whose blank mode does honor keep, and CLAUDE.md now warns that losing the tags empties the cells readers need. The real gap was coverage: Chapters 4–19 had no keep tags, so their blank/ notebooks had no code at all, not even the cell that downloads thinkpython.py, diagram.py and jupyturtle.py, and a follow-along reader's first import failed. 63 cells in Chapters 4–18 are now tagged keep, matching Chapters 1–3: the download cell, the thinkpython/autoreload cell, the %xmode cell and the diagram-drawing cells. (Chapter 19 has no code.)

The original 2026-09-19 notes:

Investigated 2026-09-19, because the tag's presence suggested a mechanism that might already solve this. It does not.

  • Exactly one script reads it: blank/prep_notebooks.py, which preserves the source of a code cell instead of emptying it. Neither chapters/prep_notebooks.py nor jb/prep_notebooks.py mentions keep. So for the downloadable notebooks — the ones #79 is about — the tag has no effect whatsoever.
  • It does not preserve outputs. Even in blank/, outputs are cleared unconditionally. keep means "keep the code," never "keep the result."
  • It was never finished. All 18 keep tags in the book are in chapters 1, 2, and 3 (2, 4, and 12 respectively). Chapters 4–19 have none.
  • Two of them are on markdown cells (chap03 cells 77 and 79), where the blank/ script cannot act on them at all, since it only tests cell['cell_type'] == 'code'.

Read together: an abandoned experiment, scoped to the first three chapters, that solves a different problem (keeping code in blank/) than the one in #79.

Scale: a scan for the "prose promises an example, solution cell follows" pattern found it only in Chapter 3 — three instances, all fixed below. The scan keyed on specific phrasings, so a wider audit is still worth doing, but this is not the book-wide problem it first looked like.

Fix applied (Chapter 3)

Per the "keep it simple" decision: put the expected output in a preformatted block in the markdown, so the example does not depend on the solution having been run. This works in every target at once — soln/, chapters/, blank/, and the website — and needs no change to any prep script.

Exercise Block added
print_right (cell 66) three right-aligned lines
triangle (cell 68) the 5-level pyramid
rectangle (cell 71) the 4×5 rectangle — the case reported in #79

Verified by running chapters/prep_notebooks.py over the result: the reader now gets the rectangle, a # Solution goes here cell, and the rectangle('H', 5, 4) call.

Two solution bugs found and fixed while doing this, both of which would have contradicted the new blocks:

  • triangle looped over range(height+1), printing string * 0 first — so "a pyramid with 5 levels" emitted a leading blank line and six lines of output. Now range(1, height+1).
  • print_right's docstring said "column 70" while the code used columns = 40 (the exercise asks for 40).

Scope

  • Decide the rule: expected output goes in a preformatted block in the prose, not in a cell's stored output
  • Apply it to the three Chapter 3 exercises, including the one named in #79
  • Establish what keep does and whether it was usable here (it is not)
  • Re-execute soln/chap03.ipynb so the stored output matches the corrected triangle
  • Wider audit for other exercises whose prose promises a result the reader cannot see — 2026-11-06: scanned all chapters for prose that promises an output just before a solution cell without showing it; the only hit (Chapter 11) states the result inline. Nothing to fix beyond Chapter 3
  • Decide the fate of the keep tags — finished the idea: 63 cells tagged in Chapters 4–18 (2026-11-06)
  • Rebuild chapters/, regenerate ThinkPythonNotebooks.zip, publish the site
  • Reply on #79 and close

Out of scope (for first pass)

  • Restoring outputs for exercise solutions (that would hand readers the answers)
  • Teaching chapters/prep_notebooks.py a keep-output tag — the preformatted-block approach makes it unnecessary
  • Changing how the Jupyter Book build handles outputs — the site is correct today

Known cosmetic cost: on the website, the preformatted block and the executed output of the call cell now both appear. Tagging the call cells remove-output would suppress the duplicate, at the cost of changing pages that currently look right.


Task 4: Project Gutenberg downloads make CI fail intermittently

Status: Done 2026-10-14 — the three texts are now hosted in the ThinkPython repo.

Resolution: pg345.txt, pg1184.txt and pg43.txt are committed at the top level of ThinkPython, alongside words.txt and photos.zip, and chapters 8, 11 and 12 download them from there. Gutenberg blocks datacenter IPs by design, so fetching from them on every CI run and every reader was the problem, not bad luck.

A second reason emerged while doing it: all three files had drifted. Each differs from what Gutenberg serves today — pg43 has been retitled, "ebook" recapitalised, trailing whitespace stripped, and is 46 lines shorter. Readers downloading today were already getting different text from the one the stored outputs were computed against, so chapter 12's word counts could not reproduce. The hosted copies are the ones the book was built from.

chapter 8's !wget was also replaced with the download() helper the rest of the book uses; wget is not present on stock Windows or macOS.

Verified end to end: with the local copies removed, chapters 8, 11 and 12 fetch from the new URLs and pass.

Superseded scope

Context: The scheduled monthly test run for ThinkPythonSolutions failed on 2026-07-01 (run 28486298964) with:

FileNotFoundError: [Errno 2] No such file or directory: 'pg345.txt'
FAILED chap08.ipynb::chap08.ipynb

pg345.txt is Dracula, downloaded from gutenberg.org at notebook-execution time. The FileNotFoundError is the symptom, not the cause: the download() helper fetched nothing (Gutenberg throttles and blocks datacenter IPs), and the next cell tried to open the file that was never written. Every run before and since has passed, so this is a live flake, not a break.

Exposure: chap08, chap11, and chap12 all read Project Gutenberg texts. Runs are monthly, so a flake rate low enough to look like noise still means a red build several times a year — and a red build nobody trusts is a red build nobody reads.

Scope

  • Make download() fail loudly when the fetch fails, instead of leaving the next cell to raise FileNotFoundError on a missing file
  • Decide the CI strategy: retry with backoff, mirror the three texts under a stable URL we control, or mark Gutenberg-dependent notebooks and let them fail soft in CI
  • Confirm the repo's other runtime downloads (words.txt, structshape.py, jupyturtle.py — all on raw.githubusercontent.com) are not exposed the same way
  • Turn on a failure notification for the scheduled run

Out of scope (for first pass)

  • Removing the live downloads from the book. They are pedagogically deliberate, and Chapter 8 is about reading a real text file.

Task 5: Five chapters are never tested; CI is on deprecated actions

Status: Done. CI modernization 2026-09-19 (34bc313); the outputs check landed 2026-10-14 as check_outputs.py, wired into make tests. Windows rejoined the matrix 2026-10-14 and is green.

Context: ThinkPythonSolutions/Makefile runs the test suite as two globs:

tests:
	# testing notebook 04 takes too long
	cd soln; pytest --nbmake chap0[12356789]*.ipynb
	# testing notebook 12 fails on windows (unicode!)
	cd soln; pytest --nbmake chap1[1345678]*.ipynb

That covers 01–03, 05–09, 11, 13–18. Chapters 00, 04, 10, 12, and 19 are never executed by CI. Two of the exclusions are documented (04 is slow, 12 had a Windows Unicode problem); 00, 10, and 19 are silently outside the globs, and the glob encoding makes it easy to miss that.

The Windows exclusion for chapter 12 is also obsolete: the matrix is [ubuntu-latest, macos-latest] with the comment "don't test on windows", so 12 is excluded from a platform that is not tested anyway.

CI configuration (ThinkPythonSolutions/.github/workflows/tests.yml):

  • actions/checkout@v2 and actions/setup-python@v2 — both run on Node 20, deprecated and being force-migrated by GitHub
  • One Python version, 3.10, released October 2021; the book teaches current Python
  • No fail-fast: false, so one leg's failure hides the others
  • No pip caching; each run reinstalls the full Jupyter stack (~2.5 min)

What the excluded chapters actually cost

Measured before changing anything, on Python 3.10:

Chapter Time Result Why it was excluded
chap00 3s pass nothing — outside the glob
chap10 5s pass nothing — outside the glob
chap19 2s pass nothing — outside the glob
chap12 4s pass "fails on windows (unicode!)" — real, but Windows is not in the matrix
chap04 58s pass "takes too long"

Four of the five cost under five seconds between them. Chapter 4 is genuinely the slow one, but the full 20-chapter suite still runs in 2m37s — so the exclusions were buying almost nothing.

Chapter 4 is slow because jupyturtle sleeps TURTLE_DELAY (0.2s) after every visual command, and chapter 4 draws a lot. Eleven of its make_turtle() calls use the default delay; four already pass delay=0. Dropping the delay everywhere would cut the suite by a third, but it would also remove the animation that makes the turtle chapter work as teaching, so it was left alone.

Resolution: a plain chap*.ipynb glob, so a new chapter cannot be missed, plus --durations=5 to keep slow chapters visible. CI moved to checkout/setup-python v7, Python 3.12, fail-fast: false and pip caching. Both legs pass.

Scope

  • Replace the globs — used chap*.ipynb, which is stronger than a list: nothing can be omitted
  • Get 00, 10, and 19 into the suite
  • Re-test chapter 12 and drop the stale Windows comment
  • Decide on chapter 04 — measured at 58s and included
  • Bump checkout and setup-python to v7; Python 3.12; fail-fast: false; pip caching
  • Add an outputs check: a soln/ notebook committed with execution_count: null throughout should fail the build (--nbmake executes notebooks, so stored outputs are invisible to it — this is the check that catches a chapter committed unexecuted)

Task 6: Add dataclass coverage to Chapter 18?

Status: Declined 2026-11-06; answered and closed. A section was drafted and dropped: @dataclass needs type hints, so one example would mean introducing both decorators and type hints, and the alternative, make_dataclass, works without them but is not idiomatic.

Requested by: @alchemistcai in #77, 2026-06-30

Context: Chapter 18 covers namedtuple and Counter from the standard library. The request is to add dataclasses alongside namedtuple: in the standard library since 3.7, mutable, supports methods, and less boilerplate than a hand-written class — a natural complement to the immutable namedtuple.

The counter-argument is scope. Chapter 18 is the "Python extras" chapter near the end of a book that has already taught classes the long way, and a third edition is not a place to add topics without a reason. The decision is whether dataclass is now common enough in real code that a reader who finishes this book and does not recognize it has a gap.

Scope

  • Decide: full section, a short aside next to namedtuple, an exercise, or decline — declined
  • If adding: draft, execute, and keep the chapter's length in check
  • Reply on #77 either way

Task 7: Reply to the third-party interactive edition of Chapter 3

Status: Done 2026-09-19 — Allen replied and closed the issue.

Context: @ling-k in #74 (2026-04-22) built an interactive edition of Chapter 3 — live Python execution in the exercise blocks plus a chapter-grounded AI tutor — at xlearnhub.com, under CC BY-NC 3.0, with more chapters in progress. They made no ask and offered to take it down or change anything.

Open since April with no reply. Two things worth separating: whether the attribution and license terms are met (the book is CC BY-NC, so a non-commercial derivative with attribution is within the license), and whether this is something to link from the book's home page or leave alone.

Scope

  • Look at the demo and check the attribution and license notice
  • Decide whether to acknowledge, link, or simply thank and close
  • Reply on #74

Task 8: Repo hygiene and release tooling

Status: Done 2026-10-30

Context: git status on v3 reports 58 untracked paths, which makes it useless as a signal — a real change is invisible in the noise. The causes are separable:

Generated files that are neither tracked nor ignored. jb/*.ipynb (all 20 chapters), chapters/thinkpython.py, chapters/diagram.py, chapters/structshape.py, chapters/words.txt, chapters/photos.zip, blank/* support files — all copied in by a build.sh on every build. .gitignore is the stock Python template and mentions none of them. They should be ignored (if generated) or tracked (if not); right now they are neither.

Build scripts that are half-tracked. chapters/build.sh, jb/build.sh, and jb/prep_notebooks.py are in version control. The top-level build.sh, blank/build.sh, blank/prep_notebooks.py, and chapters/prep_notebooks.py are not — so the script that causes Task 3 has no history and exists only on this machine.

Four near-identical copies of the same script. prep_notebooks.py exists in chapters/, blank/, and jb/, differing in a handful of lines (see the table at the top). A fifth file, chapters/prep_notebook.py, is singular and separate. A fix to one does not reach the others.

Stale artifacts at top level. ThinkPython3Notebooks.zip (517 KB, March 2024) predates the rename to ThinkPythonNotebooks.zip; ThinkPythonSolutionsNotebooks.zip (557 KB, July 2024) is 14 months stale against a solutions repo that has moved since. Both are tracked, so both are served to anyone who clones. Turtle.py (20 KB, January 2024) is tracked and referenced by nothing — Chapter 16 uses jupyturtle. du is 17 KB of captured du output, untracked, sitting at the top level.

The same problem in ThinkPythonSolutions. git status there reports ~50 untracked paths in soln/ alone: __pycache__/, .ipynb_checkpoints/, downloaded corpora (pg345.txt, pg1184.txt, pg43.txt and seven derived files), scratch notebooks (testing_zone.ipynb, color_explorations.ipynb), an old/ directory, and generated .dbm files. The downloads in particular are produced by running the notebooks, so they reappear after every test run.

nbdime is configured but not installed. git diff on any notebook in ThinkPythonSolutions fails outright:

ModuleNotFoundError: No module named 'nbdime'
fatal: external diff died, stopping at soln/chap17.ipynb

The diff driver is set in git config, so every notebook diff needs --no-ext-diff to work at all. Either install nbdime or drop the driver.

No CLAUDE.md. The traps in this repo are not guessable: soln/ is canonical and everything else is generated; jb/build.sh ends in ghp-import -n -p -f and publishes to GitHub Pages with no confirmation; the three prep scripts diverge in ways that matter; ThinkPythonSolutions is a separate git repo nested inside this one, so a content fix and its rebuild are two commits in two repos.

Scope

  • Write a real .gitignore in both repos: generated notebooks, copied support files, downloaded corpora, __pycache__/, .ipynb_checkpoints/
  • Install nbdime or remove the git diff driver that depends on it
  • Track every build and prep script, or move them somewhere that is tracked
  • Consolidate the prep_notebooks.py variants — chapters/ and blank/ now share the top-level prep_notebooks.py (chapters|blank); verified byte-identical output on all 20 chapters in both modes. jb/'s stays separate: it keeps tags and outputs, adds MyST anchors and writes the page descriptions, and shares no logic with the other two.
  • Deleted ThinkPython3Notebooks.zip, ThinkPythonSolutionsNotebooks.zip, Turtle.py and du. The "solutions" zip never held solutions — it contained the same chapters/*.ipynb as the student zip, from July 2024 — so it was dropped rather than regenerated. Turtle.py's only reference was a comment inside itself.
  • Commit planning/ or move it out of the working tree
  • Write CLAUDE.md covering the hazards above
  • Document the release ritual end to end — update.py does it in one command, and CLAUDE.md describes it

Out of scope (for first pass)

  • Merging ThinkPythonSolutions into this repo or converting it to a submodule
  • Changing the GitHub Pages deployment mechanism

Task 9: Jupyter Book 2.0 migration

Status: ⏸️ Deferred indefinitely (decision recorded 2026-01-12)

Context: A full migration plan from Jupyter Book 1.x to 2.0 was researched and written in January 2026, then shelved. The reasoning, from planning/jupyter_2_plan.md:

Too many critical unknowns (notebook execution settings, CC license footer, MyST extensions configuration) combined with a perfectly functioning JB 1.x setup. No compelling reason to risk breaking a working system.

The current jb/ build uses _config.yml + _toc.yml with thirteen MyST extensions, a custom MathJax configuration, a Creative Commons license footer, and execute_notebooks: 'off' — none of which have a verified 2.0 equivalent. The reference implementations that prompted the idea (BayesFertility/jb, EqualityWEF/HALE/jb) are much smaller books.

Preserved planning material (in planning/, currently untracked — see Task 8):

File Contents
jupyter_2_plan.md Full phased plan, decision record, risk summary
config_mapping.md _config.yml + _toc.yml → myst.yml, mapped field by field
issues_analysis.md Risk-ranked analysis of what breaks under 2.0
file_organization_plan.md Proposed jb2/ layout for a side-by-side migration

Revisit if: Jupyter Book 1.x stops receiving fixes, a dependency conflict forces the issue, or a 2.0-only feature becomes worth the risk.

Scope (only if reopened)

  • Re-validate config_mapping.md against the then-current 2.0 release
  • Build jb2/ side by side, leaving jb/ untouched until the output matches
  • Confirm the unknowns: execution settings, CC footer, MyST extension parity, MathJax config

Task 10: Windows readers cannot run the Project Gutenberg chapters

Status: Done 2026-10-14 — encoding='utf-8' added to every read and write of a Gutenberg-derived file in chapters 8, 11 and 12.

Resolution: The decision was to teach the argument rather than hide the problem. Each affected chapter now carries a short note before the first instance, explaining that the argument is not in the printed book and is needed on Windows. The notes are tagged to match the visibility of the code they explain: remove-print in chapters 8 and 12, and remove-cell in chapter 11, whose whole Dracula section is notebook-only.

The writers were included, not just the readers: writing non-ASCII to a cp1252 default fails the same way. words.txt was deliberately left alone — it is ASCII, so it reads correctly under either encoding, and it is the file the simple teaching examples use.

Still open: adding windows-latest back to the CI matrix to confirm this from the outside rather than by reasoning about cp1252.

Original analysis

Context: Surfaced while doing Task 5, from the old Makefile comment "testing notebook 12 fails on windows (unicode!)". The comment was right about the symptom and wrong about the scope.

The book teaches file reading in its simplest form — open(filename), with no encoding argument. No open() call anywhere in the book passes one. In text mode Python then uses the locale's preferred encoding, which on a default Windows install is cp1252. The Project Gutenberg texts are UTF-8, and they contain bytes that cp1252 does not define:

pg43.txt     UnicodeDecodeError -- byte 0x9d at offset 2295
pg345.txt    UnicodeDecodeError -- byte 0x9d at offset 1883
pg1184.txt   UnicodeDecodeError -- byte 0x9d at offset 6927

words.txt is unaffected — it is ASCII, so it decodes without error under either.

Who this hits: chapters 8, 11 and 12 — not just 12, as the comment implied. This is not a CI artifact. A reader on Windows who downloads the notebooks and runs chapter 8 gets a traceback on the first open(), in the chapter that introduces reading files.

Why it is still open: the fix is an editorial call, not a technical one.

  • Adding encoding='utf-8' to the affected open() calls fixes readers and CI together, but introduces a parameter the chapter has not taught yet, in the exact example meant to show how simple reading a file is.
  • Setting PYTHONUTF8=1 in CI would turn the Windows leg green while leaving readers exactly as broken. That is worse than not testing Windows, which is why the matrix still excludes it.
  • The problem self-resolves on Python 3.15, where UTF-8 mode becomes the default (PEP 686). That is a real argument for waiting, and no argument at all for readers on today's Python.

Scope

  • Decide whether the book teaches encoding='utf-8' when reading downloaded text, and if so where it is introduced
  • Apply it to chapters 8, 11 and 12
  • Add windows-latest back to the CI matrix once the chapters pass without PYTHONUTF8
  • Check whether any reader has already reported this

Task 11: No Colab link on any chapter page

Status: Done 2026-09-29, published — full plan in planning/colab_buttons.md. Links were later split into their own cells tagged remove-print (2026-10-14) so they stay off the printed page.

Context: Reading a chapter on the published site, there is no way to open it as a runnable notebook. The expectation is a Colab link at the top of every chapter; today no chapter page has one.

Two distinct causes:

  • chap01 has a link that never renders. It sits in a # Welcome cell tagged remove-cell, which Jupyter Book drops from the HTML. The link itself is correct — it points at chapters/chap01.ipynb, the student notebook.
  • Chapters 2–19 have no link at all, in soln/, chapters/ or jb/. For 18 of the 20 chapters there is nothing to un-hide.

chap00 mentions Colab in its Getting started prose and does render, but that is orientation text, not a per-chapter launch link.

Approach: an explicit untagged markdown cell per chapter, placed under the "You can order" header and targeting chapters/chapNN.ipynb. Jupyter Book's native launch_buttons would do this with one config line and no per-chapter edits; it was considered and rejected in favour of control over wording and placement. The plan records how it would have been configured, in case that trade-off is revisited.

Scope

  • Agree the wording, which has to read correctly both on the website and inside a downloaded notebook — the current chap01 text does not
  • Add the cell to chapters 2–19
  • Reconcile chap01's existing Welcome cell, and check chap00 does not end up saying the same thing twice
  • Subtask: revise the "You can order" header in the same pass, since the new cell lands directly beneath it (see the plan for the Think Stats version and the open questions about affiliate disclosure and the coffee link)
  • Complete build across all four targets — including blank/, which update.py omits — then spot-check a rendered page before accepting the publish

No re-execution needed: these are markdown-only edits, so stored outputs stay valid.

Related

The plan also carries a review of the build process, prompted by this task. The headline: ThinkPythonSolutions/update.py does not build blank/, never pushes soln/, treats failed steps as success, and publishes the website as a side effect. Both of the first two match problems found in the working tree on 2026-09-19. Folding that into Task 8, or fixing update.py outright, is the natural follow-up.


Task 12: Page-view counter on the HTML book

Status: Live (2026-10-13)

  • Every published page loads https://greenteapress.com/gh.gif?u=PAGE&r=REFERRER. No cookies, no third party; counted from the greenteapress.com access logs by ~/GreenTeaPress/scripts/log_report.py ("GitHub Pages" section of the weekly report). Details and rollout: GreenTeaPress PROJECT_BOARD.md, Task 15.
  • Added with ~/GreenTeaPress/scripts/add_counter.py ThinkPython --push, without a rebuild: the snippet was inserted before </body> in the 24 published pages (gh-pages 1196ec7) and appended to html: extra_footer in jb/_config.yml (d1d52a7), so the next rebuild keeps it.
  • Keep the snippet when editing extra_footer; if the build moves to Jupyter Book 2 / MyST, put it in the new template.
  • Local copy pulled 2026-10-13.

Task 13: Search engine optimization for the book site

Status: In progress (2026-10-19)

Context: Help people searching for the book's topics find https://allendowney.github.io/ThinkPython/, using ordinary good practice only. The steps were worked out on Probably Overthinking It (2026-10-18), and the shared guide is ~/GreenTeaPress/plans/seo.md ("Checklist for a Jupyter Book 1 site"). Rollout is tracked in GreenTeaPress Task 22. Audit 2026-10-18: no meta descriptions, no sitemap; the landing-page title repeats ("Think Python — Think Python").

Done 2026-10-19, without the jupytext split_at_heading step (not about SEO):

  • sphinx-sitemap + sphinxext-opengraph (pip-installed in the ThinkPython conda env), base URL, cover image in jb/_config.yml; extra_footer page-view counter kept
  • Descriptions (Allen reviewed): jb/descriptions.yml for the notebooks, which jb/prep_notebooks.py writes into each copied notebook's metadata, so soln/ is untouched; front matter in index.md and blank.md. New chapter → add its line to descriptions.yml.
  • Landing H1 "Think Python, 3rd edition" (tab was "Think Python — Think Python")
  • Test build: 22 pages, each with one description, one og:description, canonical; sitemap = the TOC
  • Publish (jb step of update.py only; soln/ unchanged) — verified live 2026-10-30: sitemap.xml returns 200, chapter pages carry description and og:description
  • Sitemap: line in ~/AllenDowney.github.io/robots.txt (line 11)
  • Allen submits the sitemap in Search Console (2026-10-30)
  • GitHub repo website field and topics (GreenTeaPress scripts/repo_about.sh); "Read the book online" link added near the top of the README
  • Compare at ~6 weeks and ~3 months: python3 ~/GreenTeaPress/scripts/search_console.py --report --path /ThinkPython/ and the counter's google.com referrers

Task 14: First edition (Python 2): source, releases

Status: 2.0.18 released 2026-10-30. The 1st edition lives on the v1 branch of this repo (renamed from master, Task 15) (the original 2012 repo: book/ LaTeX source, code/ Python 2 examples); v3 is the 3rd edition and the default branch. Renaming master → v1: Task 15.

Releases:

  • v2.0.17 (tag at 8c7d812, 2015-10-26): the version on greenteapress.com from 2016 to 2026. The 2016-02-22 PDF also had "build-in" → "built-in" (never committed) and not f026fb2 (2017, the anydbm example). No PDF attached
  • v2.0.18 (e1a357c, 2026-10-30; GreenTeaPress Task 23): a note on the half-title page pointing to the 3rd edition (Allen's wording: "There is a newer edition of this book. … Python 2 reached its end of life in 2020 … Think Python, 3rd edition … https://allendowney.github.io/ThinkPython/"); copyright-page source link → greenteapress.com/wp/think-python/; license link https; running heads as published (the fix from ThinkPython2 2.4.1); the typo fix and f026fb2. 252 pages (2.0.17's build had no index; the typewriter font is now cm-super Type 1, not bitmaps). Release v2.0.18 with thinkpython.pdf, identical to greenteapress.com/thinkpython/thinkpython.pdf (GreenTeaPress deploy 2bf16ba). It shows as "Latest" because it's the repo's only release, even though it was created with --latest=false

How to release a new version: as in ThinkPython2's board (Task 2, "How to release a new version"), with these differences: work on the 1st-edition branch in a separate worktree (git worktree add DIR v1), since this checkout is on v3; the PDF is thinkpython.pdf in ~/GreenTeaPress/greenteapress/thinkpython/; create the release with --latest=false. greenteapress/thinkpython/tex/ and thinkpython.tex.zip on greenteapress.com are the 2.0.17 snapshot, not the source.

The book's 82 printed thinkpython.com/code/… links go to the 2nd edition's Python 3 code (ThinkPython2), not the Python 2 code in code/ here. Left as is; the half-title note says the edition is out of date.


Task 15: Rename the first-edition branch master → v1

Status: Done 2026-10-30. master holds the 1st edition (Task 14) but reads as the main branch; v1 matches v3. Checked 2026-10-30: no open PRs target master (the one open PR targets v3); none of the greenteapress.com or allendowney.com files link to ThinkPython/…/master; the book's printed links go through thinkpython.com, not GitHub. GitHub redirects web links to the old branch name; 1,095 forks are unaffected; existing clones get a notice to rename their branch.

  • Rename on GitHub: gh api -X POST repos/AllenDowney/ThinkPython/branches/master/rename -f new_name=v1
  • Update this clone: git branch -m master v1 && git fetch origin && git branch -u origin/v1 v1 && git remote set-head origin -a
  • Checked: branches are gh-pages v1 v3; /tree/master/code returns 302 to /tree/v1/code; default branch still v3; release v2.0.18 and its thinkpython.pdf asset intact
  • Updated the names in Task 14 (worktree command) and GreenTeaPress Task 23


Task 16: Quizzes that LMSs can import

Status: In progress, started 2026-11-06 — full plan in planning/lms_quizzes.md.

Context: Each chapter has a formative quiz, and there is one summative quiz, 83 multiple-choice questions in all, each with a rationale for every choice. The source is ThinkPythonSolutions/soln/quizzes.ipynb, in O'Reilly's Markdown quiz format, which works with O'Reilly's tooling and stays as it is. Instructors want them in their LMS. Two QTI exports were made by hand (2025 and 2026) and neither imports: both are QTI 2.1, which Canvas and Brightspace support less well than 1.2, and the 2026 per-chapter zips are not even well-formed XML (an undeclared xsi namespace in every item).

Approach: generate the LMS files from the source rather than by hand: QTI 1.2 for Canvas and Brightspace, Moodle XML for Moodle, and Brightspace's CSV; Blackboard and Common Cartridge to be tested. They are published in quizzes/ in this repo. Validate them in make tests. Each choice keeps its rationale as feedback.

Scope

  • Review the questions for typos and errors — 6 wrong or ambiguous answers, 13 typos and smaller issues fixed, and the "chaptertitle" placeholders replaced with real titles (2026-11-06)
  • Settle the open questions in the plan — downloads in quizzes/ here; generate the Brightspace CSV too; test on Canvas only
  • QTI 1.2 exporter (export_text2qti.py, via text2qti), imported cleanly into Canvas 2026-11-06
  • Moodle XML and Brightspace CSV exporter (export_lms.py), checked 2026-11-06 but untested on a real Moodle or Brightspace
  • Fill-in blanks (________) rendered as bold and italic marks in all three LMS formats (the Chapter 10 analogy question). Fixed in the exporters and republished 2026-11-10
  • Validate the generated XML in make tests (checked by hand with xmllint so far)
  • Canvas API access to a sandbox course; the old QTI 2.1 zips shown to fail silently
  • Publish: quizzes/ built by update.py, linked from the README and jb/quizzes.md; broken zips, hand-made CSV and convert_quizzes_to_qti.py removed (2026-11-06)
  • Ask instructors on Moodle and Brightspace to try the files

Task 17: Teaching materials for instructors

Status: Planning, started 2026-11-06 — full plan in planning/teaching_materials.md.

Context: Teachers often ask whether there is additional material for the book, presumably slides and quizzes. The quizzes now exist (Task 16). Today's materials support learning (solution, student and blank notebooks, 81 exercises, the quizzes), but nothing supports planning a course or assessing students privately: the exercise solutions and the quiz answers are all public.

Candidates, grouped in the plan: syllabus templates, a pacing guide with chapter dependencies, learning objectives, slides, instructor notes, in-class activities, graded assignments with private solutions and autograders, a private exam question bank, projects, AI guidance, and an LMS course shell.

Distribution and access (what's free, what's private, how instructors get it) are planned on the private board in ThinkPythonTeaching (its Tasks 1–4), not here.

Scope

  • List what teachers have actually asked for (past emails), and add a feedback link to the website
  • Learning objectives, pacing guide and syllabus templates, on a "For Instructors" page that absorbs the Quizzes page
  • Decide public vs. private for assessment material, and how instructors get access (our own private repo, or O'Reilly's instructor resources)
  • Pilot slides for one chapter, generated vs. separate decks
  • Pilot one graded assignment with a private solution and an autograder
  • Scale up what works

Task 18: Archive and mine the old Software Design materials

Status: Planning, started 2026-11-06 — full plan in planning/course_archive.md. Feeds Task 17.

Context: Allen taught Software Design from Think Python for years, and the quizzes, exams, homework and solutions are scattered: an external drive with Spring 2004 to Fall 2011 (/media/downey/My Passport/rocky/sd*, the largest source, and full of student records that must stay out), old course pages for 2004 to 2008 (~/AllenDowneyCom/allendowney/sd0*, still public on allendowney.com), Subversion checkouts for Spring 2011 and Fall 2013 (~/svn), downloaded Google Sites for 2011 and 2013, and ~/SoftwareDesign. Everything predates the 3rd edition: Python 2, old chapter numbers, Swampy.

Approach: copy everything unchanged into a private archive with a record of where it came from; convert it to text; catalog each question by topic and 3rd-edition chapter; then mine it into public quiz additions, a private exam bank (in the quizzes.ipynb format, so the Task 16 exporters work), in-class activities and assignments, each updated to Python 3 and checked by running it.

Scope

  • Decide where the archive lives — private repo ThinkPythonTeaching, cloned here and gitignored (2026-11-06)
  • Find the material Allen remembered as "on Canvas" — it's the external drive
  • Look for the 2021 Takeout archives of the classic Google Sites in Drive; find any other terms (2005, 2009, 2010, 2012)
  • Gather the sources into ThinkPythonTeaching/archive/ — 1,863 files from 8 terms (2004–2013), student folders, projects, repos and records skipped and logged only as counts (2026-11-06, a12e8c1)
  • Convert to text (text/, 1,590 files) and catalog by file (catalog/files.csv) — 2026-11-06
  • Catalog by question: 418 questions, 314 distinct, catalog/REPORT.md by chapter (2026-11-06)
  • Purge the student files that reached the private repo's early history — history squashed to one commit and force-pushed (2026-11-06)
  • Mine Chapters 9–11 (2026-11-06): bank/chap09.md–chap11.md with passing check scripts; 2 public quiz additions
  • Mine Chapters 2–6 (2026-11-06); 4 more public quiz additions
  • Mine Chapters 14–17 (2026-11-06); 2 more public quiz additions
  • Mine the remaining chapters (7, 8, 12, 13, 18) — 2026-11-06; 2 more public quiz additions
  • Allen: check the MLA rules paraphrased in ch18-sort-entries against the handbook
  • Turn the reviewed bank into the material teachers get (Task 17)