Realtime Updates in PyScript/Pyodide
Published May 24, 2023
When writing Python code to run in the Browser (whether in PyScript or Pyodide), one common desire is to print something out to the page as the program progresses. Maybe it's status messages from phases of execution, or warning messages, or informational updates. In their simplest form, they might look like:
button.html
|
|
- PyScript
- Pyodide
|
|
|
|
However, when running the code, it appears that only the final number 99
appears on the page, when we'd expect to see the number 0
to 99
appear, one after another.
The issue isn't that the textContent isn't being changed; the issue is that there's no opportunity for the screen to update to actually display the change. The solution is to use a coroutine.
Confirming that Changes Do Happen
To observe that the textContent
of our targeted <div>
is indeed changing, we can add a small Mutation Observer to the very top of the HTML page. A Mutation Observer is just what it sounds like - it watches for any mutations (changes) on a specified element, and runs some user-defined code in response. This particular mutatio will log the observed Element to the browser dev console whenever any change is made:
|
|
|
|
|
|
With this added code, the console fills with 0,
1,
..., 98
, 99
. So the textContent of our target Div is, in fact, changing with each call to print
/display
/textContent=
. So why can't we see that on the page?
Slowing Things Down to Human Speed
One might think that the code is simply proceeding too fast for you eyes to see the numbers change, but that's not exactly happening either. Let's slow things down by modifying the for
loop:
- PyScript
- Pyodide
|
|
|
|
Now the loop has to "do a little useless work" before it advances to the next number. (You may need to change 1_000_000
to a larger or smaller number, depending on your system's capabilities.) Opening the dev console again , the numbers still appear, just at a more measured pace. But the text on the page doesn't update until the Python code has finished. So what gives?
The Real Issue
The issue is that while updates to the DOM are synchronous (i.e. no further code will be executed until the DOM update is complete), updates to the screen are asynchronous. What's more, the entire call to runPython() is synchronous, so no updates to the screen will occur until the runPython terminates. Essentially, the call to runPython is a blocking call, and nothing else can happen on the page - screen updates and repainting, other JavaScript calls, etc - until runPython returns.
This blog post gives a good high-level explanation of the interaction between synchronous code and visible changes on screen.
The Solution
So, if the screen can't update until our synchronous code call terminates, what can we do? Make our code asynchronous! By turning our code into a coroutine which occasionally yields back to the browser's event loop to do some work (i.e. update the screen), we can see the updates visibly as they happen.
Pyodide has a nifty utility for this in the form of the runPythonAsync function, which allows you to write async code without resorting to wrapping your code into a coroutine. Here's a description of this feature and its purpose, which is demonstrated in the final sample code below.
PyScript requires the user to be slightly more explicit about creating and scheduling coroutines and awaitables, and top-level await
is not permitted. Instead, we'll write our code as a coroutine using async def
, and schedule it using asyncio.ensure_future
Here's an Overview of Asyncio in PyScript.
Finally, here's the required code. The "useless slowdown loop" is still present so that the results are visible, but there's no need for it to be there in production.
- PyScript
- Pyodide
|
|
|
|
Now, go forth, and let all your intermediate results be visible!