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.
- 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
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.
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, withlangtaken from the copydeck so screen readers pronounce localised copy correctlyrole="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.titleand useresult.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-labelledbyremains unambiguous when multiple explanations coexist on a page
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
See CONTRIBUTING.md for detailed instructions.
In brief:
npm install
npm run dev:build # watch and build everything
npm testFor a one-off full build use: npm run build:all
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).
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:tracesnpm 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.
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
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 versionThe script:
- Checks you're on a clean
mainin sync withorigin, and logged in to npm - Runs the tests and build
- Bumps the version (updating
package.json/package-lock.json), commits, and tagsvX.Y.Z - Publishes to npm (prompting for your auth as needed)
- Points the demo at the new release (bumps
docs/to the just-published version) and commits it - Pushes the commits and tag
- 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.
npm login: publishing uses your local npm credentials (the package publishes publicly viapublishConfig.access: "public")gh auth login: the GitHub Release is created with theghCLI