How Python reaches the VM

There is no second interpreter and no transpilation to JavaScript source. The vendored RustPython parser produces an AST; Zipp's own compiler (symtable.rs for CPython scoping rules, emitter.rs for code generation) lowers it to the same FuncProto bytecode the JavaScript compiler produces. Python's object model lives in a fixed JavaScript-shaped runtime that the VM compiles once per program, and guest Python never sees the host's JavaScript globals.

Every Python code object has a fixed ABI: register 0 holds the function object and register 1 the array of bound positional values that the runtime's bind produced. Locals are registers, captured locals are cells, and module globals are a Map. A Python call is a direct VM Call, so recursion lives on the VM's explicit frame stack and ends in a catchable RecursionError.

Run Python natively
zipp py main.py                       # one file
zipp py ./project                     # a folder whose entry is main.py
zipp py lab/train.py --steps 20       # a script inside a folder, with arguments
zipp run --lang=python -              # read standard input

What is supported

The frontend is validated differentially: the programs in tests/python_corpus run under CPython and under Zipp, and standard output must match byte for byte. The recorded CPython outputs are committed, so continuous integration needs no CPython.

  • Arbitrary-precision int, float, bool, the full operator set, and the full str method set with %, format and f-strings.
  • list, tuple, dict, set, frozenset, range, slices and comprehensions.
  • Every function signature form, closures with nonlocal and global, decorators, and generators with send, throw, close and yield from.
  • Classes with C3 multiple inheritance, super(), properties, descriptors, __slots__, metaclasses, __init_subclass__ and __set_name__.
  • The builtin exception hierarchy with raise ... from, chained __cause__ and __context__, and CPython-style tracebacks.
  • Modules and packages by folder with CPython-like import cycles, full match pattern support, and dataclasses, enum, typing, itertools, functools, collections, json, re, struct, hashlib, pathlib, argparse and more from the standard library.

A project folder is loaded into a virtual filesystem (8 MiB per file, 64 MiB total) that open(), os.path and pathlib see. Files the program writes are reported back to the host; the CLI copies them to disk while the browser only reports them.

Performance, honestly

Python on Zipp is currently an interpreter over a JavaScript-shaped VM with the JIT disabled, and integers are BigInts. On a release build fib(25) takes about 0.09 s, a million-iteration total += i % 7 loop about 0.5 s and 200,000 dict insertions with string keys about 0.7 s; CPython does the same in 0.005 s, 0.05 s and 0.03 s. Dedicated Python bytecodes are the documented next step. There are no published Python benchmark tables, and this page will not invent any.

Python in the browser

The all WebAssembly build variant includes the Python frontend: 7,773,932 bytes raw, 1,777,382 bytes after Brotli, about 43% more than the JavaScript-only module. A browser host calls engine.initSource(source, "python") for one file or engine.initPythonProject(files, entry, argv) for a folder, then drains output and host requests.

In a Web Worker
import init, { Engine } from './zipp_wasm.js'
await init()
const engine = new Engine()
engine.initSource('print(sum(range(10)))', 'python')
console.log(engine.takeOutput())   // 45
engine.dispose()

The playground runs exactly this: load a folder of .py files from disk into the browser's virtual filesystem, pick an entry, run, and watch a canvas. Files never leave the browser.

Diagram of Python source flowing through the Zipp WebAssembly engine to a browser GPU backend
Python source → Zipp WASM → host requests → WebGPU or WebGL2.

GPU compute from Python

The bundled zipp_gpu module records float32 compute graphs (add, sub, mul, ReLU, transpose, matmul, sum and a toroidal Game of Life step) and submits them to the host. The graph leaves the engine as a gpu.execute request; the host validates it and runs it on WebGPU, WebGL2, compiled WASM kernels or a JavaScript reference backend, then delivers the named outputs to a Python callback. Nothing inside the engine touches a GPU.

A Game of Life grid rendered from Python running on Zipp WebAssembly
Conway's Life driven by Python on Zipp WASM, stepped on a WebGL2 backend. Recorded on an RTX 5090; it demonstrates behaviour, not GPU speed.

The GPU page covers the graph protocol, backends, limits and the experimental torch.compile path.

The Torch subset

Zipp bundles an experimental Python implementation of a Torch API subset. It is not the native PyTorch package, TorchInductor, CUDA or a pip environment. Eager CPU support is broad: tensors over typed-array storage, creation ops, indexing, shape ops, broadcasting elementwise and reduction ops, matmul and einsum, reverse-mode autograd, nn modules including Linear, Embedding, Conv1d, Conv2d, GRUCell, LSTMCell and LayerNorm, common losses, optim.SGD, Adam, AdamW and RMSprop, and torch.save / torch.load in PyTorch's zip checkpoint format.

The training example reproduced native PyTorch's ten rounded losses (0.085254 down to 0.010812), and the Life rules matched an independent PyTorch implementation across 1×1, 2×2, 7×7 and 96×96 grids. Those are correctness checks, not performance claims, and device="cuda" is rejected rather than silently ignored.

Calling JavaScript from Python

With the opt-in python-js-interop feature, import javascript; javascript.eval(source) runs JavaScript in the same VM instance, never in the browser's engine. Data is copied across, not proxied: numbers, strings, booleans, BigInts, lists and plain dicts convert, with None mapping to null. Cycles, accessors, functions, symbols and non-plain objects are rejected, conversion is capped at 10,000 values and depth 32, and source at 32,768 UTF-16 code units.