Skip to main content

Runnable Python cells

<RunnablePython> embeds a CodeMirror editor + a Run button + an output panel directly in a lesson. Python runs in the browser via Pyodide (WebAssembly), so there's no server round-trip and no shared state across learners.

import RunnablePython from '@/components/pyodide/RunnablePython.tsx';

<RunnablePython
client:visible
caption="Edit and run β€” your changes don't leave the browser."
initialCode={`name = "world"
print(f"Hello, {name}!")
`}
/>

What the learner sees:

  • A code editor with the initialCode pre-filled.
  • A Run button (or your runLabel override).
  • A status indicator while Pyodide boots / the snippet runs.
  • After Run: stdout and stderr panels, plus exception traceback if Python raised.

Props​

PropTypeRequiredDefaultNotes
initialCodestringyesβ€”The Python source the learner sees
captionstringnoβ€”Short description rendered above the editor
runLabelstringno"Run"Button label override (e.g. "Fit model")

Source: apps/docs/src/components/pyodide/RunnablePython.tsx.

How it works​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Main thread β€” React island β”‚
β”‚ <RunnablePython> β”‚
β”‚ ↓ uses β”‚
β”‚ useRunner() hook β”‚
β”‚ ↓ spawns β”‚
β”‚ Worker(pyodide.worker.ts, type: 'module') β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ postMessage
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Worker thread β”‚
β”‚ import('../pyodide/pyodide.module.js') β”‚
β”‚ ↓ β”‚
β”‚ loadPyodide({ indexURL: '../pyodide/' }) β”‚
β”‚ ↓ β”‚
β”‚ On run: β”‚
β”‚ captureStdoutStderr() β”‚
β”‚ pyodide.runPython(code) β”‚
β”‚ return { stdout, stderr, error? } β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key files:

First-run cost​

Pyodide is ~13 MB (WASM + stdlib). The first Run click on a page boots Pyodide; expect 2–5 s on a warm cache, longer on cold. Subsequent runs are fast (50–200 ms for small snippets).

The component shows status messages during boot:

Loading Pyodide…
Pyodide ready.
Running…
Done.

So the learner knows what's happening.

Examples​

Simple expression​

<RunnablePython
client:visible
initialCode={`# Try changing the operator
print(2 ** 8)
`}
/>

Multi-line script​

<RunnablePython
client:visible
caption="Pure functions, no I/O β€” fast on every run."
initialCode={`def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a

for i in range(10):
print(f"fib({i}) = {fib(i)}")
`}
/>

Stdin / input β€” not yet​

input() doesn't work in Pyodide-in-worker yet because it's a synchronous blocking call and the worker has no terminal. Stub it for the lesson:

<RunnablePython
client:visible
initialCode={`# In a real terminal you'd use input(). Here, hardcode:
name = "Robot"
print(f"Hello, {name}!")
`}
/>

The framework's runner roadmap includes COOP/COEP-page support that unblocks input() (Phase 3 per the source's notes). Until then, this limitation is real.

Imports from the standard library​

Most pure-Python stdlib modules work:

<RunnablePython
client:visible
initialCode={`from collections import Counter

words = "the quick brown fox jumps over the lazy dog".split()
counts = Counter(words)
print(counts.most_common(3))
`}
/>

Imports from third-party packages​

Pyodide can pip-install pure-Python wheels and a curated set of native packages (numpy, pandas, scipy, etc.). The base bundle doesn't include them β€” you'd await pyodide.loadPackage('numpy') from JS first. The current RunnablePython MVP doesn't expose this; for now, stick to the stdlib. If you need numpy/pandas/etc. in a lesson, file an issue.

Long-running cell​

<RunnablePython
client:visible
runLabel="Fit model"
initialCode={`import time
print("Training…")
time.sleep(2)
print("Done.")
`}
/>

The button label changes from Run to Fit model. While running, the button shows Running… and is disabled.

What the learner can and can't do​

βœ… Edit any character of initialCode and re-run.
βœ… Use any pure-Python stdlib module.
βœ… See full traceback on errors.
βœ… See stdout and stderr in separate panels.
βœ… Iterate freely β€” runs are sandboxed; nothing escapes the worker.

❌ Use input() (deferred to Phase 3).
❌ Persist state across page reloads (deferred).
❌ Fetch arbitrary URLs without CORS shenanigans (use pyodide.loadPackage for known mirrors).
❌ Spawn subprocesses, open sockets, or write to disk (Pyodide is sandboxed).

Authoring tips​

  • Show print output, not return values. Pyodide doesn't echo the last expression (no REPL). Wrap the interesting value in a print(...) so the learner sees something.
  • Keep snippets to ~20 lines. Longer snippets feel like cheating in a lesson; if you need 50 lines of setup, it belongs in a real notebook, not a lesson cell.
  • Surface the gotchas. Python's 0.1 + 0.2 == 0.3 β†’ False is perfect cell material. So is [1, 2] == [1.0, 2.0] β†’ True (type coercion).
  • Don't import os and ask learners to do filesystem things. Pyodide's filesystem is virtual; os.listdir('/') shows Pyodide's internal layout, which is a distraction.
  • Consider replacing with <RunnableRobot> if your lesson is about Robot Framework specifically. RF in Pyodide gives the learner real RF semantics (test cases, keywords, log.html / report.html).

SCORM packaging implications​

Pyodide is heavy. By default the SCORM zip excludes Pyodide; the runner falls back to fetching it from the same origin (which fails in a zero-network LMS). To bundle Pyodide for offline-capable LMS delivery:

INCLUDE_PYODIDE_RUNTIME=1 \
node apps/docs/scripts/package-scorm12.mjs

This adds ~6.3 MB compressed to the zip. See SCORM 1.2 packaging for the full surface.

Where to go next​

  • Runnable Robot Framework cells β€” full RF in-browser via the same Pyodide infrastructure, but with the RF wheel + CodeMirror's RF grammar pre-wired.
  • Code blocks β€” when you only need to show Python, not run it.