Run Python in your browser. No server required.
WebAssembly is a binary instruction format that runs in the browser at near-native speed. Think of it as a portable compilation target — languages like C, C++, Rust, and even Python can be compiled to WASM and run anywhere a browser (or WASM runtime) exists.
WASM isn't a replacement for JavaScript. It's a complement. Heavy computation (data processing, image manipulation, cryptography, running an entire Python interpreter) can run in WASM while JavaScript handles the DOM and UI.
This page includes a tiny add.wasm file (41 bytes!) that exports a single
add(a, b) function. It was hand-crafted from WebAssembly binary format —
no compiler needed for something this simple.
// Loading a .wasm file from a relative path
const response = await fetch('./add.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
instance.exports.add(40, 2); // 42
Pyodide is CPython compiled to WebAssembly using Emscripten. It gives you a full Python 3.12+ interpreter running entirely in the browser — with access to the standard library, NumPy, Pandas, and 100+ other packages.
Since you already know Python, Pyodide lets you use your existing skills to build interactive web tools, data visualizations, and educational demos — all without writing JavaScript or running a server.
pyodide.js (~200 KB bootstrap script)
pyodide.asm.wasm (~7 MB compressed) — the CPython interpreter compiled to WASM
python_stdlib.zip for the standard library and pyodide-lock.json for package metadata
pyodide object with .runPython(), .runPythonAsync(), and more
import sys
print(f"Python {sys.version}")
print(f"Platform: {sys.platform}")
print(f"Running in WASM: {'emscripten' in sys.platform}")
Pyodide gives you two main ways to execute Python:
pyodide.runPython(code) — synchronous, returns the last expression's valuepyodide.runPythonAsync(code) — async, supports await and top-level importsEdit the code below and hit Run. This is real Python, running in your browser via WASM.
Pyodide includes micropip, a package installer that works in the browser.
It can install packages from:
.whl files served alongside your HTML
A .whl file is Python's standard distribution format — it's just a ZIP file
with a specific naming convention (name-version-pytag-abitag-platform.whl).
For Pyodide, pure-Python wheels (py3-none-any.whl) work out of the box.
Compiled extensions need to be built specifically for the emscripten platform.
import micropip
await micropip.install("regex") # installs from PyPI
import regex
m = regex.search(r"\b\w+(?:ing)\b", "Pyodide is amazing for learning")
print(f"Found: {m.group()}")
This tutorial directory includes tutorial_utils-0.1.0-py3-none-any.whl —
a small package we built to demonstrate local wheel loading. Because this HTML file
and the .whl are served from the same directory on GitHub Pages,
we can install it with a relative URL:
import micropip
await micropip.install("./tutorial_utils-0.1.0-py3-none-any.whl")
import tutorial_utils as tu
print(tu.demo())
Pyodide provides seamless interop between Python and JavaScript. You can access JavaScript objects from Python and vice versa.
// JavaScript side:
const pyFn = pyodide.runPython(`
def process(data):
return [x ** 2 for x in data if x % 2 == 0]
process
`);
pyFn([1, 2, 3, 4, 5, 6]).toJs();
// Returns: [4, 16, 36]
Pyodide ships with pre-built versions of NumPy, Pandas, scikit-learn, matplotlib, and more. These are full compiled extensions (C/Fortran) cross-compiled to WASM via Emscripten.
This is the frontier that Simon Willison's recent blog post explores. Beyond pure-Python wheels, you can compile Rust or C code into WASM wheels that load inside Pyodide. This is how projects like Pydantic's Monty (a sandboxed Python subset written in Rust) run in the browser.
1. Standalone WASM — Load a .wasm file directly in JavaScript
(like our add.wasm example in Section 1). Fast, minimal, but you write JavaScript glue code.
2. WASM wheel in Pyodide — Compile your Rust/C extension into a
.whl targeting the emscripten platform. Install it with micropip and use it
from Python. The entire stack (Python + your extension) runs in WASM.
If you want to compile a Rust extension for Pyodide:
# 1. Set up the Emscripten toolchain
$ git clone https://github.com/emscripten-core/emsdk.git
$ cd emsdk && ./emsdk install latest && ./emsdk activate latest
# 2. Build with pyodide-build (for Rust packages using PyO3/maturin)
$ pip install pyodide-build
$ pyodide build # in your Python package directory
# 3. The output is a .whl file you can serve alongside your HTML
# 4. Install in Pyodide via micropip:
# await micropip.install("./my_package-0.1.0-cp312-cp312-emscripten_3_1_58_wasm32.whl")
This tutorial uses the Pyodide CDN (cdn.jsdelivr.net) for convenience.
For production or offline use, you can self-host Pyodide entirely.
<!-- Just add the script tag. indexURL is auto-detected. -->
<script src="https://cdn.jsdelivr.net/pyodide/v0.29.3/full/pyodide.js"></script>
<script>
const pyodide = await loadPyodide();
</script>
# Download the Pyodide release (~200MB full, or ~15MB core)
$ wget https://github.com/pyodide/pyodide/releases/download/0.27.5/pyodide-0.27.5.tar.bz2
$ tar xjf pyodide-0.27.5.tar.bz2 -C ./tutorial/pyodide/
# Your directory structure:
# tutorial/
# pyodide-wasm-tutorial.html
# pyodide/
# pyodide.js (~200 KB)
# pyodide.asm.wasm (~7 MB)
# pyodide.asm.js (~400 KB)
# python_stdlib.zip (~5 MB)
# pyodide-lock.json
# *.whl (pre-built packages)
<!-- Load from relative path. indexURL auto-detects from script src. -->
<script src="./pyodide/pyodide.js"></script>
<script>
// No indexURL needed - it's inferred from the script tag location
const pyodide = await loadPyodide();
</script>
GitHub Pages serves static files with correct WASM MIME types and CORS headers —
making it an ideal free host. Just push your tutorial/ directory and enable
GitHub Pages from Settings → Pages → Deploy from branch.
# Your GitHub Pages URL will be:
# https://<username>.github.io/<repo>/tutorial/pyodide-wasm-tutorial.html
#
# The .whl and .wasm files are at:
# https://<username>.github.io/<repo>/tutorial/tutorial_utils-0.1.0-py3-none-any.whl
# https://<username>.github.io/<repo>/tutorial/add.wasm
#
# Because they're in the same directory, relative paths like
# "./add.wasm" and "./tutorial_utils-0.1.0-py3-none-any.whl" just work.
Try anything you want. The full Python standard library is available, plus any
packages you install with micropip. Use await freely —
the code runs via runPythonAsync.