Inspired by this post:

Create Dev's offline page with (Tiny)GO and WebAssembly ๐ฆ๐กโจ
Sendil Kumar ใป Jul 6 '19 ใป 4 min read
I have decided to give it a try using Python.
This is the first time I am writing WebAssembly which I wanted to do for a long time. So for this, I am using Pyodide.
Let's get started
First download Pyodide, extract it and copy the pyodide.js
file in your current directory.
Now Let's write the server in server.py
.
from http.server import BaseHTTPRequestHandler, HTTPServer
class HTTPRequestHandler(BaseHTTPRequestHandler):
"""Request handler class"""
def do_GET(self):
"""Handler for GET requests."""
# Send response status code
self.send_response(200)
# Send headers
self.send_header("Cache-Control", "no-cache")
if self.path.endswith(".wasm"):
# Header to serve .wasm file
self.send_header("Content-Type", "application/wasm")
else:
self.send_header("Content-Type", "text/html")
self.end_headers()
# Serve the file contents
urlpath = self.path
if urlpath == "/":
urlpath = "/index.html"
with open(f".{urlpath}", "rb") as f:
content = f.read()
self.wfile.write(content)
def main():
print("starting server...")
# Server settings
server_address = ("localhost", 8080)
httpd = HTTPServer(server_address, HTTPRequestHandler)
print("running server...")
httpd.serve_forever()
if __name__ == "__main__":
main()
So we have pyodide.js
, server.py
.
Let's write the index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Python wasm</title>
</head>
<body>
<script src="pyodide.js"></script>
<div id="status">Initializing Python...</div>
<canvas id="draw-here"></canvas>
<style>
.color {
display: inline-block;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
margin: 10px;
}
</style>
<div id="colors" style="text-align:center"></div>
<script>
window.languagePluginUrl = "/pyodide.js";
languagePluginLoader.then(() => {
pyodide.runPython(` ! Python code goes here ! `)
})
</script>
</body>
</html>
Now let's write the Python code for canvas.
from js import document as doc
# Make the "Initializing Python" status disappear
doc.getElementById("status").innerHTML = ""
canvas = doc.getElementById("draw-here")
canvas.setAttribute("width", doc.body.clientWidth)
canvas.setAttribute("height", 300)
ctx = canvas.getContext("2d")
ctx.strokeStyle = "#F4908E"
ctx.lineJoin = "round"
ctx.lineWidth = 5
# Global variables
pen = False
lastPoint = (0, 0)
def onmousemove(e):
global lastPoint
if pen:
newPoint = (e.offsetX, e.offsetY)
ctx.beginPath()
ctx.moveTo(lastPoint[0], lastPoint[1])
ctx.lineTo(newPoint[0], newPoint[1])
ctx.closePath()
ctx.stroke()
lastPoint = newPoint
def onmousedown(e):
global pen, lastPoint
pen = True
lastPoint = (e.offsetX, e.offsetY)
def onmouseup(e):
global pen
pen = False
canvas.addEventListener("mousemove", onmousemove)
canvas.addEventListener("mousedown", onmousedown)
canvas.addEventListener("mouseup", onmouseup)
# Colors
div = doc.getElementById("colors")
colors = ["#F4908E", "#F2F097", "#88B0DC", "#F7B5D1", "#53C4AF", "#FDE38C"]
for c in colors:
node = doc.createElement("div")
node.setAttribute("class", "color")
node.setAttribute("id", c)
node.setAttribute("style", f"background-color: {c}")
def setColor(e):
ctx.strokeStyle = e.target.id
node.addEventListener("click", setColor)
div.appendChild(node)
Now we can fetch this code as text via AJAX and execute with pyodide.runPython(code)
.
But for simplicity, we will directly paste the code in index.html
.
So the final version looks like this:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Python wasm</title>
</head>
<body>
<script src="pyodide.js"></script>
<div id="status">Initializing Python...</div>
<canvas id="draw-here"></canvas>
<style>
.color {
display: inline-block;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
margin: 10px;
}
</style>
<div id="colors" style="text-align:center"></div>
<script>
window.languagePluginUrl = "/pyodide.js";
languagePluginLoader.then(() => {
pyodide.runPython(`
from js import document as doc
# Make the "Initializing Python" status disappear
doc.getElementById("status").innerHTML = ""
canvas = doc.getElementById("draw-here")
canvas.setAttribute("width", doc.body.clientWidth)
canvas.setAttribute("height", 300)
ctx = canvas.getContext("2d")
ctx.strokeStyle = "#F4908E"
ctx.lineJoin = "round"
ctx.lineWidth = 5
# Global variables
pen = False
lastPoint = (0, 0)
def onmousemove(e):
global lastPoint
if pen:
newPoint = (e.offsetX, e.offsetY)
ctx.beginPath()
ctx.moveTo(lastPoint[0], lastPoint[1])
ctx.lineTo(newPoint[0], newPoint[1])
ctx.closePath()
ctx.stroke()
lastPoint = newPoint
def onmousedown(e):
global pen, lastPoint
pen = True
lastPoint = (e.offsetX, e.offsetY)
def onmouseup(e):
global pen
pen = False
canvas.addEventListener("mousemove", onmousemove)
canvas.addEventListener("mousedown", onmousedown)
canvas.addEventListener("mouseup", onmouseup)
# Colors
div = doc.getElementById("colors")
colors = ["#F4908E", "#F2F097", "#88B0DC", "#F7B5D1", "#53C4AF", "#FDE38C"]
for c in colors:
node = doc.createElement("div")
node.setAttribute("class", "color")
node.setAttribute("id", c)
node.setAttribute("style", f"background-color: {c}")
def setColor(e):
ctx.strokeStyle = e.target.id
node.addEventListener("click", setColor)
div.appendChild(node)
`)
})
</script>
</body>
</html>
Now we only need to run the webserver with python server.py
and open http://localhost:8080
If you are having trouble, here is the full project.
Discussion (8)
This is a very cool demo of pyodide!
Won't this have to compile the python to webassembly on the client's browser, and then run that webassembly? I would have expected the python to be compiled to webassembly before the client loads it. It seems to me that this process will make the load time of the python script significantly worse than if was even just written in js.
Am I missing something, or is there a way of using pyodide that takes advantage of webassembly's load time and performance gains?
First of all, thanks asking this question. I realised I missed to address this topic in the post.
Yes... being dynamic and garbage collected python code is not easy to be compiled into web assembly, but not the Python interpreter which is written in C. Pyodide is kind of running a python interpreter (compiled into web assembly) in the browser which as you mentioned is performance heavy. So for now not suitable for web apps in production. However, efforts are going on and maybe in future there will be a way to bypass this limitation.
And this was the intention of this post. To learn and share with people where we currently are with python and web assembly.
However there are use cases where this performance loss due to the compilation time doesn't matter such as scientific research in browser (which is in pyodide's headline).
Ah I understand now. The python runtime is WebAssembly rather than the python script itself. Thank you much for your detailed and understanding response! I can now appreciate that this is really awesome!
Thank you for this demo! Do you know if there is any way to do file IO with this? Perhaps passing JS objects into the python environment, or somehow interacting with the virtual filesystem?
Specifically I want to load an Excel file into Pandas.
Pandas can load csv and Excel files from files, urls, strings and anything that behaves like a file (i.e. inherits from io.BaseIO). Maybe you can write an adapter between the is fileloader API and io.BaseIO
Sorry I haven't tried it and I'm not sure if I'll find the time to dig into this.
Anything possible :D