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 correctly (3.1.2 Language of Parts).role="group"(not a landmark) keeps things uncluttered when several explanations render on one page - The title is deliberately not a heading. Heading level depends on the surrounding page outline, which a library can't know, so the title supplies the group's accessible name instead. If you want it in your heading outline, render your own heading from
result.titleand useresult.html(or the structured fields) for the body - Code is marked up as code; inline tokens use
<code>and blocks use<pre><code> - The suggested fix has a visible "Suggested fix" label; the original traceback stays in a native
<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