Skip to content

About

Revision site for the teenagers I teach Python to. Notes, practice and a Python editor that runs in the browser. No build step.

Topics

Resources

Stars

21 stars

Watchers

0 watching

Forks

Repository files navigation

Paul's Python Course

Open the site Pyodide License Last commit

A revision site for the teenagers I teach Python to in person. After each lesson they come here to re-read what we covered, try the practice, and run Python in the browser without installing anything.

Live: https://bouwles.github.io/pauls-python-course/

Plain HTML, CSS and vanilla JavaScript. No React, no build step, no bundler, no npm install, no backend, no tracking. Clone it and open index.html.


What it looks like

Home — every lesson, with how far through you are

The home page, listing lessons 1 to 3 with progress bars and two locked lessons below them

Lesson page — editable examples that actually run, including input()

Python runs client side via Pyodide. input() opens an inline prompt in the output area rather than a browser dialog, so a program can stop and wait for an answer without freezing the page.

A lesson section in dark mode showing the input example run, with the typed answer Sara echoed and Hi Sara printed underneath

Practice — errors explained in English, and checks that never say "wrong"

Every traceback keeps its real line number and gets a plain-English line underneath it. Failed checks say what was expected and offer a hint.

A practice task showing a TypeError traceback with a plain-English explanation underneath, and gentle feedback saying nothing came out yet

Quizzes — recall, predict-the-output, and type-the-code

A quiz question showing a code sample and three possible outputs, with the right answer ticked in green and the chosen wrong answer marked in red

A loop that will not stop is survivable

Python runs in a Web Worker, so a runaway loop never freezes the page. Output is capped and counted, and Stop ends it with the code left in the editor.

A while loop with no counter printing endlessly, with a Stop button in the toolbar and a line reporting millions of further lines not shown

On a phone, because that is what my students actually use

A lesson section on a phone, with the objectives and contents collapsed into a dropdown under the header and a code example showing its output


What is in it

  • Lessons rendered entirely from one data file, with a sticky sidebar of objectives and a contents list that follows you down the page.
  • Runnable code blocks. Every example is editable and has Run and Copy. Pyodide loads lazily on the first Run and says so while it boots.
  • An infinite loop cannot kill the page. Python runs in a Web Worker, so a runaway while leaves the page fully usable. Stop ends it and the editor keeps the code. Nothing is ever killed automatically.
  • input() support with an inline prompt, so programs that ask questions work like they do on a real machine.
  • Error translations. SyntaxError, NameError, str + int, indentation at two levels and a dozen more get a sentence of plain English. So does the quiet case where a run prints nothing at all. Both tables live in one file.
  • Practice tasks with five kinds of gentle check, a hint button, and a worked answer that stays hidden until they have had a go themselves.
  • Quizzes on their own page: multiple choice, predict-the-output, and type-the-line. No score — at the end it lists the sections behind whatever they got wrong first time, as links back into the lesson.
  • Playground for messing about, with the code kept between visits.
  • Progress in localStorage only. Nothing is sent anywhere, there are no accounts, no cookie banner and no analytics.
  • Light and dark themes, both designed properly, following the system setting with a manual toggle.
  • Keyboard usable throughout, real focus rings, AA contrast in both themes.

Running it locally

Double-click index.html. That is genuinely it — Pyodide and CodeMirror come from their CDNs and work fine from file://.

If you would rather serve it over http://, which is closer to how GitHub Pages behaves:

python -m http.server 8000

Then open http://localhost:8000.


Adding the next lesson

Everything a lesson contains lives in data/lessons.js. That is the only file you edit. Lessons 1 to 3 are written; lesson 4 is the next one.

  1. Open data/lessons.js.
  2. Copy the whole Lesson 3 object (from { id: 3, down to its closing },) and paste it below itself, inside the same window.LESSONS = [ ... ] array.
  3. Change id to 4, give it a new slug, title, summary, and rewrite the contents.
  4. Update lesson 3's next line, which is the teaser for what comes after it.
  5. Scroll to window.UPCOMING at the bottom and delete the { id: 4, ... } entry, so it stops showing as a locked card.
  6. Save, refresh the browser. The lesson appears on the home page, at lesson.html?id=4, and its quiz at quiz.html?id=4.

Nothing else needs touching. No HTML, no CSS, no JavaScript.

What goes in a lesson object

{
  id: 4,
  slug: "lists",
  title: "Lists",
  summary: "One line, shown on the lesson card.",
  objectives: ["Shown in the sidebar", "Keep them short"],
  sections: [ ... ],      // see below
  practice: [ ... ],      // see below
  quiz: [ ... ],          // see below
  cheatsheet: [ { code: "int(x)", meaning: "turn text into a number" } ],
  next: "Teaser line for the lesson after this one."
}

Section types

Each one becomes a numbered section with its own entry in the sidebar contents.

{ type: "text",    heading: "...", body: "one string, or an array of paragraphs" }

{ type: "code",    heading: "...", body: "...",
                   code: 'print("Hello")\n',
                   explain: "shown under the editor",
                   runnable: true }        // false renders it read-only

{ type: "callout", heading: "...", body: "..." }   // tinted, accent rule

{ type: "mistake", heading: "...", body: "...",
                   wrong: 'print(Hello)',
                   right: 'print("Hello")',
                   why: "the explanation" }

Any section can also carry these, whatever its type:

aside: true                    // "Optional" tag, quieter styling, stepped back.
                               // For the bit you might skip in class.

table: {                       // a small table, for walking through something
  head: ["Round", "total"],    // step by step
  rows: [["1st", "1"], ["2nd", "3"]],
  code: false                  // default true: first column is set as code
}

after: "..."                   // paragraphs placed below the table,
                               // string or array

link: { href: "lesson.html?id=2#section-10",
        text: "The version from lesson 2" }   // a pointer somewhere else

Section anchors are #section-N, counting from 1 in the order they appear, so lesson.html?id=2#section-10 is the tenth section of lesson 2.

Practice tasks

{
  id: "p1",                        // unique inside this lesson, used by progress
  prompt: "Print your own name.",
  starter: "# type your code here\n",
  hint: "Shown when they press the hint button.",
  solution: 'print("Paul")\n',     // hidden until they have pressed Run once
  check: { type: "output-not-empty" }
}
check type what it does
output-not-empty passes if the program printed anything
output-contains value is a string or an array of strings, all must appear in the output (case-insensitive)
output-matches value is a regex (or a regex string plus optional flags) tested against the output
code-contains same as output-contains but tested against the code they wrote
custom fn({ code, output }) returns true or false

Any check can carry a message, the friendly sentence shown when it does not pass. Give check an array if you need more than one, and all must pass:

check: [
  { type: "code-contains", value: "age =", message: "I could not find a variable called age." },
  { type: "output-not-empty" }
]

Quiz questions

These render on quiz.html?id=<lesson>, not on the lesson page. Three types.

choice is the default, so type can be left off. answer is the index of the right option, counting from 0. review is the index of the section worth re-reading if they get it wrong, and it drives the list shown at the end.

{
  question: "What does # do?",
  options: ["Runs the line twice", "Makes Python ignore the rest of the line"],
  answer: 1,
  explain: "Everything after # is skipped.",
  review: 3                      // -> section 4 in the lesson, 0-indexed
}

Add optionsAreCode: true when the options are snippets rather than sentences, and they will be set in the monospace face.

predict shows a code sample and asks what comes out. The options are output, so they are always set as code and can contain newlines.

{
  type: "predict",
  question: "What comes out when this runs?",
  code: '# print("one")\nprint("two")\n',
  options: ["two", "one\ntwo", "one"],
  answer: 0,
  explain: "The first line is commented out.",
  review: 3
}

write asks them to type a line of Python. accept is a list of answers that count as right; the first is shown if they get it wrong. Matching forgives spacing and swaps single quotes for double, and nothing else, so Print is still wrong.

{
  type: "write",
  question: "Write the line that shows the word Hello on the screen.",
  placeholder: "one line of Python",
  accept: ['print("Hello")'],
  explain: "print, then brackets, then the text inside quotes.",
  review: 1
}

There is no score. When every question has been answered once, the quiz lists the sections behind the ones they got wrong on the first attempt, as links back into the lesson.


Extending the error explanations

When a student's code errors, the red traceback gets a plain-English line underneath it. That table is assets/js/errors.js. Add an entry:

{ test: /SomeError: the exact text/i, hint: "What it usually means." }

First match wins, so put specific patterns above general ones. hint can also be a function receiving the regex match, if you want to quote part of the error back to them.

The same file holds PPC_SILENCE_HINTS, for the quiet failures where nothing went wrong and nothing was printed either. Those confuse beginners more than a red traceback does — a while whose condition is already false does nothing at all and looks broken. These are matched against the code they wrote, not against an error:

{ test: /^\s*while\s+.+:/m, hint: "Ran without printing anything. If a while..." }

Deploying

Every push to main publishes the site through GitHub Actions. There is no build step — the workflow uploads the repo as-is.

git add -A
git commit -m "Add lesson 2"
git push

Then watch the run under the Actions tab. It takes about a minute.

If you fork this, go to Settings → Pages → Build and deployment and set Source to GitHub Actions. Nothing else to configure.


File map

index.html            home page: lesson list and progress
lesson.html           renders one lesson from ?id=
quiz.html             all quizzes, or one quiz with ?id=
playground.html       blank editor
404.html
data/lessons.js       ALL lesson content - the only file you edit to add lessons
assets/css/site.css   the whole design, both themes
assets/js/store.js    progress in localStorage, theme, small DOM helpers
assets/js/errors.js   plain-English error translations
assets/js/runner.js   Pyodide loading, input() handling, the editor widget
assets/js/lesson.js   builds the lesson page
assets/js/quiz.js     builds the quiz index and the quizzes
assets/js/home.js     builds the home page
assets/js/playground.js
screenshots/          the images in this README
.github/workflows/deploy.yml

Things worth knowing

  • Python is Pyodide, loaded from jsDelivr the first time anyone presses Run. It is a few megabytes, so the first run takes a few seconds and says so. After that it is instant for the rest of the visit.
  • Python runs in a Web Worker, not on the page's own thread. That is the only reason a student's infinite loop is survivable: the page stays responsive and Stop can terminate the worker outright. The worker is built from a Blob rather than its own .js file, because a worker loaded from a file:// path is blocked by the browser and a blob: one is not — which is what keeps "just open index.html" working.
  • The editor is CodeMirror 5 from cdnjs, which works as plain script tags. CodeMirror 6 would need a bundler, and the whole point here is that there is no build step. If either CDN is unreachable the editors fall back to plain textareas.
  • input() works because the code is rewritten before it runs: input(...) becomes an awaited call, and any function that ends up containing an await becomes an async def along with the calls to it. The tree is compiled directly rather than unparsed, so traceback line numbers still match what was typed. Beginner code is fine; input() inside a lambda or a comprehension is not. Because it is already asynchronous, it works inside the worker by message passing, with no need for SharedArrayBuffer or the cross-origin isolation headers that GitHub Pages cannot send.
  • Stop terminates the worker, which is the only way to interrupt Python mid-loop. A replacement worker is started immediately in the background, so the run after a Stop is not slow. The editor is never touched.
  • Runaway output is capped. A loop can print faster than any page can render, so the worker keeps the first 2000 lines of a run and from then on only counts, updating an "and N more lines, not shown" line. Output is flushed on elapsed time from inside the stdout hook rather than on a timer, because a tight Python loop never gives the worker's event loop a turn.
  • Nothing is ever killed automatically. After five seconds a run just says it is still going and points at Stop. Some legitimate loops are slow.
  • Progress is localStorage under ppc:progress:v1. It never leaves the device, so the same student on a school computer and on their phone will see different progress. The reset button is in the footer.

Licence

Do what you like with the code. The lesson writing is mine.

License

MIT — see LICENSE.

About

Revision site for the teenagers I teach Python to. Notes, practice and a Python editor that runs in the browser. No build step.

Topics

Resources

Stars

21 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages