Choosing Between Pyodide and MicroPython for PyScript Applications
A decision guide for picking Pyodide or MicroPython in PyScript: compare bundle size, startup time, package availability, threading, and JavaScript interop. Includes minimal working HTML for each runtime and a validation checklist using browser DevTools.
24 Jul 2026, 14:34 UTC

The Decision at a Glance
PyScript lets you run Python in the browser via WebAssembly, but you must pick a runtime at load time: Pyodide (a CPython port with the scientific stack) or MicroPython (a minimal implementation). The choice determines which packages you can import, how fast the page becomes interactive, and whether you can use threading. This guide lays out the constraints, compares the runtimes side by side, and shows how to validate the choice in your own environment.
Constraints That Drive the Choice
- Bundle size budget: Pyodide downloads 6–10 MB (compressed) of WebAssembly and standard library; MicroPython downloads 300–500 KB.
- Startup latency tolerance: Pyodide typically takes 1–3 seconds to initialize on a warm cache; MicroPython is ready in under 200 ms.
- Package requirements: If you need NumPy, pandas, SciPy, or any C-extension wheel, you need Pyodide. MicroPython cannot load binary wheels.
- Threading needs: Pyodide supports
threadingvia Web Workers when the server sendsCross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corp. MicroPython has no threading support. - Language features: Pyodide provides a near-complete CPython 3.12 standard library including
asyncio. MicroPython omitsasyncio, has a limitedosmodule, and lacks many built-ins.
Side-by-Side Comparison
| Factor | Pyodide | MicroPython |
|---|---|---|
| Compressed download | 6–10 MB | 300–500 KB |
| Cold-start time (typical) | 1–3 s | <200 ms |
| Standard library coverage | Near-complete CPython 3.12 | Subset (no asyncio, limited os) |
| Prebuilt scientific packages | 100+ (NumPy, pandas, SciPy, Matplotlib…) | Very few; C-extensions unsupported |
| Pure-Python wheel install at runtime | micropip.install() works reliably |
Experimental; often fails |
| JavaScript interop | Full js module with proxy objects, Promise-to-await conversion |
Basic js module; manual await js.Promise.resolve(...) required |
| Threading / Web Workers | Supported (needs COOP/COEP headers) | Not supported |
| Typical use cases | Data science, ML inference, complex apps | UI scripting, education, tiny widgets, progressive enhancement |
Trade-offs in Practice
Pyodide gives you the full Python data-science ecosystem in the browser, but the cost is paid on every page load until the WebAssembly modules are cached in IndexedDB. Memory consumption can exceed 100 MB once NumPy and pandas are instantiated. If your hosting platform cannot serve the required COOP/COEP headers, you lose threading entirely.
MicroPython keeps the page lightweight and interactive almost instantly. However, you cannot import numpy or use asyncio. The js interop layer is thinner—promises don't auto-convert to awaitables, so you write more boilerplate when calling browser APIs. Third-party pure-Python packages may install via micropip, but the success rate is low.
Progressive enhancement strategy: Start with MicroPython for the initial UI. When the user triggers a data-intensive action, dynamically load a Pyodide worker (via py-config worker="true") and hand off the heavy computation. This keeps the critical path fast while still unlocking the scientific stack on demand.
Concrete Implementation: Minimal Pages for Each Runtime
Pyodide Page (data-science ready)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pyodide Demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pyscript/core@2024.11.1/dist/core.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@pyscript/core@2024.11.1/dist/core.js"></script>
</head>
<body>
<py-config type="pyodide">
packages = ["numpy", "pandas"]
[[fetch]]
files = ["data.csv"]
</py-config>
<div id="output">Loading…</div>
<py-script>
import numpy as np
import pandas as pd
from js import document
# Example computation
data = np.random.randn(1000)
mean_val = data.mean()
std_val = data.std()
output = document.getElementById("output")
output.innerText = f"Mean: {mean_val:.4f}, Std: {std_val:.4f}"
</py-script>
</body>
</html>
Save as pyodide-demo.html. The packages array tells Pyodide to fetch the NumPy and pandas wheels from the CDN before your script runs. The [[fetch]] block pre-caches a CSV file so pandas.read_csv("data.csv") works offline after the first visit.
MicroPython Page (tiny widget)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MicroPython Demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pyscript/core@2024.11.1/dist/core.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@pyscript/core@2024.11.1/dist/core.js"></script>
</head>
<body>
<py-config type="mpy"></py-config>
<button id="btn">Click me</button>
<div id="msg"></div>
<py-script>
from js import document, console
btn = document.getElementById("btn")
msg = document.getElementById("msg")
count = 0
def on_click(event):
global count
count += 1
msg.innerText = f"Button clicked {count} times"
console.log(f"Click #{count}")
btn.addEventListener("click", on_click)
</py-script>
</body>
</html>
Save as mpy-demo.html. No packages section is needed—the runtime loads in a few hundred kilobytes. Note the manual event-listener binding; MicroPython's js module does not auto-wrap callbacks.
Validation Checklist
- Serve locally: Run
python -m http.server 8000in the folder containing the HTML files, then openhttp://localhost:8000/pyodide-demo.htmlandhttp://localhost:8000/mpy-demo.html. - Measure download size: Open DevTools → Network tab, filter for
wasmorapplication/wasm. Pyodide should show multiple .wasm files totaling 6–10 MB compressed; MicroPython shows a single ~300 KB file. - Time startup: In the Console, run
performance.now()after the page loads, or addconst t0 = performance.now()at the top of a<script>and logperformance.now() - t0inside the first<py-script>block. - Verify imports: Confirm no red errors in Console. For Pyodide,
import numpy; print(numpy.__version__)should succeed. For MicroPython,import sys; print(sys.implementation.name)should printmicropython. - Test offline: Load each page once, then stop the server (Ctrl-C) and refresh. Pyodide serves cached wheels from IndexedDB; MicroPython serves its runtime from the browser cache. Both should still run.
- Cross-browser check: Repeat in Firefox and Safari. Watch for COOP/COEP warnings if you enable Pyodide threading (
py-config worker="true").
Common Pitfalls to Watch
- CDN MIME types: Some corporate proxies block
application/wasm. If the .wasm files download asapplication/octet-stream, the browser will reject them. Configure your CDN or host the runtime assets yourself. - Package version pinning: Pyodide's
packageslist resolves to specific wheel versions bundled with the Pyodide release. You cannotpip install numpy==2.0.0if Pyodide 0.27 ships NumPy 1.26. Check the package index for the current mapping. - Shared global state: Multiple
<py-script>tags on the same page share the same interpreter instance unless you isolate them withpy-config src="worker.py" worker="true". Plan for variable collisions. - MicroPython async gaps:
await fetch(...)won't work directly. Useresp = await js.Promise.resolve(js.fetch(url))thendata = await js.Promise.resolve(resp.json()).
When to Revisit the Decision
If your MicroPython widget grows to need a single NumPy operation, you don't have to rewrite the whole page. Add a second <py-config type="pyodide" worker="true" src="heavy.py"></py-config> and communicate via postMessage. The main thread stays on MicroPython; the worker spins up Pyodide only when the user triggers the heavy task.
Conversely, if a Pyodide page feels sluggish on mobile, profile with Lighthouse. Consider lazy-loading the runtime: defer the <script> that loads core.js until the user scrolls to the interactive section, or use py-config autoload="false" and call PyScript.init() programmatically.
Bottom Line
Choose Pyodide when your application requires NumPy, pandas, SciPy, or CPython-standard-library completeness, and you can absorb a 6–10 MB download and 1–3 s startup. Choose MicroPython when bundle size and time-to-interactive are the primary constraints and your logic stays within the standard library plus simple DOM manipulation. Validate with the checklist above on your actual target browsers and network conditions before committing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.