I keep the lecture notes for my Data Science course at HHU Düsseldorf in a git repository: twenty-nine chapters of Markdown and Jupyter notebooks, German throughout, built into a website and a PDF, in front of students since 2023. Every September I read the whole thing front to back before term starts and fix what has drifted. This year I did that with Claude working in the repository alongside me, and I came out of it with a PDF build that works again, a stack of corrections I would not have found by reading, and a clearer sense of which parts of this job I want to hand over and which I want to keep. If you maintain a script for your own course, some of it may transfer.

Leaving Jupyter Book 1

Jupyter Book 1 served the script well for three years, and its quirks were not the reason to leave. The Jupyter Book team has put the Sphinx-based stack into maintenance mode, with no new development planned on their side, and moved the actual work to the MyST engine that replaces it (their own announcement). Once that is the situation, staying put means sitting on a version that gets fixed only if somebody wanders past and opens a pull request. So, migrate: to what the project calls Jupyter Book 2 and what the command line calls mystmd.

I tried that twice on my own. On 8 May I committed "switch build system to jupyter-book 2, first try" and reverted it the next day. On 16 June the HTML came out fine, and the last commit of that evening reads "giving up on the pdf for now :-(". The old _config.yml and _toc.yml then sat in the repository until September, when the third attempt went through. What made the difference the third time was not a better plan. It was that Claude could build the book and read the LaTeX log itself, rather than me describing a failure to someone who could not see it. An assistant that cannot run your build is guessing from the same distance you are.

Beitragskalender des Skript-Repositories von 2023 bis 2026: vier Jahreszeilen aus Wochenspalten, gefuellte Quadrate nur zwischen April und Juli, dazu im September 2026 sieben schwarz umrandete Tage
Every commit to the script since 2023, one square per day. Click for the full size (2400 × 2000 pixels, 119 KB).

The two errors every MyST book with maths runs into

MyST will export a PDF along two routes, Typst or LaTeX. Typst gave up on the first \bigcup it met, so LaTeX it is, which means latexmk and a TeX Live with texlive-xetex, -latex-extra, -bibtex-extra and -lang-german, plus imagemagick, which MyST calls to convert GIF figures to PNG. Leave imagemagick out and you get the nastiest kind of failure: xdvipdfmx aborts, MyST reports success, and you are left holding a corrupt PDF with no idea why.

With the toolchain in place, my first honest build produced 304 errors, from two causes that both sit in MyST rather than in my book.

The template MyST ships imports amsmath and not amssymb. My chapters are full of ℝ, ℕ and Ω, so every \mathbb came out as an undefined control sequence, 188 times. Then every code cell's output arrives wrapped as \begin{lstlisting}[language=undefined], or language=null for cells without a language, and the listings package has never heard of either, which accounts for the other 116.

Both disappear if you override the template. Copy MyST's plain_latex, add four things, and point myst.yml at your copy:

% templates/tex/ds-skript/template.tex
\usepackage{amssymb}             % MyST imports only amsmath; the book uses \mathbb
\usepackage[ngerman]{babel}      % German hyphenation and quotes
\usepackage{listings}            % loaded here so the two dummy languages can be defined
\lstdefinelanguage{undefined}{}  % code-cell *outputs* arrive as language=undefined
\lstdefinelanguage{null}{}       % cells without a language as language=null
\lstset{basicstyle=\ttfamily\small, breaklines=true, showstringspaces=false}
\usepackage[a4paper, margin=2.5cm]{geometry}   % article defaults to US letter
exports:
  - format: pdf
    template: ./templates/tex/ds-skript
    output: exports/ds-skript.pdf

The geometry line is not cosmetic, by the way. The article default is US letter with a text block of about 11.5 cm, which shrinks every 12×4 inch matplotlib figure to something nobody can read. And showstringspaces=false keeps LaTeX from decorating the spaces inside your Python strings with little ␣ marks.

Reading the whole thing, more thoroughly than usual

The yearly read-through is not new, and I would not want to skip it. What was new is that Claude read the book as well, in table-of-contents order, notebooks included as text, with four mechanical checks running alongside: a build without warnings, a lecture recording linked from every chapter, no chapter with two H1 headings, and the list of numpy functions actually still matching the book.

Most of it held up. Every worked example recomputed correctly, which is the part I care about most, since those are the numbers students check their own work against. What came out beyond that was eight statements a student would have learned wrong, forty-one smaller mismatches between chapters, and about ninety small artefacts of the kind that accumulate in any text edited piecemeal over years.

One of the eight is a nice example of the sort of thing a careful reader misses and a pedantic one does not. The chapter on continuous distributions defined the support of a random variable as the closure of the intersection of all sets of measure one. That intersection is empty for any density: pick any point x in the interval, and the interval minus x still has measure one, so no point survives every set, and the closure of nothing is still nothing. The repair is to intersect the closed sets of measure one, after which no closure is needed at all, because an intersection of closed sets is closed already. I had read that definition many times. It reads perfectly well.

Can a student who missed the lecture still pass?

That question turned into the most useful part of the whole month, and if you take one idea from here, take this one.

A course leaks material. Something gets explained on a slide, something else at the blackboard in a sentence nobody wrote down, an exam question quietly assumes a formula that only ever appeared in an exercise sheet. So we went through the leaks systematically. All 216 items harvested from my slide decks were checked against the book, and 199 of them turned out to have a home there already; the rest got an explicit note saying why they stay out, since databases and big data belong to other courses rather than this one. Then the six exam and exercise sources were read in full and every question cross-checked against the book by full-text search, with three verdicts per question: in the book and findable, present but scattered or under another name, or missing outright. One exam question used the binomial pmf, and the book defined the binomial distribution nowhere, mentioning it only in passing as a sampling call.

None of this produced new material. It moved things that already existed in the course into the book and rephrased them to sit properly in the surrounding text.

Notation

Checking notation across a maths-heavy book is slow and easy to get wrong, and I handed it over completely. My conventions allow both N and n for sample size, which bothers nobody until both appear in the same formula, and that happened in exactly three places across the whole script. Skewness and kurtosis were defined unstandardised in one chapter and standardised in another, where standardised is both the usual convention and what pandas.skew() computes. R² appeared in two different forms depending on which chapter you opened, and the two agree on training data with an intercept, but not on the test set where I actually used them.

Finding that by hand means holding the exact symbols of one chapter in your head while reading another one, months later. Changing a symbol everywhere it appears, without missing an instance or breaking the formula next to it, is worse.

Drafts on the side, not in the book

The working rule that made all of this comfortable: nothing gets written into a chapter directly. Proposed text goes into a separate file first, one entry per item, each with the exact insertion point, the finished paragraph in the book's own markup, and a block of notes underneath listing what was verified, how, and what is still open. Every number in a draft is computed in the project's own uv environment before I see it, rather than asserted.

Then I read the file. Most items get nothing back from me but "alles gut" (OK), a few get corrections, and only after that does anything move into the chapters, get its notebooks re-executed and get committed. Nothing reaches the shared repository until I say "push it". The notes underneath are the part I would not give up: in three rounds out of four they caught an item whose target had gone stale, a section that had moved, a table that existed already, material that had arrived by another route in the meantime.

What I don't ask it for

I do not like Claude's prose voice, and I do not like Gemini's or ChatGPT's any better. They share a cadence that I can feel coming after two sentences, and a teaching text is not the place for it. So the sentences in my book are mine. What I ask for is a rephrasing of something I have already written, a second opinion on an ordering, a check that a definition says what I think it says. Fresh paragraphs from a blank prompt, no. This blog post is not quite holding up to that standard, and I think it does not read as well as my usual writing…

#