Computer:

Craft:

/!
This article is a work in progress

Pycript with Typst

You know how every major compilled language are getting wasm support? Well, if you can compile C to wasm, you kinda can compile the CPython interpreter to wasm, and if you can do that, you kinda can run Python in your browser. Which I think is cool.

And also, I use a lot of Python, so it would be cool be able to have some interactive.

2026-06-02

A drawing of a blue-ish round-ish platypus with big eyes, holding a laptop. This platypus is quite cute, but I might be biased.

A few years ago I already had the idea of running Python on the client side of my site, and even started using PyScript to do it. At the time I was generating my site with Hugo, and even if its great, at some point I got frustrated by the limit of its templating/scripting options and just stop working on my site. A PdD thesis later, Typst is a thing, version 0.13 introduced HTML export, and version 0.14 added enough eatures to make it usable. So now I have a new website, generated using Typst and a lot of Python-assisted-ducktape, with a powerfull scripting langage and my own HTML template. I might write an article about it at some point, but I was talking about Python.

Alternative Solutions

I have a few option to illustrate the result of Pythons script. The simpler solution is to just copy the logs of the code in a raw block, or display screenshots images:

```python
print("Hello World")
```
```
>>> Hello World
```
print("Hello World")
Hello World

Another option would be to use something like pyrunner, that run Python code when compiling the Typst document, using Typst wasm vm, and then intsert the output in the document.

Those two options have the benefit of being relativelly simple and not requiring anything from the web client, but there is no interactiveness.

A third option would be to run the code on the server like interactive notebooks, but I’m not having a static website to run a second service that run arbitrary code on my server.

So the option I choosed is the same I already started exploring a few years ago: running the Python interpreter on the client browser. No code execution on my machine, so less security risks or cost for me. The main drawback is that the client need to support wasm (this should not be a big issue nowday), and allow it (this is more problematic, noscript mode won’t let wasm program run, and I don’t really want my reader need to allow javascript & wasm to read my blog).

For now, I think I will use a mixed of the first and last options. The good thing I have nothing to do for the first option: just copy the output of the run. The last option, running Python in the cient, has a lot of potential and is the point of this article, so obviously I will also use it.

Pyodide and PyScript

There are a few Python interpreters that can be compiled to wasm and used in a browser. For example RustPython is Python interpreter implemented in Rust, which make is easy to compile to wasm (Rust rather easy to compile to wasm). Another option is Pyodide, a port of the actual CPython interpreter to wasm, with a tool called micropip that allow to install package. They even ported a lot of popular packages that use native C/C++/Rust extentions.

Although impressible and probably quite usable directly, I am not very confortable with front-end shenanigans and Pyodide require some setting up before just running Python code. Enter PyScript. PyScript is an additional layer over a Python interpreter (either Pyodide or MicroPython, a light-weight implementation targetting embeded devices) that handle the complexity of setting up the interpreter and provide an API to access the browser JavaScript API from Python. I am not exactly clear on where Pyodide ends and PyScript starts, and at some point in the future I may considere replacing PyScript with some custom Tyspt to generate the setup code at compile time, but for now, I use PyScript over Pyodide.

Standard PyScript Usage

PyScript is quite simple to use (cf the doc):

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/releases/2026.3.1/core.css" />
<script type="module" src="https://pyscript.net/releases/2026.3.1/core.js"></script>
</head>
<body>
<script type="py">
from pyscript import display
display("Hello World!")
</script>
</body>
</html>

You just need to load the module core.js in the <head> (and some css), then you can use Python in <script> tags like JavaScript (with the type="py" attribute).

One of the interesting feature is that it integrates Xterm.js to provide an CLI interface:

<script type="py" terminal worker>
import code
code.interact()
</script>
# /// script
# [tool.pyscript]
# repl = true
# ///
print("Hello World")

Here, the terminal attribute indicate to PyScript that the script must runs in a terminal, and the worker attribute indicate that the code must run in a separate worker. The worker is necessary because else the code would run in the main thread of the browther, in thing like input() that wait for interaction would make the whole page hang.

Not Depending on Other Sites

As you can see, PyScript is easy to use, if you are ok with using files hosted on another server. That’s not really my thing. I already had this issue the first time I tested it, but I was happy to find that since then they greatly simplified the local installation. The Running Offline section of the documentation cover this case. The main point is that PyScript now release an offline.zip file that contain the necessary files, and the ported package can be downloaded from the Pyodide release.

But that would be too easy… At the time of writting (for the version 2026.3.1 of PyScript), replacing core.css and core.js with locally served files with all the additionnal locally served file will not prevent PyScript from downloading its own interpreter from a remode CDN, a practice that I find impolite at best. There is an offline attribute that can be used in the tag that load core.js, but for some reason it will make PyScrypt try to find the interpreter relativelly to the current html page and not relativelly to the core.js file. The best solution I found is to tell PyScrypt to use the local interpreter through its configuration (for example https://jean-marie.mineau.eu/pyscript/2026.3.1/pyscript/pyodide/pyodide.mjs, see next section how to do that).

Configuring PySript