Running Python in the Browser with Web Assembly

I’ve been wanting to expand Qvault’s curriculum, and one of the most requested programming languages has been Python. Because my courses allow students to write and execute code right in the web browser, I decided to look into existing projects that allow a Python interpreter to run in the browser using Web Assembly. I settled on a tool called Pyodide, which does just that.

To see it in action, check out the finished product, a Python playground.

Sorry to interrupt! I just wanted to mention that you should check out my new free Python course, “Big-O Data Structures”. You’ll learn all you need to know to crush your upcoming coding interviews and whiteboard sessions.

What is Pyodide?

Pyodide is an open-source project that comprises a Python interpreter that has been compiled to Web Assembly.

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

webassembly.org

In other words, normally only JavaScript can run in a browser, but if you can compile your source code to Wasm, then you can run any programming language in the browser. (At the time of writing we run Python, Rust, and Go this way on our playground and in our courses)

Pyodide brings the Python 3.8 runtime to the browser via WebAssembly, along with the Python scientific stack including NumPy, Pandas, Matplotlib, parts of SciPy, and NetworkX. The packages directory lists over 35 packages which are currently available.

Github Project

How did I do it?

My Python execution plan is quite similar to the way I run Go code in the browser. There are basically three steps:

  • Write a worker file that defines how code is executed
  • Write a worker helper that abstracts the details of spinning up, communicating, and terminating workers
  • Implement the helper in the view so that users can execute code and see the code’s output

If you want to know how that all works please read this article about Web Workers and WASM in Go before continuing.

If you have finished that first article on Web Workers, then all you will need to understand the difference between our Python and Go logic is the worker file itself:

// pull down pyodide from the public CDN importScripts('https://pyodide-cdn2.iodide.io/v0.15.0/full/pyodide.js'); addEventListener('message', async (e) => { // wait for the interpreter to be fully loaded await languagePluginLoader; self.runPythonWithStdout = () => { try { // execute the code passed to the worker pyodide.runPython(e.data); } catch (err){ postMessage({ error: err }); return; } // capture the code's standard output // and send it back to the main thread let stdout = pyodide.runPython("sys.stdout.getvalue()") if (stdout) { stdout = stdout.split('\n') for (line of stdout){ postMessage({ message: line }); } } } // redirect stdout to io.StringIO so that we can get it later pyodide.runPython(` import io, code, sys from js import runPythonWithStdout sys.stdout = io.StringIO() sys.stderr = io.StringIO() ## This runs self.runPythonWithStdout defined in the JS runPythonWithStdout() `) postMessage({ done: true }); }, false);
Code language: JavaScript (javascript)

As you can see, the only particularly challenging part for our use case was adding the glue to properly capture the code’s standard output.

Have questions or feedback?

Follow and hit me up on Twitter @q_vault if you have any questions or comments. If I’ve made a mistake in the article be sure to let me know so I can get it corrected!

Leave a Comment