Rapid Python GUI development with HTML5 and Pyodide

A new version of this document is available here.

Rapid Python GUI development with HTML5 and Pyodide

Even scientists have to write a graphical user interface from time to time. Be it an interactive visualization of a complex data set, an application for students to play around with to learn a subject, or a showcase of some really cool stuff for the public. Using the Python programming language (as many scientists do these days), you could use one of the many available GUI frameworks, but all of them require you to learn a lot of obscure concepts that are completely useless for any other task.

If writing a GUI would only be as easy as creating a web page, where you can write simple user interfaces in a couple of lines in HTML. And even make them pretty using CSS. Now, a static web page is not an interactive user interface, of course, so we need to put an actual programming language into the mix. If one could only use Python in the web browser and use all the awesome libraries like NumPy, SciPy, or matplotlib ... Well, turns out you can, thanks to the Pyodide project that provides a Python implementation able to run inside all modern web browsers without having to install any external plugins. And you can even use the parts of NumPy and SciPy that were not even written in Python thanks to the WebAssembly standard that all modern browsers support these days.

The Ideal gas simulator has been built using these tools. Its main feature are:

  • Uses a SymPy expression provided by the user to define the external potential (you can even use arbitrary SymPy functions by prefixing them with "sp.", e.g., "sp.cos(x)")
  • Calculates the probability distribution corresponding to the external potential at the given temperature
  • Uses Matplotlib to display the probability distributions for the position and the velocity
  • Uses NumPy to generate random numbers drawn from said distribution as the initial positions of the particles
  • Numerically solves the equations of motions for 100 particles and displays the dynamics in real time
  • Changes to the temperature, mass, or the external potential by the user lead to an automatic recalculation of the probability distributions and the dynamics

The actual HTML user interface is extremely slim, containing less than 100 lines, including CSS statements and Pyodide intialization code.

Running Python code in the browser

While the above features are rather straightforward to implement for an application without an interactive GUI, writing such an interactive GUI using Pyodide adds some additional aspects that are worth discussing. First, since you cannot use Python directly in the browser (at least not yet), one has to insert a few lines of JavaScript code into the HTML file. This code is basically always the same, and a slightly simplified version of the code in the Ideal gas simulator reads

    <script>
      window.languagePluginUrl = 'pyodide/';
    </script>
    <script src="pyodide/pyodide.js"></script>
    <script>
      var xhr = new XMLHttpRequest();
      xhr.open('GET', 'idealgas.py', true);
      xhr.onload = function () {
      languagePluginLoader.then(() => {
       pyodide.loadPackage(['numpy', 'matplotlib', 'sympy']).then(() => {
          pyodide.runPython(xhr.responseText) })
      })
      }
      xhr.send(null);
    </script>

The first script element sets the languagePluginUrl variable such that Pyodide uses your local copy (located in the pyodide subdirectory) and does not download anything from an external server. The second script element loads the Pyodide Python interpreter, after which you can run Python code directly using the pyodide.runPython() function in JavaScript. This means that the Python code you are writing actually gets translated to JavaScript code that can be executed by the browser. The third and last script element does two things. First, it downloads the actual Python script (notice the .py ending) and calls pyodide.runPython() with the contents of the Python script as its argument. And second, it tells to Pyodide which external packages our Python script is going to need (NumPy, Matplotlib, and Sympy in our case).

 Interacting with the browser

Running Python code in the browser is only half of the story. The other half is the interaction between the Python code and the browser environment, such as updating parts of the user interface from Python code or reacting to user inputs. For this, Pyodide provides the js module, which you can use like a normal Python module. For instance, suppose we have defined a paragraph in our HTML file according to

    <p id="status">Some text</p> 

We can access the Python object corresponding to this paragraph using

    from js import document
    pStatus = document.getElementById("status")

Remarkably, getElementById is not a Python function, but one provided by the JavaScript implementation of your browser. However, you can call it like an ordinary Python function because Pyodide will translate between Python objects and JavaScript objects, and vice versa. In general, this works without you even noticing because the two languages are quite similar, but you can have a look at the section on type conversion in the Pyodide documentation if you want to know further details.

The text inside the paragraph element can be using the innerHTML property of the corresponding Python object. For instance,

    x = pStatus.innerHTML
    pStatus.innerHTML = 'Some other text'

stores the original text in the variable x and then updates the paragraph with a new text.

While this allows us to change the user interface as we see fit from our Python script, we still need to discuss how to react upon user inputs. Suppose we have an input field defined in the HTML file by:

    <input id="temperature" value="1.0">

Then, we can a register a Python function that should be called whenever the input field is changed by calling the addEventListener method:

    def MyPythonFunction(params):
        ...
    iTemperature = document.getElementById("temperature")
    iTemperature.addEventListener("change", MyPythonFunction)

Note that the Python function definition needs to have a single parameter even if you are not going to use it.

If you want to call a Python function at regular intervals, you can create a timer using the Window.setInterval() method:

    from js import window
    window.setInterval(MyOtherPythonFunction, 40)

This will lead to the Python function MyOtherPythonFunction being called every 40 milliseconds.

Debug output

If you want to write some status messages for debugging purposes, you can simply use Python's print function and watch the JavaScript console (which can be opened by pressing Ctrl-Shift-J in most browsers).

Working with images

Since we are using Matplotlib to plot data, we also need a way to tell the browser to show the generated plots at the right place. The easiest way is to first define an empty image element in the HTML file via

    <img id="MyPlot">

Then, we can use plt.savefig() to write the image to a string object, which we then specify as the image source using the Data URL encoding method:

    import io
    import base64
    iMyPlot = document.getElementById("MyPlot")
    img =  io.StringIO()
    plt.savefig(img, format="svg")
    img.seek(0)
    imgdata = img.read()
    iMyPlot.setAttribute("src","data:image/svg+xml;base64,"
                         + str(base64.b64encode(imgdata.encode()), "ascii"))

Here, we have defined a StringIO object, which can be used like a regular file. Since the SVG file needs to be supplied in Base64 encoding, we also have to import the corresponding Python module to do that.

Caveats

While this form of GUI development has many advantages, there are also some points that one needs to be aware of:

  • Not all Python modules can be used. Anything that is written in pure Python should work, modules that have parts written in other languages (such as C) first need to be packaged for use in Pyodide. This also applies to some modules in the Python standard library (e.g., urllib), because they rely on low-level functions provided by the operating system that are not available to the JavaScript implementation in the browser. Since Pyodide is relatively young, one can expect advantages at that front, but you should first check whether your favorite module is supported.
  • Pyodide is resource-hungry. Since each Pyodide application needs to come bundled with a Python interpreter, even a simple "Hello, world!" example is going to be about 10 MB in size. This is fine if you are just downloading from your local server, but distributing your application over a slow internet link might take some time. Additionally, the CPU load is going to be much worse than when using a native GUI framework. On recent computers, that might be fine, but using an old notebook with little RAM can make your application slow to respond. One also has to keep in mind that Matplotlib is not really meant for real-time plotting, which is why the Ideal gas simulator is writing directly to a canvas object to visualize the particles moving around.
  • As mentioned, Pyodide is relatively new, first published in 2019. Things might break in future releases.

 Downloads

You can download the Ideal gas simulator in two versions: one small tarball (17 kB) containing just the HTML interface and the Python implementation, and one big tarball (31 MB) that also contains the necessary Pyodide files. Both versions also include a simple webserver written in Python, which you can access on port 8080. Do not allow untrusted users to access this server,  as it is not secured against directory traversal attacks. The code of the Ideal gas simulator is licensed under version 3 of the GNU GPL, while the Pyodide files are available under the MPL 2.0 (see the file LICENSE in the pyodide directory for instructions how to obtain the source code).

Questions?

Do not hesitate to contact Priv.-Doz. Dr. Hendrik Weimer.