Pyodide: Python in the Browser & Node.js
Pyodide is an open-source project that compiles the CPython interpreter to WebAssembly, enabling the full execution of Python directly within web browsers and Node.js environments without requiring a server backend. Its current stable release is 0.29.3, with frequent patch and minor updates; a significant version change to 314.0.0a1 signals alignment with upcoming Python 3.14 releases and a revised ABI stabilization strategy. Key differentiators include seamless bidirectional interoperability between JavaScript and Python, comprehensive support for a vast array of popular scientific Python libraries (such as NumPy, Pandas, and Matplotlib), and a lightweight package manager (`micropip`) that allows dynamic installation of pure Python wheels from PyPI. This enables advanced client-side data processing, interactive visualizations, and powerful educational tools to run entirely within the browser, significantly expanding Python's reach in web development.
Common errors
-
Uncaught (in promise) Error: Pyodide is already loading
cause Attempting to call `loadPyodide` multiple times, especially after a prior load failed or is still in progress, without proper cleanup.fixEnsure `loadPyodide` is only called once per worker or page lifecycle. If a load fails, a full page/worker reload might be necessary, as internal state is not easily reset. Implement retry logic around the entire worker initialization if network stability is a concern. -
Module not found: Can't resolve 'fs/promises'
cause This error typically occurs when bundling Pyodide for a browser environment using tools like Webpack or Vite, as Pyodide's internal Node.js compatibility shims (which use `fs/promises`) are not correctly polyfilled or excluded for the browser target.fixConfigure your bundler to correctly polyfill Node.js modules for browser environments. For Webpack, this might involve `resolve.fallback`. Ensure `pyodide` is treated as an external module or that its Node.js-specific parts are properly handled if you are only targeting the browser. In some cases, updating your bundler configuration or Pyodide version can resolve this. -
ModuleNotFoundError: No module named 'pyodide'
cause This error occurs when trying to `pip install pyodide` in a native Python environment. The `pyodide` npm package is a JavaScript distribution, not a Python package intended for native Python installation. The Python module `pyodide` (and `piplite`) is only available within the Pyodide runtime or the `pyodide-py` package for type-checking.fixTo use Pyodide, load it via JavaScript in a browser or Node.js environment. Do not try to `pip install pyodide`. If you need the Python `pyodide` module's API for type hints in a native Python project, install `pyodide-py` via `pip install pyodide-py`. When running within Pyodide, the `pyodide` module is available by default, and other packages are installed using `micropip.install`. -
TypeError: object of type 'XXX' is not JSON serializable (from Python code)
cause Attempting to return complex Python objects (like NumPy arrays, Pandas DataFrames, or custom Python classes) directly to JavaScript, which expects JSON-serializable types or primitive values. JavaScript does not automatically know how to serialize arbitrary Python objects.fixConvert Python objects to JavaScript-compatible types before returning them. For NumPy arrays, use `tolist()` (`py_array.tolist()`). For Pandas DataFrames, use `to_json()` or `to_dict()` (`df.to_json()`). For custom objects, provide a `__js_repr__` method or manually convert to basic Python types (lists, dicts) before converting to JS using `pyodide.toJs(python_object)`.
Warnings
- breaking Pyodide versions 0.x have undergone significant API redesigns, particularly around 0.17.0, which introduced a more robust `asyncio` integration and overhauled core APIs. Older code relying on pre-0.17 APIs will likely break.
- gotcha Pyodide bundles a full CPython interpreter and many scientific libraries, leading to a large initial download size (hundreds of MB for full distribution). This can severely impact page load performance.
- gotcha Long-running Python computations executed directly on the main thread can block the browser's UI, leading to unresponsiveness.
- gotcha When transferring Python objects to JavaScript, Pyodide creates `PyProxy` objects. Failing to explicitly call `.destroy()` on these proxies (especially for frequently created or large objects) can lead to memory leaks over time.
- gotcha Calling `loadPyodide()` again after a previous attempt failed (e.g., due to a network error) will often throw an `Error: Pyodide is already loading` or similar, preventing retry logic.
- breaking Pyodide is expected to transition to a new versioning scheme, potentially aligning with Python minor versions (e.g., 3.14.x) instead of the current 0.x series. This will be accompanied by a new ABI, which means packages built for older Pyodide ABIs will not be compatible with new major Pyodide versions.
Install
-
npm install pyodide -
yarn add pyodide -
pnpm add pyodide
Imports
- loadPyodide
import loadPyodide from 'pyodide'; // Not a default export
import { loadPyodide } from 'pyodide'; // Or for older Node.js/CJS: // const { loadPyodide } = require('pyodide'); - PyodideInterface
import type { PyodideInterface } from 'pyodide'; - pyodide.runPython
pyodide.runPython('print("Hello")'); // Missing await, as it returns a Promiseawait pyodide.runPython(` import sys print('Hello from Python!', file=sys.stderr) `); - pyodide.loadPackage
pyodide.loadPackage('pandas'); // Missing awaitawait pyodide.loadPackage('numpy'); - micropip.install
await pyodide.install('requests'); // No direct 'install' method on pyodide objectawait pyodide.loadPackage('micropip'); const micropip = pyodide.pyimport('micropip'); await micropip.install('matplotlib');
Quickstart
import { loadPyodide } from 'pyodide';
async function main() {
console.log('Initializing Pyodide...');
const pyodide = await loadPyodide({
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.25.0/full/' // Always pin the version!
});
console.log('Pyodide initialized.');
// Redirect Python stdout/stderr to JS console
pyodide.setStdout((text) => console.log('PY_STDOUT:', text));
pyodide.setStderr((text) => console.error('PY_STDERR:', text));
// Run simple Python code
console.log('Running Python code...');
const pythonResult = await pyodide.runPython(`
import sys
print('Hello from Python!')
x = 10
y = 20
x + y
`);
console.log('Python computed:', pythonResult);
// Load a Python package and use it
console.log('Loading NumPy...');
await pyodide.loadPackage('numpy');
const numpyVersion = pyodide.runPython('import numpy; numpy.__version__');
console.log('NumPy version:', numpyVersion);
const jsArray = [1, 2, 3, 4, 5];
pyodide.globals.set('js_array', jsArray);
const pythonArrayResult = await pyodide.runPython(`
import numpy as np
py_array = np.array(js_array)
print(f'Python array sum: {py_array.sum()}')
py_array.tolist()
`);
console.log('Python processed array:', pythonArrayResult);
// Clean up PyProxy objects if necessary (important for preventing memory leaks)
pyodide.destroy();
console.log('Pyodide instance destroyed.');
}
main().catch(err => {
console.error('An error occurred:', err);
});