Numbered tasks for tracking work. Each task has a permanent number; add new tasks at the end. Update status as work progresses.
- Task 1: Chapter 18
Counterexample produces nonsense output (#78) — done; published, answered, closed. - Task 2: Chapter 17
has_pairexercise 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
keepnow 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
dataclasscoverage 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 aremove-printnote. - 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.
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.
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.
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.
The notebook has since been re-executed, so source and stored output now agree. Cell 47 reads:
counter2 = Counter('bans')
counter + counter2and 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.
- Remove the stray test cell that rebound
counter - Re-execute
soln/chap18.ipynbso 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
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 Trueand 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.
- Decide the intended shape: the outline now gives
has_pair(self) - Fix the scaffolding cell in
soln/chap17.ipynb - Document
check_setsso the solution is readable on its own - Sweep the rest of Chapter 17's
%%add_method_toexercises for the same stub/test mismatch (has_full_houseuses the same pattern) - Rebuild and publish
- Reply on #76 and close
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
5and height4, 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.
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. Neitherchapters/prep_notebooks.pynorjb/prep_notebooks.pymentionskeep. 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.keepmeans "keep the code," never "keep the result." - It was never finished. All 18
keeptags 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 (
chap03cells 77 and 79), where theblank/script cannot act on them at all, since it only testscell['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.
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:
trianglelooped overrange(height+1), printingstring * 0first — so "a pyramid with 5 levels" emitted a leading blank line and six lines of output. Nowrange(1, height+1).print_right's docstring said "column 70" while the code usedcolumns = 40(the exercise asks for 40).
- 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
keepdoes and whether it was usable here (it is not) - Re-execute
soln/chap03.ipynbso the stored output matches the correctedtriangle - 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
keeptags — finished the idea: 63 cells tagged in Chapters 4–18 (2026-11-06) - Rebuild
chapters/, regenerateThinkPythonNotebooks.zip, publish the site - Reply on #79 and close
- Restoring outputs for exercise solutions (that would hand readers the answers)
- Teaching
chapters/prep_notebooks.pyakeep-outputtag — 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.
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.
- Make
download()fail loudly when the fetch fails, instead of leaving the next cell to raiseFileNotFoundErroron 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 onraw.githubusercontent.com) are not exposed the same way - Turn on a failure notification for the scheduled run
- Removing the live downloads from the book. They are pedagogically deliberate, and Chapter 8 is about reading a real text file.
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]*.ipynbThat 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@v2andactions/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)
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.
- 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
checkoutandsetup-pythonto v7; Python 3.12;fail-fast: false; pip caching - Add an outputs check: a
soln/notebook committed withexecution_count: nullthroughout should fail the build (--nbmakeexecutes notebooks, so stored outputs are invisible to it — this is the check that catches a chapter committed unexecuted)
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.
- 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
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.
- Look at the demo and check the attribution and license notice
- Decide whether to acknowledge, link, or simply thank and close
- Reply on #74
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.
- Write a real
.gitignorein both repos: generated notebooks, copied support files, downloaded corpora,__pycache__/,.ipynb_checkpoints/ - Install
nbdimeor 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.pyvariants —chapters/andblank/now share the top-levelprep_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.pyanddu. The "solutions" zip never held solutions — it contained the samechapters/*.ipynbas 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.mdcovering the hazards above - Document the release ritual end to end —
update.pydoes it in one command, and CLAUDE.md describes it
- Merging
ThinkPythonSolutionsinto this repo or converting it to a submodule - Changing the GitHub Pages deployment mechanism
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.
- Re-validate
config_mapping.mdagainst the then-current 2.0 release - Build
jb2/side by side, leavingjb/untouched until the output matches - Confirm the unknowns: execution settings, CC footer, MyST extension parity, MathJax config
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 affectedopen()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=1in 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.
- 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-latestback to the CI matrix once the chapters pass withoutPYTHONUTF8 - Check whether any reader has already reported this
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:
chap01has a link that never renders. It sits in a# Welcomecell taggedremove-cell, which Jupyter Book drops from the HTML. The link itself is correct — it points atchapters/chap01.ipynb, the student notebook.- Chapters 2–19 have no link at all, in
soln/,chapters/orjb/. 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.
- Agree the wording, which has to read correctly both on the website and inside a
downloaded notebook — the current
chap01text does not - Add the cell to chapters 2–19
- Reconcile
chap01's existingWelcomecell, and checkchap00does 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/, whichupdate.pyomits — then spot-check a rendered page before accepting the publish
No re-execution needed: these are markdown-only edits, so stored outputs stay valid.
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.
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: GreenTeaPressPROJECT_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-pages1196ec7) and appended tohtml: extra_footerinjb/_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.
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 theThinkPythonconda env), base URL, cover image injb/_config.yml;extra_footerpage-view counter kept - Descriptions (Allen reviewed):
jb/descriptions.ymlfor the notebooks, whichjb/prep_notebooks.pywrites into each copied notebook's metadata, sosoln/is untouched; front matter inindex.mdandblank.md. New chapter → add its line todescriptions.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.pyonly;soln/unchanged) — verified live 2026-10-30:sitemap.xmlreturns 200, chapter pages carrydescriptionandog: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
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 notf026fb2(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 andf026fb2. 252 pages (2.0.17's build had no index; the typewriter font is now cm-super Type 1, not bitmaps). Releasev2.0.18withthinkpython.pdf, identical togreenteapress.com/thinkpython/thinkpython.pdf(GreenTeaPress deploy2bf16ba). 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.
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/codereturns 302 to/tree/v1/code; default branch stillv3; releasev2.0.18and itsthinkpython.pdfasset intact - Updated the names in Task 14 (worktree command) and GreenTeaPress Task 23
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.
- 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, viatext2qti), 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 withxmllintso far) - Canvas API access to a sandbox course; the old QTI 2.1 zips shown to fail silently
- Publish:
quizzes/built byupdate.py, linked from the README andjb/quizzes.md; broken zips, hand-made CSV andconvert_quizzes_to_qti.pyremoved (2026-11-06) - Ask instructors on Moodle and Brightspace to try the files
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.
- 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
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.
- 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.mdby 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.mdwith 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-entriesagainst the handbook - Turn the reviewed bank into the material teachers get (Task 17)