Loading Python Packages in PyScript with py-config: What Works and What Doesn't
How to declare Python dependencies in PyScript's py-config, which packages actually load in the Pyodide runtime, and the compatibility and performance limits that decide whether PyScript fits your project.
25 Aug 2025, 13:30 UTC

If you want PyScript to do anything beyond trivial DOM manipulation, the first real decision is how you declare dependencies. The answer: list them in the <py-config> tag (or an equivalent config object, depending on your PyScript version), and the runtime will fetch and install them into the Pyodide environment before your <py-script> code runs. The catch is that only pure-Python wheels and packages explicitly built for Pyodide will load — everything else fails at install time, not at import time.
How the pieces fit together
PyScript itself is a thin layer. The actual Python interpreter is Pyodide, a build of CPython compiled to WebAssembly, which PyScript downloads from a CDN on first page load. When the page loads, the sequence is roughly:
- The PyScript JavaScript bundle loads and reads your config.
- Pyodide's Wasm binary and the Python standard library download (this is the multi-megabyte step users notice).
- Each package in your
packageslist is resolved — either from Pyodide's own package index or, for pure-Python wheels, from PyPI via micropip. - Your
<py-script>code executes.
Because step 3 happens before your code runs, a bad package name or an incompatible package means your script never executes. The error shows up in the browser console, not on the page, which confuses people who expect a visible failure.
A worked configuration
Version note: PyScript's tag-based API (<py-config>, <py-script>) has changed across releases, and newer versions moved toward a <script type="py"> model with JSON config. The example below follows the classic tag-based form; check the current PyScript documentation for the exact attribute names in the release you pin. Always pin a version in the CDN URLs rather than using latest.
<!DOCTYPE html>
<html>
<head>
<!-- Pin an exact version; do not use "latest" in production -->
<link rel="stylesheet" href="https://cdn.example/pyscript/PINNED_VERSION/pyscript.css" />
<script defer src="https://cdn.example/pyscript/PINNED_VERSION/pyscript.js"></script>
</head>
<body>
<py-config>
packages = ["numpy", "matplotlib"]
</py-config>
<div id="output">Waiting for Python...</div>
<py-script>
import numpy as np
from js import document
arr = np.arange(10)
document.getElementById("output").innerText = f"Sum: {arr.sum()}"
</py-script>
</body>
</html>Run this by serving the file over HTTP (e.g., python -m http.server from your shell — file:// URLs break some loading behavior) and opening it in a browser. No special permissions are needed; everything runs client-side. To verify it worked:
- The div text changes from "Waiting for Python..." to "Sum: 45".
- In the browser's Network tab, you should see the Pyodide
.wasmbinary and package files downloading. If numpy fails to install, the console shows a micropip/Pyodide error before your code runs.
numpy and matplotlib work here because Pyodide ships prebuilt versions of them. That is the exception, not the rule.
The package compatibility wall
This is the limit that kills most real projects. A package is loadable only if one of these holds:
- It is pure Python (a wheel with no compiled extensions) — micropip can install it from PyPI.
- It has been compiled for Pyodide — the Pyodide project maintains builds of popular scientific packages (numpy, pandas, scipy, matplotlib, scikit-learn and others, with the exact list varying by Pyodide release).
Anything with a C, Cython, or Rust extension that Pyodide hasn't built — think psycopg2, cryptography in some forms, most database drivers, anything wrapping a native library — cannot be installed. There is no workaround inside the browser; the usual fix is to move that work to a backend API and call it with fetch from Python.
Practical check before committing to PyScript for a project: open a Pyodide console (the Pyodide project hosts one) and try await micropip.install("your-package") for each dependency. If it installs and imports there, it will work in PyScript.
Performance limits worth planning around
- Cold load time. The Pyodide runtime plus stdlib is several megabytes, and each package adds more. Browsers cache the Wasm, but first visits are slow. Show a loading indicator and don't put PyScript on a landing page where bounce matters.
- FFI overhead. Crossing the Python↔JavaScript boundary (via
from js import documentor proxies) has per-call cost. Fine for updating a div on a button click; bad for per-pixel canvas loops or high-frequency event handlers. Batch DOM work or do it in JavaScript. - No threads, limited sockets. Browser sandboxing means no raw sockets and no true threading. Async works via the browser event loop, but code written around
requestsor blocking I/O needs reworking aroundfetch/pyodide.http.
Common mistakes
- Assuming PyPI availability equals PyScript availability. Check for a pure-Python wheel or a Pyodide build first.
- Using unpinned CDN URLs. PyScript's API has changed between releases; an unpinned script tag can silently break a working page.
- Debugging on the page instead of the console. Install and runtime errors go to the browser developer console. Open it first.
- Doing heavy DOM manipulation in Python. Keep Python for computation; hand rendering to JavaScript when it's hot-path work.
- Serving over file://. Some browsers block module/Wasm loading from local files. Use a local HTTP server.
The decision rule: PyScript is a good fit when your logic is Python-shaped, your dependencies are pure Python or in Pyodide's built set, and your users tolerate a heavy first load. If any of those fail, a small JavaScript frontend calling a Python backend is usually less total work.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.