How to run transports in a browser with Pyodide¶
This guide shows you how to build the Pyodide wheel and load it in a browser.
Build the wheel¶
Build and test the Python 3.14 Pyodide wheel:
make test-pyodide
The wheel is written to dist/pyodide/.
Open the example¶
Serve the repository root:
python -m http.server 8000
Open the example with the wheel URL in its query string, replacing <wheel> with the filename in
dist/pyodide/:
http://127.0.0.1:8000/examples/pyodide.html?wheel=/dist/pyodide/<wheel>
The page loads Pyodide 314.0.4, installs the local wheel and its dependencies, then creates a Python server and client mirror. Use Toggle on server to stream a server patch and Rename from client to send a client proposal through the authoritative server.
Connect to a live server¶
Client.connect and Client.connect_sse detect Pyodide (sys.platform == "emscripten") and ride
the browser’s native WebSocket / EventSource through the js FFI, because the websockets and
httpx libraries need raw sockets and the browser does not give Python any. The API is unchanged:
import transports
client = transports.Client(codec="msgpack")
await client.connect("wss://example.com/ws") # the browser's WebSocket under Pyodide
Snapshots and patches update the mirror as they arrive; register client.on_change /
client.on_reject to react. Client.run works the same way. It reconnects with ?since= resume
whenever the socket drops, and both authority modes work (a client-authoritative mirror pushes its
state back over the browser socket after the server re-snapshots).
Run the browser test¶
Install the JavaScript test dependencies once, then run the focused Playwright test:
make develop-js
make test-pyodide-browser
The browser test uses the wheel already present in dist/pyodide/. Run make test-pyodide first
after changing Python or Rust code.
Use the Jupyter widget¶
transports.widget(server) builds an anywidget. Display it and every
hosted model mirrors live in the notebook frontend, updating on each transports.sync(server). The
frontend ships inside the wheel (transports/extension/cdn/widget.js) and loads its packaged WASM
client state. Custom frontends hook the bubbled transports-change / transports-reject DOM events
or use el.transports = {client, edit}. el.transports.edit(id, ["brightness"], 75) sends a
server-authoritative proposal, and a value the model rejects surfaces inline through the reject
frame.
import transports
session = transports.Session()
session.host(model)
server = transports.Server(session)
w = transports.widget(server) # pip install anywidget
w # display; then mutate models + transports.sync(server)
Run it all in JupyterLite¶
Both ends run in WebAssembly: the Pyodide kernel hosts the Session and the widget frontend
mirrors it, with no server and no sockets. A hosted build publishes with these docs at
/transports/lite/. Open lab/index.html, then
transports-demo.ipynb.
make jupyterlite # builds the site into dist/lite (wheel + demo notebook included)
make test-jupyterlite # or: drive the site's REPL in Chromium end-to-end
Serve dist/lite from any static host. The transports wheel installs from the site’s own wheel
index with %pip install transports, or from PyPI, which carries the Pyodide wheel since v0.7.0.
anywidget comes from PyPI. Widget frontend extensions cannot be installed with %pip at runtime,
so the site build bundles them (jupyterlab_widgets for the ipywidgets manager plus anywidget;
see the jupyterlite Make target). A Lite site built without them shows the widget’s text repr
instead of the live view.
If a previously visited site misbehaves after a redeploy (e.g. FileNotFoundError from an old
wheel, or RuntimeError: WebAssembly stack switching not supported): JupyterLite caches
aggressively. A service worker plus browser storage can keep serving the previous build’s kernel
and packages. Hard
refresh (Cmd/Ctrl+Shift+R), or clear the site’s data (service worker + IndexedDB) and reload.
Deploy the example¶
Host the wheel on the same origin as the page and pass its URL through ?wheel=. After a release
publishes a compatible Pyodide wheel to PyPI, omit the query parameter to install transports
directly from PyPI.