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 (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.title and use result.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-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