Frequently Asked Questions
This page addresses common questions and troubleshooting scenarios encountered by the PyScript community on Discord, in community calls, and through general usage.
The FAQ covers two main areas: common errors and helpful hints.
Common errors
Reading error messages
When your application doesn't run and you see no error messages on the page, check your browser's console.
The last line of an error message usually reveals the problem:
Traceback (most recent call last):
File "/lib/python311.zip/_pyodide/_base.py", line 501, in eval_code
.run(globals, locals)
^^^^^^^^^^^^^^^^^^^^
File "/lib/python311.zip/_pyodide/_base.py", line 339, in run
coroutine = eval(self.code, globals, locals)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<exec>", line 1, in <module>
NameError: name 'failure' is not defined
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'failure' isn't defined
Both examples show a
NameError
because the name failure doesn't exist. Everything above the error
message provides potentially useful technical detail for debugging.
These are the most common errors PyScript users encounter.
SharedArrayBuffer
This is the most common error new PyScript users face:
Failure
Your application doesn't run and your browser console shows:
When
This error occurs when code running in a worker tries to access window
or document objects that exist on the main thread.
The error indicates either your web server is incorrectly configured
or a service-worker attribute is missing from your script element.
Specifically, one of three situations applies:
Your web server configuration prevents the browser from enabling Atomics
(a technology for cross-thread communication). When your script element
has a worker attribute and your Python code uses window or
document objects that exist on the main thread, this browser
limitation causes failure unless you reconfigure your server.
You're using <script type="py-editor"> (which always runs in a worker)
without providing a fallback via a service-worker attribute on that
element.
You've explicitly created a PyWorker or MPWorker instance somewhere
in your code without providing a service_worker fallback.
The workers guide documents all these cases with code examples and solutions.
Why
For document.getElementById('some-id').value to work in a worker,
JavaScript requires two primitives:
SharedArrayBuffer allows multiple threads to read and write shared memory.
Atomics
provides wait(sab, index) and notify(sab, index) to coordinate
threads, where sab is a SharedArrayBuffer.
Whilst a worker waits for a main thread operation, it doesn't consume CPU. It idles until the referenced buffer index changes, effectively never blocking the main thread whilst pausing its own execution.
These primitives make worker-main thread communication seamless for developers. We encourage using workers over running Python on the main thread, especially with Pyodide, because workers keep the user interface responsive during heavy computation.
Unfortunately, we cannot patch or work around these primitives - they're defined by web standards. However, various solutions exist for working within these limitations. The workers guide explains how.
Borrowed proxy
This common error occurs with event listeners, timers, or anywhere JavaScript lazily invokes a Python callback:
Failure
Your browser console shows:
When
This error happens when using Pyodide on the main thread and passing a bare Python function as a JavaScript callback:
The garbage collector immediately cleans up the Python function after passing it to JavaScript. For the function to work as a future callback, it must not be garbage collected - hence the error.
Info
This error doesn't occur when code executes in a worker and the JavaScript reference comes from the main thread:
from pyscript import window
# This works fine in a worker.
window.setTimeout(lambda x: print(x), 1000, "OK")
Proxy objects cannot be communicated between a worker and the main thread. Behind the scenes, PyScript ensures references are maintained between workers and the main thread. Worker-based Python functions appear as JavaScript proxy objects on the main thread, avoiding the borrowed proxy problem.
Two solutions exist:
Manually wrap functions with
pyscript.ffi.create_proxy:
Or set
experimental_create_proxy = "auto"
in your configuration. This intercepts Python objects passed to
JavaScript callbacks and manages memory automatically via JavaScript's
garbage collector.
Note
FinalizationRegistry
enables automatic memory management. It's not observable and you
cannot predict when it frees proxy objects. Memory consumption might
be slightly higher than manual create_proxy, but JavaScript's
engine manages memory efficiently and frees retained proxies when
memory pressure increases.
Why
Pyodide and MicroPython both have garbage collectors for automatic memory management. When Python object references pass to JavaScript via the FFI, Python interpreters cannot guarantee JavaScript's garbage collector will free them. They may lose control since there's no reliable way to know when JavaScript no longer needs the objects.
One solution expects users to explicitly create and destroy proxy objects. But manual memory management defeats automatic collection and risks dead references - destroying Python objects still active in JavaScript. This creates difficulty.
Pyodide provides
ffi.wrappers
for common cases. PyScript's experimental_create_proxy = "auto"
configuration automates memory management via FinalizationRegistry.
Python packages
Sometimes Python packages specified via
packages configuration don't
work with PyScript's interpreters.
Failure
Using Pyodide: Your browser console shows:
Failure
Using MicroPython: Your browser console shows:
When
This is a complicated problem, but the summary is:
First, check you've used the correct package name. This is a remarkably common mistake worth verifying first.
In Pyodide, the error indicates the package contains code written in C, C++, or Rust. These compiled languages haven't been compiled for WebAssembly. The Pyodide project and Python Packaging Authority are working to make WebAssembly a default compilation target. Until then, follow Pyodide's guidance to overcome this limitation.
In MicroPython, the package hasn't been ported to the
micropython-lib repository.
To use pure Python packages with MicroPython, use the
files configuration to manually
copy the package onto the filesystem, or reference it via URL.
The packaging pointers section provides hints and tips about packaging with PyScript.
Why
Pyodide and MicroPython are different Python interpreters running in WebAssembly. Packages built for Pyodide may not work for MicroPython, and vice versa. Furthermore, packages containing compiled code may not have been compiled for WebAssembly.
If a package is written in Python that both interpreters support (subtle differences exist between them), you should be able to use it by getting it into the Python path via configuration.
Currently, MicroPython cannot expose modules requiring native
compilation, but PyScript is working with the MicroPython team to
provide builds including commonly requested packages (like MicroPython's
versions of numpy or sqlite).
Warning
Depending on complexity, seamlessly porting from Pyodide to MicroPython may be difficult. MicroPython has comprehensive documentation explaining differences from "regular" CPython (the version Pyodide provides).
JavaScript modules
When using JavaScript modules you may encounter these errors:
Failure
Uncaught SyntaxError: The requested module './library.js' does not provide an export named 'default'
Failure
Uncaught SyntaxError: The requested module './library.js' does not provide an export named 'util'
When
These errors occur because the JavaScript module isn't written as a standards-compliant JavaScript module.
To solve this, various content delivery networks provide automatic conversion to standard ESM (ECMAScript Modules). We recommend esm.run:
<mpy-config>
[js_modules.main]
"https://esm.run/d3" = "d3"
</mpy-config>
<script type="mpy">
from pyscript.js_modules import d3
</script>
Alternatively, ensure referenced JavaScript code uses export or
request an .mjs version. The
user guide covers all
options and technical considerations for using JavaScript modules.
Why
Although the JavaScript module standard has existed since 2015, many libraries still produce files incompatible with modern standards.
This reflects the JavaScript ecosystem's evolution rather than a technical limitation. Developers are learning the new standard and migrating legacy code from obsolete patterns.
While legacy code exists, JavaScript may require special handling.
Possible deadlock
This error message indicates a serious problem:
When
This error occurs when code on a worker and the main thread are in deadlock. Neither fragment can proceed without waiting for the other.
Why
Consider this worker code:
from pyscript import sync
sync.worker_task = lambda: print('🔥 this is fine 🔥')
# Deadlock occurs here. 💀🔒
sync.main_task()
And this main thread code:
<script type="mpy">
from pyscript import PyWorker
async def main_task():
# Deadlock occurs here. 💀🔒
await pw.sync.worker_task()
pw = PyWorker("./worker.py", {"type": "pyodide"})
pw.sync.main_task = main_task
</script>
The main thread calls main_task(), which awaits worker_task() on the
worker. But worker_task() can only execute after main_task()
completes. Neither can proceed - classic deadlock.
PyScript detects this situation and raises the error to prevent your application from freezing.
Solution
Restructure your code to avoid circular dependencies between main thread and worker. One thread should complete its work before the other begins, or they should work independently without waiting for each other.
TypeError: crypto.randomUUID is not a function
This error appears in specific browser environments:
When
This occurs when using PyScript in environments where the
crypto.randomUUID API isn't available. This typically happens in:
Older browsers not supporting this API.
Non-secure contexts (HTTP instead of HTTPS). The crypto.randomUUID
function requires a secure context.
Certain embedded browser environments or WebViews with restricted APIs.
Solution
Use HTTPS for your application. The crypto API requires secure
contexts.
Update to modern browsers supporting the full Web Crypto API.
If working in a restricted environment, you may need to polyfill
crypto.randomUUID or use an alternative approach for generating unique
identifiers.
Helpful hints
This section provides guidance on common scenarios and best practices.
PyScript latest
When including PyScript in your HTML, you can reference specific
versions or use latest:
<!-- Specific version (recommended for production). -->
<script type="module" src="https://pyscript.net/releases/2026.1.1/core.js"></script>
<!-- Latest version (useful for development). -->
<script type="module" src="https://pyscript.net/latest/core.js"></script>
Production vs development
For production applications, always use specific version numbers. This ensures your application continues working even when new PyScript versions are released. Updates happen on your schedule, not automatically.
For development and experimentation, latest provides convenient access
to new features without updating version numbers.
Version compatibility
When reporting bugs or asking questions, always mention which PyScript version you're using. Different versions may behave differently, and version information helps diagnose problems.
Check the releases page to see available versions and their release notes.
Workers via JavaScript
You can create workers programmatically from JavaScript:
<script type="module">
import { PyWorker } from "https://pyscript.net/releases/2026.1.1/core.js";
const worker = new PyWorker("./worker.py", {type: "pyodide"});
// Call Python functions from JavaScript.
await worker.sync.python_function();
</script>
This approach is useful when:
You're building primarily JavaScript applications that need Python functionality.
You want dynamic worker creation based on runtime conditions.
You're integrating PyScript into existing JavaScript frameworks.
The worker runs Python code in a separate thread, keeping your main
thread responsive. Use worker.sync to call Python functions from
JavaScript, and vice versa through pyscript.window.
JavaScript Class.new()
When creating JavaScript class instances from Python, use the .new()
method:
from pyscript import window
# Create a new Date instance.
date = window.Date.new()
# Create other class instances.
map_instance = window.Map.new()
set_instance = window.Set.new()
This pattern exists because Python's Date() would attempt to call the
JavaScript class as a function rather than constructing an instance.
The .new() method explicitly invokes JavaScript's new operator,
ensuring proper class instantiation.
PyScript events
PyScript dispatches custom events throughout the application lifecycle. You can listen for these events to coordinate behaviour:
<script type="module">
document.addEventListener('py:ready', (event) => {
console.log('Python interpreter ready');
console.log('Script:', event.detail.script);
});
document.addEventListener('py:done', (event) => {
console.log('All scripts finished');
});
document.addEventListener('mpy:ready', (event) => {
console.log('MicroPython interpreter ready');
});
</script>
Available events
py:ready - Pyodide interpreter is ready and about to run code.
mpy:ready - MicroPython interpreter is ready and about to run
code.
py:done - All Pyodide scripts have finished executing.
mpy:done - All MicroPython scripts have finished executing.
py:all-done - All PyScript activity has completed.
Event details
Events carry useful information in their detail property:
document.addEventListener('py:ready', (event) => {
// Access the script element.
const script = event.detail.script;
// Access the interpreter wrapper.
const wrap = event.detail.wrap;
});
Use these events to:
Show loading indicators whilst Python initialises.
Coordinate between JavaScript and Python code.
Enable UI elements only after Python is ready.
Track application lifecycle for debugging or analytics.
Packaging pointers
Understanding packaging helps you use external Python libraries effectively.
Pyodide packages
Pyodide includes many pre-built packages. Check the package list to see what's available.
Install pure Python packages from PyPI using micropip:
Some packages with C extensions are available. If micropip reports it
cannot find a pure Python wheel, the package either:
Contains C extensions not compiled for WebAssembly.
Isn't compatible with the browser environment.
Has dependencies that aren't available.
MicroPython packages
MicroPython uses packages from micropython-lib. Reference them in configuration:
For packages not in micropython-lib, use the files configuration to
include them:
Or reference local files:
Package size considerations
Pyodide packages can be large. The numpy package alone is several
megabytes. Consider:
Using MicroPython for applications where package access isn't critical.
Loading only necessary packages.
Showing loading indicators whilst packages download.
Caching packages for offline use in production applications.
Filesystem
PyScript provides virtual filesystems through Emscripten. Understanding how they work helps you manage files effectively.
Virtual filesystem basics
Both Pyodide and MicroPython run in sandboxed environments with virtual filesystems. These aren't the user's actual filesystem - they're in-memory or browser-storage-backed filesystems provided by Emscripten.
Files you create or modify exist only in this virtual environment. They persist during the session but may not survive page reloads unless explicitly saved to browser storage.
Loading files
Use the files configuration to make files available:
PyScript downloads these files and places them in the virtual filesystem. Your Python code can then open them normally:
It's also possible to manually upload files onto the virtual file system
from the browser (<input type="file">), using the DOM API.
The following fragment is just one way to achieve this:
# Assume an input element of type "file" with an id of "upload" in
# the DOM.
# E.g. <input type="file" id="upload">
from pyscript import when, fetch, window
@when("change", "#upload")
async def on_change(event):
"""
Activated when the user has selected a file to upload via
the file input element.
"""
# For each file the user has selected to upload...
for file in input.files:
# Create a temporary URL.
tmp = window.URL.createObjectURL(file)
# Fetch and save its content somewhere.
with open(f"./{file.name}", "wb") as dest:
dest.write(await fetch(tmp).bytearray())
# Then revoke the tmp URL.
window.URL.revokeObjectURL(tmp)
Writing files
You can create and write files in the virtual filesystem:
These files exist in memory. To provide them for download, use the browser's download mechanism:
from pyscript import window, ffi
def download_file(filename, content):
"""
Trigger browser download of file content.
"""
# Ensure you use the correct mime-type!
blob = window.Blob.new([content], ffi.to_js({"type": "text/plain"}))
# Create a temporary download link/URL.
url = window.URL.createObjectURL(blob)
link = window.document.createElement("a")
link.href = url
link.download = filename
# Activate the link (pretend to click it).
link.click()
# Then revoke the temporary URL.
window.URL.revokeObjectURL(url)
# Use it.
download_file("output.txt", "File contents here")
Browser storage
For persistent storage across sessions, use browser storage APIs:
from pyscript import window
# Save data to localStorage.
window.localStorage.setItem("key", "value")
# Retrieve data.
value = window.localStorage.getItem("key")
Or use the File System Access API for actual file access (requires user permission):
from pyscript import window
# Request file picker (modern browsers only).
file_handle = await window.showSaveFilePicker()
writable = await file_handle.createWritable()
await writable.write("content")
await writable.close()
create_proxy
The create_proxy function manages Python-JavaScript reference
lifecycles.
When to use create_proxy
In Pyodide on the main thread, wrap Python functions passed as JavaScript callbacks:
from pyscript import ffi, window
def callback(event):
"""
Handle events.
"""
print(event.type)
# Create proxy before passing to JavaScript.
window.addEventListener("click", ffi.create_proxy(callback))
When create_proxy isn't needed
In workers, PyScript automatically manages references. You don't need
create_proxy:
from pyscript import window
def callback(event):
"""
Handle events in worker.
"""
print(event.type)
# No create_proxy needed in workers.
window.addEventListener("click", callback)
With experimental_create_proxy = "auto" in configuration, PyScript
automatically wraps functions:
from pyscript import window
def callback(event):
"""
Handle events with auto proxying.
"""
print(event.type)
# No create_proxy needed with auto mode.
window.addEventListener("click", callback)
In MicroPython
MicroPython creates proxies automatically. The create_proxy function
exists for code portability between Pyodide and MicroPython, but it's
just a pass-through in MicroPython:
from pyscript import ffi, window
def callback(event):
"""
Handle events.
"""
print(event.type)
# Works with or without create_proxy in MicroPython.
window.addEventListener("click", callback)
window.addEventListener("click", ffi.create_proxy(callback))
Both versions work identically in MicroPython.
Manual proxy destruction
If manually managing proxies in Pyodide, destroy them when done:
from pyscript import ffi, window
def callback(event):
"""
One-time handler.
"""
print(event.type)
proxy = ffi.create_proxy(callback)
window.addEventListener("click", proxy, ffi.to_js({"once": True}))
# After the event fires once, destroy the proxy.
# (In practice, the "once" option auto-removes it, but this shows the
# pattern for cases where you manage lifecycle manually.)
proxy.destroy()
Manual destruction prevents memory leaks when callbacks are no longer needed.
to_js
The to_js function converts Python objects to JavaScript equivalents.
Python dicts to JavaScript objects
Python dictionaries convert to JavaScript object literals, not Maps:
from pyscript import ffi, window
config = {"async": False, "cache": True}
# Converts to JavaScript object literal.
js_config = ffi.to_js(config)
# Pass to JavaScript APIs expecting objects.
window.someAPI(js_config)
This differs from Pyodide's default behaviour (which creates Maps).
PyScript's to_js always creates object literals for better JavaScript
compatibility.
When to use to_js
Use to_js when passing Python data structures to JavaScript APIs:
from pyscript import ffi, window
# Passing configuration objects.
options = {"method": "POST", "headers": {"Content-Type": "application/json"}}
window.fetch("/api", ffi.to_js(options))
# Passing arrays.
numbers = [1, 2, 3, 4, 5]
window.console.log(ffi.to_js(numbers))
# Passing nested structures.
data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
window.processData(ffi.to_js(data))
Important caveat
Warning
Objects created by to_js are detached from the original Python
object. Changes to the JavaScript object don't affect the Python
object:
This detachment is usually desirable - you're passing data to JavaScript, not sharing mutable state. But be aware of this behaviour.
MicroPython differences
MicroPython's to_js already creates object literals by default. You
may not need to_js in MicroPython unless ensuring cross-interpreter
compatibility:
from pyscript import window
# Works in MicroPython without to_js.
config = {"async": False}
window.someAPI(config)
# But using to_js ensures Pyodide compatibility.
from pyscript import ffi
window.someAPI(ffi.to_js(config))
For code that might run with either interpreter, use to_js
consistently.