Python on the Zipp VM
Zipp's Python frontend is its own implementation of Python 3, validated on 13 September 2026. Python source is parsed by Zipp's own parser and lowered by Zipp's compiler straight to register bytecode, so Python and JavaScript run on one virtual machine, natively or in the browser.
How Python reaches the VM
There is no second interpreter and no transpilation to JavaScript source. Zipp's own parser (zipp-pyparse) produces a syntax tree; 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.
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 inputWhat 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,complexwith the whole ofcmath, the full operator set, and the fullstrmethod set with%,formatand f-strings. list,tuple,dict,set,frozenset,range, slices and comprehensions.- Every function signature form, closures with
nonlocalandglobal, decorators, and generators withsend,throw,closeandyield 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's traceback layout, traceback objects and thetracebackmodule, with CPython's "Did you mean" suggestions. - Modules and packages by folder with CPython-like import cycles, full
matchpattern support, anddataclasses,enum,typing,itertools,functools,collections,json,re,struct,hashlib,pathlib,argparseand 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
On the command line Zipp compiles hot Python loops to x86-64 code. Over the 36 programs of tools/python_bench.py (geometric mean of work time against CPython 3.13) it runs at about 1.75× CPython's time with that JIT, from about 22× when the September speed work began; range_loop, int_arith, float_arith, tuple_swap and global_read are faster than CPython. Every benchmark checksum still equals CPython's.
What moved it: direct calls with fixed-arity entries and per-class inline caches; fused Python-only VM instructions for globals, attributes, subscripts, exceptions and generators; native json, string and sort helpers; a JIT tier for hot loops; integers within ±2^46 stored in the value word instead of as heap BigInts; dicts and sets in a native compact hash table modelled on CPython 3.13's; PyCall, a direct call instruction with per-site caches; and hidden-class layouts for instances, whose attribute reads and writes compiled loops inline. Every fused instruction and compiled loop falls back to the interpreter's own step, so the results are always the interpreter's.
Startup is cached too: the CLI keeps the compiled Python runtime and each imported library module in a per-user cache, so a warm hello-world takes about 20 ms (CPython: 15-20 ms) and import torch about 160 ms. The JIT tier is x86-64 and command-line only; WebAssembly, the sandbox and embedders with an instruction budget run the same code interpreted, and slower.
Python in the browser
Two WebAssembly variants include the Python frontend. python leaves torch out (1,701,112 bytes after Brotli) and adds it at run time from the 380,240-byte zipp_torch.wasm package through addPythonPackage, which checks the package's format, engine ABI hash and the SHA-256 of every file first; all has torch built in (2,056,613 bytes after Brotli). 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. prewarmPython() compiles the Python runtime ahead of use; the playground calls it when idle, so the first Python run takes about 20 ms instead of 220.
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.
GPU compute from Python
The bundled zipp_gpu module records float32 compute graphs - broadcasting arithmetic, @, relu/gelu/sigmoid/tanh, softmax, axis reductions, a fused cross-entropy, SGD and Adam update steps, 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. Graph.prepare turns a graph into a session (gpu.session.* requests) whose carried tensors stay on the device between steps. Nothing inside the WebAssembly engine touches a GPU. Natively, zipp py runs the same graphs on a hardware GPU through wgpu (Vulkan, Direct3D 12 or Metal) when one is present, synchronously and with the same results, and on the engine's tensor kernels otherwise.

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, but it covers what ordinary training code reaches for: about 200 more tensor functions with in-place semantics and PyTorch's autograd rules, nn with convolutions up to 3-D, normalization, pooling, recurrent layers, attention and transformers, about 35 activations and 20 losses, 13 optimizers and 15 learning-rate schedulers, torch.utils.data, torch.linalg and torch.fft (both native), torch.distributions, float16 and bfloat16 stored in two bytes with CPU autocast, complex, sparse and quantized tensors, single-process torch.distributed, and torch.save / torch.load in PyTorch's checkpoint format in both directions.
Each area is checked against values PyTorch 2.11 generated, and eager torch's heavy loops (matmul, convolution, elementwise ops, reductions, FFT, factorizations) run as native kernels that repeat the JavaScript fallback's arithmetic in the same order, byte for byte. Hugging Face's modeling_qwen3.py, modeling_qwen2.py, modeling_llama.py, modeling_mistral.py and modeling_gpt_neo.py run unmodified. device="cuda" is rejected rather than silently ignored, and the Torch compatibility guide lists what is still missing.
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.