The Python community has been discussing the best way to make Python a first-class citizen in the modern web browser for a long time. The biggest challenge is the fact that web browsers really only support one programming language: JavaScript. However, as web technologies have advanced, we have pushed more and more applications to the web like games, scientific visualizations, and audio and video editing software. This means that we have brought heavy computations to the web -- something that JavaScript wasn't designed for. All these challenges raised the need for a low-level language for the web that can offer fast, portable, compact, and secure execution. As result, major browser vendors worked on this idea and introduced WebAssembly to the world, back in 2017.
In this tutorial, we'll look at how WebAssembly can help you run Python code in the browser.
To be clear JavaScript is a powerful programming language in itself. It's just not meant for certain things. For more on this, check out From ASM.JS to WebAssembly by Brendan Eich, the creator of JavaScript.
Contents
What We're Building
Let's say you want to teach a Python course. To make your course more interesting and fun, after each lesson you want to include an exercise for your students, so they can practice what they've learned.
The issue here is that the students need to prepare a development environment by installing a specific version of Python, creating and activating a virtual environment, and installing all the necessary packages. This can consume a lot of time and effort. It's also difficult to provide exact instructions on this since every machine is different.
While you could create a back-end to run the submitted code in a Docker container or perhaps an AWS Lambda function, you opt to keep the stack simple and add a Python editor in the course content that can run Python code on the client-side, in the web browser, and show the result to the users. This is exactly what you'll be building in this tutorial:
Check out the live demo here. You can also see a React version of it at wasmeditor.com.
WebAssembly
Based on the definition from the Mozilla Developer Network (MDN) Docs, WebAssembly (WASM) is:
A new type of code that can be run in modern web browsers and provides new features and major gains in performance. It is not primarily intended to be written by hand, rather it is designed to be an effective compilation target for source languages like C, C++, Rust, etc.
So WASM let's us run code written in different languages (not only JavaScript) in the browser with the following benefits:
- It's fast, efficient, and portable.
- It's secure since the code is run in a safe sandbox execution environment.
- It can be run on the client-side.
So, in our example above, we don't need to worry if the users run the code on our server and we don't need to be worried if thousands of students try the practice code since the code execution happens on the client-side, in the web browser.
WebAssembly wasn't designed to kill JavaScript. It's complementary to JavaScript. It can be used when JavaScript isn't the right tool, like for games, image recognition, and image/video editing, to name a few.
See Use Cases from WebAssembly.org for more on when you may want to leverage WebAssembly.
Pyodide
This tutorial uses the Pyodide library to run Python code, which compiles the CPython interpreter to WebAssembly and runs the binary in the browser's JavaScript environment. It comes with a number of pre-installed Python packages. You can also use Micropip to use even more packages that don't come by default.
Hello World
Create a new HTML file with the following code:
<head>
<script src="https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"></script>
<script>
async function main() {
let pyodide = await loadPyodide({
indexURL : "https://cdn.jsdelivr.net/pyodide/v0.20.0/full/"
});
console.log(pyodide.runPython("print('Hello, world from the browser!')"));
};
main();
</script>
</head>
Open the file in your browser. Then, within the console in your browser's developer tools, you should do something like:
Loading distutils
Loading distutils from https://cdn.jsdelivr.net/pyodide/v0.20.0/full/distutils.js
Loaded distutils
Python initialization complete
Hello, world from the browser!
As you can see, the last line is the result of the Python code execution in the browser.
Let's take a quick look at the code above:
- First, you can download and install Pyodide using either a CDN or directly from GitHub releases.
- loadPyodide loads and initializes the Pyodide wasm module.
- pyodide.runPython takes Python code as a string and returns the result of the code.
Pyodide Advantages
In the previous example, you saw how easy it is to install Pyodide and start using it. You just need to import pyodide.js from the CDN and initialize it via loadPyodide
. After that, you can use pyodide.runPython("Your Python Code Here")
to run your Python code in the browser.
When you first download Pyodide, the download size is big since you're downloading the full CPython interpreter, but your browser will cache it and you don't need to download it again.
There's also a large, active community of folks working on Pyodide:
Pyodide Limitations
To load Pyodide the first time, it will take four or five seconds (depending on your connection) since you have to download ~10MB. Also, Pyodide code runs around 3x to 5x slower than native Python.
Other Options
In general, if you want to run Python in the browser, you have two approaches available:
- Use a transpiler to convert Python to JavaScript. Brython, Transcrypt, and Skulpt all use this approach.
- Convert the Python runtime for use in the browser. Pyodide and PyPy.js use this approach.
One main difference between option one and two is that the mentioned libraries in option one don't support Python packages. That said, their download size is much smaller than the libraries in option two and, consequently, they're faster.
We went with Pyodide for this tutorial because it has easier syntax and it supports Python packages. If you're interested in other options feel free to check their documentation.
Python Code Editor
In this section, we'll create a simple Python editor that can run code in the browser using:
Create a new project:
$ mkdir python_editor_wasm
$ cd python_editor_wasm
Create and activate a virtual environment:
$ python3.10 -m venv env
$ source env/bin/activate
(env)$
Install Flask:
(env)$ pip install Flask
In the root of the project create a file called app.py and add the following code:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
Create a "templates" folder at the root of our project, and under it add index.html file.
templates/index.html:
<!doctype html>
<html class="h-full bg-slate-900">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- install tailwindcss from cdn, don't do this for production application -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- install pyodide version 0.20.0 -->
<script src="https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"></script>
<!-- import codemirror stylings -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.min.css" />
<!-- install codemirror.js version /5.63.3 from cdn -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.js"
integrity="sha512-XMlgZzPyVXf1I/wbGnofk1Hfdx+zAWyZjh6c21yGo/k1zNC4Ve6xcQnTDTCHrjFGsOrVicJsBURLYktVEu/8vQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- install codemirror python language support -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/mode/python/python.min.js"
integrity="sha512-/mavDpedrvPG/0Grj2Ughxte/fsm42ZmZWWpHz1jCbzd5ECv8CB7PomGtw0NAnhHmE/lkDFkRMupjoohbKNA1Q=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- import codemirror dracula theme styles from cdn -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/theme/dracula.css"/>
<style>
/* set codemirror ide height to 100% of the textarea */
.CodeMirror {
height: 100%;
}
</style>
</head>
<body class="h-full overflow-hidden max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8">
<p class="text-slate-200 text-3xl my-4 font-extrabold mx-2 pt-8">Run Python in your browser</p>
<div class="h-3/4 flex flex-row">
<div class="grid w-2/3 border-dashed border-2 border-slate-500 mx-2">
<!-- our code editor, where codemirror renders it's editor -->
<textarea id="code" name="code" class="h-full"></textarea>
</div>
<div class="grid w-1/3 border-dashed border-2 border-slate-500 mx-2">
<!-- output section where we show the stdout of the python code execution -->
<textarea readonly class="p-8 text-slate-200 bg-slate-900" id="output" name="output"></textarea>
</div>
</div>
<!-- run button to pass the code to pyodide.runPython() -->
<button onclick="evaluatePython()" type="button" class="mx-2 my-4 h-12 px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm bg-green-700 hover:bg-green-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-700 text-slate-300">Run</button>
<!-- clean the output section -->
<button onclick="clearHistory()" type="button" class="mx-2 my-4 h-12 px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm bg-red-700 hover:bg-red-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-700 text-slate-300">Clear History</button>
<script src="/static/js/main.js"></script>
</body>
</html>
In the head of the index.html file, we imported Tailwind CSS for styling, Pyodide.js version 0.20.0
, and CodeMirror and its dependencies.
The UI has three important components:
- Editor: Where users can write Python code. It's a
textarea
HTML element with theid
ofcode
. When we initializedcodemirror
, we let it know that we want to use this element as a code editor. - Output: Where the output of the code will be displayed. It's a
textarea
element with theid
ofoutput
. When Pyodide executes Python code, it will output the result to this element. We displayed an error messages in this element as well. - Run button: When users click on this button, we grab the value of the editor element and pass it as a string to
pyodide.runPython
. Whenpyodide.runPython
returns the result we display it in the output element.
Now in the root of the project, create "static/js" folders. Then, under the "js" folder, create a new file called main.js.
static/js/main.js:
// find the output element
const output = document.getElementById("output");
// initialize codemirror and pass configuration to support Python and the dracula theme
const editor = CodeMirror.fromTextArea(
document.getElementById("code"), {
mode: {
name: "python",
version: 3,
singleLineStringErrors: false,
},
theme: "dracula",
lineNumbers: true,
indentUnit: 4,
matchBrackets: true,
}
);
// set the initial value of the editor
editor.setValue("print('Hello world')");
output.value = "Initializing...\n";
// add pyodide returned value to the output
function addToOutput(stdout) {
output.value += ">>> " + "\n" + stdout + "\n";
}
// clean the output section
function clearHistory() {
output.value = "";
}
// init pyodide and show sys.version when it's loaded successfully
async function main() {
let pyodide = await loadPyodide({
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.20.0/full/",
});
output.value = pyodide.runPython(`
import sys
sys.version
`);
output.value += "\n" + "Python Ready !" + "\n";
return pyodide;
}
// run the main function
let pyodideReadyPromise = main();
// pass the editor value to the pyodide.runPython function and show the result in the output section
async function evaluatePython() {
let pyodide = await pyodideReadyPromise;
try {
pyodide.runPython(`
import io
sys.stdout = io.StringIO()
`);
let result = pyodide.runPython(editor.getValue());
let stdout = pyodide.runPython("sys.stdout.getvalue()");
addToOutput(stdout);
} catch (err) {
addToOutput(err);
}
}
Here, we:
- Initialized CodeMirror with support for Python and the Dracula theme.
- Initialized Pyodide.
- Added a function called
evaluatePython
that executes when the user clicks on theRun
button. It passes the value of thecode
element topyodide.runPython
and displays the results in theoutput
element viaaddToOutput
. - Added a function called
clearHistory
that clears theoutput
element when the user clicks on theClear History
button.
To run the Flask development server locally, run:
(env)$ flask run
The server should now be running on port 5000. Navigate to http://127.0.0.1:5000 in your browser to test out the code editor.
Conclusion
In this tutorial, we barely touched the tip of the iceberg with both Pyodide and WebAssembly. We saw how we can use WebAssembly to run Python code in the browser, but WebAssembly, in general, covers broader use cases.
Our deployment platforms are more varied than ever and we simply cannot afford the time and money to rewrite software for multiple platforms constantly. WebAssembly can impact the worlds of client-side web development, server-side development, games, education, cloud computing, mobile platforms, IoT, serverless, and many more.
The goal of WebAssembly is to deliver software that is fast, safe, portable, and compact.
You can find the code repo here.