Skip to content

RaspberryPiFoundation/python-friendly-error-messages

Repository files navigation

Python Friendly Error Messages

A small library that explains Python error messages in a friendlier way, inspired by p5.js's Friendly Error System.

It can be used in browser-based editors (like RPF's Code Editor web component) or any environment that executes Python code through Pyodide or Skulpt.

This library is currently Pyodide-first. The copydeck and demos are developed and verified against a pinned Pyodide version (see docs/pyodide-config.js), and the demo runs that Pyodide build live to show real tracebacks. Skulpt is still supported, both runtimes emit CPython-style tracebacks and share one adapter, but it is not the current priority.

Features

  • Parses and normalises errors from Pyodide or Skulpt (via a shared CPython-traceback adapter)
  • Matches errors against a copydeck (containing rules and templates)
  • Copydeck-based explanations can be localised (and the copydeck contains prompts and context to help with this)
  • Returns structured explanations as well as ready-to-use HTML snippets

Usage

import {
  loadCopydeckFor,
  registerAdapter,
  cpythonAdapter,
  friendlyExplain
} from "python-friendly-error-messages";

await loadCopydeckFor(navigator.language); // falls back to "en"

// register runtimes - Skulpt and Pyodide both appear to emit CPython-style tracebacks,
// so the same adapter handles both. The runtime name you register under is
// added onto the resulting trace
registerAdapter("skulpt", cpythonAdapter);
registerAdapter("pyodide", cpythonAdapter);

// later, when you have an error string and some code:
const result = friendlyExplain({
  error: rawTracebackString,
  code: editorCode,
  runtime: "skulpt" // or "pyodide", matching the adapter/runtime that produced the traceback
});

// friendlyExplain returns null when the library has no friendly mapping for the
// error (or cannot parse it). Fall back to showing the raw Python/Pyodide error:
if (result) {
  // result.html is a ready-made snippet
  // or use result.title, result.summary, result.steps, result.patch, result.trace
} else {
  // no friendly explanation - show the original traceback as-is
}

// if the trace reports an unhelpful source location (eg. Pyodide runs code
// as "<exec>"), pass file explicitly to override what's parsed from the trace:
const result = friendlyExplain({
  error: rawTracebackString,
  code: editorCode,
  runtime: "pyodide",
  file: "main.py", // overrides the file from the trace
});

// optionally limit which sections appear in result.html:
const result = friendlyExplain({
  error: rawTracebackString,
  code: editorCode,
  runtime: "skulpt",
  sections: ["title", "summary"] // "why", "steps", "patch", "details" also available
});

See the demo for a full set of examples.

Note: The "patch" section contains a suggested code change to fix the error, but should be considered experimental at this stage.

Accessibility

result.html is built to be accessible by default (with WCAG 2.1 AA in mind):

  • The whole explanation is one labelled group: <div class="pfem" role="group" lang="…" aria-labelledby="…">, named by its title, with lang taken from the copydeck so screen readers pronounce localised copy correctly
    • role="group" keeps things uncluttered when several explanations render on one page
  • The title is deliberately not a heading, the title supplies the group's accessible name instead. If you want an actual heading, render your own from result.title and use result.html (or the structured fields) for the body
  • Code is marked up as such; inline tokens use <code> and blocks use <pre><code>
  • The suggested fix has a visible "Suggested fix" label; the original traceback stays in <details>/<summary>
  • Element ids are randomised per call so aria-labelledby remains unambiguous when multiple explanations coexist on a page

Your responsibilities

A couple of WCAG 2.1AA requirements can only be met by the host app:

  • Announce it: the explanation appears in response to running code. For a screen reader to announce it without stealing focus, insert it into a pre-existing live region (aria-live="polite" / role="status") that is already in the DOM, or move focus to it
  • Contrast & colour: all styling is yours, ensure text contrast, and don't rely on colours (.pfem__var, .pfem__file, …) alone to convey meaning

Development

See CONTRIBUTING.md for detailed instructions.

In brief:

npm install
npm run dev:build   # watch and build everything
npm test

For a one-off full build use: npm run build:all

Copydecks

Copydecks are JSON files that contain rules and templates for matching and explaining errors. They are stored in copydecks/ and can be edited or added to.

New error explanations can (should) be generated by an LLM, for ease (TODO: add system instructions for this). The generated content must be reviewed and edited by an appropriately-qualified human (eg. learning managers) prior to release, to ensure accuracy and clarity.

Copydecks contain prompts and additional context for localisation.

For management of human-reviewed copydeck content, scripts (in ./scripts) are provided to extract and update copydeck content in a Google Sheet (and re-import it after review).

Demo

The demo in docs/ runs real Pyodide live in the browser: it executes each example snippet and feeds the actual traceback to the library, so what you see is exactly what that Pyodide/Python version produces. The version is pinned in one place, docs/pyodide-config.js, and is shown in the demo header.

To move to a newer Pyodide:

# 1. bump PYODIDE_VERSION in docs/pyodide-config.js
# 2. keep the pyodide devDependency in sync
npm install --save-dev pyodide@<version>
# 3. refresh the cached traces used by the demo data view and the coverage test
npm run regen:traces

npm run regen:traces runs the pinned Pyodide once and writes the real traceback for every example into docs/demo-examples.js. This keeps the fast vitest coverage test asserting against genuine Pyodide output.

Building

Create a clean build for distribution:

npm run build:all && npm run build:browser

Output files will be in dist/.

You can now import, and use it, elsewhere (see Usage notes).

The package is published to: https://www.npmjs.com/package/@raspberrypifoundation/python-friendly-error-messages

Releasing

Releases are (currently) generated and published to npm from your local machine, so it can use your npm auth and 2FA OTP rather than a long-lived CI token (CI publishing is a possible future enhancement).

One command does everything:

./scripts/release.sh patch   # 0.3.0 → 0.3.1
./scripts/release.sh minor   # 0.3.0 → 0.4.0
./scripts/release.sh major   # 0.3.0 → 1.0.0
./scripts/release.sh 1.2.3   # explicit version

The script:

  1. Checks you're on a clean main in sync with origin, and logged in to npm
  2. Runs the tests and build
  3. Bumps the version (updating package.json / package-lock.json), commits, and tags vX.Y.Z
  4. Publishes to npm (prompting for your auth as needed)
  5. Points the demo at the new release (bumps docs/ to the just-published version) and commits it
  6. Pushes the commits and tag
  7. Creates a GitHub Release with notes generated from the commits/PRs since the previous tag

If npm publish fails, nothing is pushed. The script prints how to undo the local bump and retry.

Prerequisites (one-time)

  • npm login: publishing uses your local npm credentials (the package publishes publicly via publishConfig.access: "public")
  • gh auth login: the GitHub Release is created with the gh CLI

About

A small, runtime-agnostic (but Pyodide-focused), library that explains Python error messages in a friendlier way, inspired by p5.js's Friendly Error System.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors