Handling Non-Blocking Python Tasks in the Browser with PyScript Async/Await
Learn how to use Pyodide's async/await support in PyScript to run Python coroutines that don't freeze the browser UI, including integration with JavaScript Promises.
02 Dec 2025, 17:23 UTC

The Problem: UI Freezing During Python Execution
When running Python in the browser via PyScript, long-running computations or network requests typically block the main JavaScript thread. Because the browser's UI rendering and the WebAssembly (WASM) execution share the same thread, a heavy Python loop or a synchronous sleep call will freeze the entire page, making it unresponsive to user input until the task completes.
The solution is to leverage Pyodide's native async/await support (available in version 0.23 and later). By defining Python functions as coroutines and calling them from asynchronous JavaScript contexts, you can perform background tasks that yield control back to the browser, keeping the UI fluid.
Prerequisites
- A modern web browser supporting WebAssembly.
- PyScript configured to use Pyodide version 0.23 or higher.
- Basic familiarity with JavaScript
asyncfunctions and Promises.
Implementing Asynchronous Python Functions
To prevent the UI from locking, you must define your Python logic using async def. This tells the Pyodide runtime that the function is a coroutine and can be paused to allow other browser events to process.
Step 1: Define the Async Python Logic
In your <py-script> block or linked .py file, create a function that utilizes await. For tasks that are CPU-intensive rather than I/O-bound, use await asyncio.sleep(0) to manually yield control back to the event loop.
import asyncio
from pyscript import window
async def fetch_and_process_data(item_id):
try:
# Simulate an asynchronous network request or heavy task
# asyncio.sleep(0) yields control to the JS event loop
await asyncio.sleep(1)
result = f"Processed data for item {item_id}"
return result
except Exception as e:
return f"Error: {str(e)}"
# Expose the function to the global JavaScript window object
window.fetchAndProcessData = fetch_and_process_data
Step 2: Call Python from JavaScript
When Pyodide exposes an async def function to JavaScript, it returns a native JavaScript Promise. You cannot call this function as a standard synchronous function; you must use await within an async JS function or chain it with .then().
async function handleButtonClick() {
const itemId = document.getElementById('item-input').value;
const statusElement = document.getElementById('status');
statusElement.innerText = "Processing...";
try {
// The Python coroutine is wrapped as a JS Promise
const result = await window.fetchAndProcessData(itemId);
statusElement.innerText = result;
} catch (error) {
console.error("Python Execution Error:", error);
statusElement.innerText = "An error occurred during execution.";
}
}
Diagnostic Decision: When to yield?
Deciding where to place await is critical for UI responsiveness. Use the following table to determine your approach:
| Scenario | Approach | Reasoning |
|---|---|---|
| Network API calls | await pyfetch(...) |
Native browser I/O is inherently asynchronous. |
| Large data loops | await asyncio.sleep(0) |
Forces the WASM thread to pause and let the browser repaint. |
| Simple calculations | Standard def |
The overhead of async is unnecessary for sub-16ms tasks. |
Verification and Limitations
How to verify the result
- Open the browser's Developer Tools (F12) and navigate to the Network tab. Verify that the Pyodide WASM binary loaded is version 0.23.x or newer.
- Trigger the async function. While the
await asyncio.sleep(1)is active, try to highlight text or click other buttons on the page. If the page remains interactive, the async implementation is working. - Check the Console tab. If the Python function fails, the error should be caught by the JavaScript
catchblock rather than appearing as an unhandled Pyodide exception.
Limitations
- Thread Constraints: Even with
async/await, the code still runs on the main browser thread. Extremely heavy CPU computations (e.g., massive matrix multiplication) will still freeze the UI unless broken into small chunks withsleep(0). - Package Installation: Using
micropipto install heavy packages at runtime is a synchronous operation. This will block the UI regardless of whether your subsequent functions are async.
Rollback and Recovery
If the async implementation causes unexpected state issues (such as race conditions where multiple JS calls modify the same Python global variable), revert the Python function to a standard def and move the logic into a Web Worker. This completely offloads the execution to a separate OS thread, removing the need for manual yielding.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.