Pyodide & WebAssembly Tutorial

Run Python in your browser. No server required.

Loading Pyodide...

1 What is WebAssembly (WASM)?

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.

Key insight:

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.

How WASM works

Source Code Compiler Browser ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ C / Rust │──────>│ Emscript │──────> │ .wasm binary │ │ Python │ or │ wasm-pack│ │ │ │ Go / etc. │ │ LLVM │ │ Loaded & executed │ └──────────┘ └──────────┘ │ by the browser's │ │ WASM VM │ └──────────────────┘

Live demo: Loading a .wasm file

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
Click "Run WASM Demo" to load add.wasm

2 What is Pyodide?

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.

Python for you:

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.

How Pyodide loads

1. Browser downloads pyodide.js (~200 KB bootstrap script)
2. Bootstrap fetches pyodide.asm.wasm (~7 MB compressed) — the CPython interpreter compiled to WASM
3. It loads python_stdlib.zip for the standard library and pyodide-lock.json for package metadata
4. You get a pyodide object with .runPython(), .runPythonAsync(), and more

Your first Pyodide code

import sys
print(f"Python {sys.version}")
print(f"Platform: {sys.platform}")
print(f"Running in WASM: {'emscripten' in sys.platform}")
Waiting for Pyodide to load...

3 Running Python in the Browser

Pyodide gives you two main ways to execute Python:

Interactive Python editor

Edit the code below and hit Run. This is real Python, running in your browser via WASM.

Waiting for Pyodide to load...

4 Loading Python Packages

Pyodide includes micropip, a package installer that works in the browser. It can install packages from:

What's a wheel?

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.

Installing from PyPI

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()}")

Loading a local wheel from a relative path

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())
Waiting for Pyodide to load...

5 Python ↔ JavaScript Bridge

Pyodide provides seamless interop between Python and JavaScript. You can access JavaScript objects from Python and vice versa.

Python accessing JavaScript & the DOM

Waiting for Pyodide to load...

JavaScript calling Python functions

// 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]

6 Data Science in the Browser

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.

Waiting for Pyodide to load...

7 WASM Wheels: Compiled Extensions in the Browser

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.

Two flavors of WASM 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.

How a WASM wheel works

┌────────────────────────────────────────────────────┐ │ Browser │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ Pyodide (CPython WASM) │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ │ │ Your Python code │ │ │ │ │ │ >>> import pydantic_monty │ │ │ │ │ │ >>> monty.run("print(1 + 2)") │ │ │ │ │ └───────────────┬─────────────────────────┘ │ │ │ │ │ calls │ │ │ │ ┌───────────────▼─────────────────────────┐ │ │ │ │ │ WASM wheel (.so compiled from Rust) │ │ │ │ │ │ pydantic_monty-0.0.3-...-wasm32.whl │ │ │ │ │ └─────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ All running in WebAssembly. No server needed. │ └────────────────────────────────────────────────────┘

Building your own WASM wheel

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")

8 Deployment & Self-Hosting

This tutorial uses the Pyodide CDN (cdn.jsdelivr.net) for convenience. For production or offline use, you can self-host Pyodide entirely.

Option A: CDN (what this tutorial uses)

<!-- 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>

Option B: Self-hosted (all files local)

# 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 deployment

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.

9 Playground

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.

Waiting for Pyodide to load...

10 Further Reading