The VM was already language-agnostic

Zipp's VM executes FuncProto objects: a flat vector of three-address instructions over a fixed register file, with an explicit frame stack in the heap. Calls, exceptions, generators, closures and module state are runtime data structures. The bytecode does not know what a var is or that this exists; the JavaScript compiler lowers those concepts into registers, cells and property operations. Any language whose semantics can be expressed as registers, cells, property access on heap objects and a call convention can target it.

Python fits. Its scoping is static (a symbol table decides locals, cells and globals at compile time, exactly as CPython does), its objects are dictionaries with a method-resolution order, and its calls bind positional and keyword arguments to a signature. All three map onto things the VM already has.

The pipeline

  1. Parse with the RustPython parser

    A vendored fork of rustpython-parser 0.4.0 produces the AST. It is a parser only; none of RustPython's interpreter is involved. Compile-time caps guard it: 1 MiB per module, 2^20 tokens, 200 nested brackets, 100 indentation levels, 96 nested expressions, 8,192 functions per program.

  2. Build the symbol table

    symtable.rs applies CPython's scoping rules: which names are local, which are free, which are cells, which are module globals, what nonlocal and global do, how comprehensions and class bodies scope.

  3. Emit register bytecode

    emitter.rs, stmts.rs and exprs.rs lower each code object to a FuncProto with a fixed ABI. Register 0 is the Python function object; register 1 is the array of bound positional values produced by the runtime's bind. Locals are registers, captured locals are cell objects, free variables are read off the function's cells, and module globals are a Map.

  4. Link against a fixed runtime

    Python's object model (core.js, types.js, builtins.js, stdlib.js, entry.js) is JavaScript that the VM compiles once per program. Guest Python never sees the host's JavaScript globals; the runtime is an implementation detail, not an interop surface.

Because a Python call is a direct VM Call, Python recursion lives on the VM's explicit frame stack and ends in a catchable RecursionError. Because Python exceptions are VM exceptions, try / except / finally and raise ... from compile to the same unwinding the JavaScript compiler uses. Generators use the VM's suspendable frames, so send, throw, close and yield from came almost for free.

What had to be built anyway

The VM gave the frontend control flow and memory; it did not give it Python. The runtime implements arbitrary-precision integers (on the VM's BigInt), the full str and bytes method sets, the container types with CPython's semantics, C3 method resolution, descriptors, properties, __slots__, metaclasses, __init_subclass__ and __set_name__, the full match statement, CPython-style tracebacks including the [Previous line repeated N more times] collapsing, and a standard-library subset large enough to run real programs: math, random, json, re, itertools, functools, collections, dataclasses, enum, typing, struct, hashlib, pathlib, argparse and more.

Projects are folders. Every .py file is a module or package by folder, with CPython-like import cycles, and every file is readable through open(), os.path and pathlib from a virtual filesystem capped at 8 MiB per file and 64 MiB in total. Writes are reported to the host as versioned changes; the CLI writes them to disk, the browser only reports them.

Validation: CPython is the oracle

Every program in tests/python_corpus runs under a local CPython and under Zipp, and the standard output must match byte for byte. The CPython outputs are committed, so continuous integration needs no CPython. The Torch subset has its own fixtures against PyTorch: the training example reproduces ten rounded losses from 0.085254 to 0.010812, and a GPU training fixture compares five steps against CPU PyTorch 2.11. These are functional checks and the documentation refuses to call them anything else.

What it costs today

Python on Zipp is currently an interpreter over a JavaScript-shaped VM, with the native JIT disabled for Python states because the frontend pulls in the instrumentation feature, and with every integer a BigInt. 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, against 0.005, 0.05 and 0.03 s in CPython. The stated next step is dedicated Python bytecodes: small-integer fast paths and direct dispatch for the operations the JavaScript-shaped runtime currently reaches through property access.

The feature gaps are listed rather than hidden: no async / await, no type-parameter syntax, no except*, no complex numbers, no threads, no sockets, and input() raises EOFError.

Interop, deliberately limited

Since both languages share a VM, calling across is cheap to offer and dangerous to offer carelessly. The opt-in python-js-interop feature exposes javascript.eval(source) and copies data across: numbers, strings, booleans, BigInts, lists and plain dicts, with None as null, capped at 10,000 visited values, depth 32 and 32,768 code units of source. Functions, accessors, proxies and cycles are rejected. The documentation states the important part in bold: this shares VM globals and is not an isolation boundary between mutually untrusted languages. Neither published WebAssembly archive enables it.

Why do it this way

The alternative designs were a second interpreter (two GCs, two limit systems, two host surfaces, two audits) or transpiling Python to JavaScript source (semantics drift, no tracebacks, no way to keep Python off the host globals). Compiling to the shared bytecode meant the browser sandbox's instruction budget, heap ceiling, host-call channel and Worker deadline apply to Python unchanged, the same Engine class runs both, and the WebAssembly module with Python is 43% larger than the one without rather than twice the size. It also meant the day the Python frontend landed, it already had a garbage collector, a conformance-tested string implementation and an audited host boundary.