Skip to content

Getting started

This page compiles and runs a first program, looks at what p2w produced, and shows what happens when a program uses something p2w does not support. It assumes p2w and its tools are installed.

Compile and run

Save this as fib.py:

def fib(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

print(fib(30))

p2w -r compiles it, assembles the result with wasm-tools, and runs the module under Node.js:

$ uv run p2w -r fib.py
832040

Step by step

Without -r, p2w writes WebAssembly text, to standard output or to the file given with -o:

$ uv run p2w -v fib.py -o fib.wat
Compiling fib.py...
Generated 5805 lines of WAT.
Wrote fib.wat
$ wasm-tools parse fib.wat -o fib.wasm

Most of those lines are the runtime: the builtins, the string, list and dict helpers, and the GC type definitions, written in WAT. p2w drops the runtime functions nothing in the program reaches, so fib.wasm is about 41 KB.

The module cannot run on its own. It imports its I/O from the host (env.write_char, env.write_i32, env.f64_to_string, …), and exports _start, which runs the module's top-level code, and memory. p2w -r writes a small Node.js loader that provides the imports and calls _start; a page in a browser needs a loader of its own (see JavaScript interop).

A longer program

Classes, __slots__, math, lambdas, f-strings, match, generators and exceptions compile as they run under CPython:

import math


class Shape:
    name = "shape"

    def area(self) -> float:
        raise NotImplementedError

    def __repr__(self) -> str:
        return f"{self.name}({self.area():.2f})"


class Circle(Shape):
    __slots__ = ("r",)
    name = "circle"

    def __init__(self, r: float) -> None:
        self.r = r

    def area(self) -> float:
        return math.pi * self.r**2


class Square(Shape):
    __slots__ = ("side",)
    name = "square"

    def __init__(self, side: float) -> None:
        self.side = side

    def area(self) -> float:
        return self.side * self.side


shapes = [Circle(1.0), Square(2.0), Circle(0.5)]
print(sorted(shapes, key=lambda s: s.area()))

counts: dict[str, int] = {}
for word in "the cat sat on the mat".split():
    counts[word] = counts.get(word, 0) + 1

match max(counts.items(), key=lambda kv: kv[1]):
    case (word, 1):
        print("all words are unique")
    case (word, n):
        print(f"{word!r} appears {n} times")


def evens(n):
    for i in range(n):
        if i % 2 == 0:
            yield i


try:
    print(sum(evens(10)) // 0)
except ZeroDivisionError as e:
    print("error:", e)
$ uv run p2w -r shapes.py
[circle(0.79), circle(3.14), square(4.00)]
'the' appears 2 times
error: integer division or modulo by zero

The output is byte for byte what python3 shapes.py prints.

When p2w says no

A construct p2w cannot compile faithfully is refused at compile time, with the construct named:

$ echo 'import itertools' > uses_itertools.py
$ uv run p2w -r uses_itertools.py
p2w: error: NotImplementedError: Import not supported: itertools

Some limits can only show at run time. Integers are 31 or 64 bits wide, so a result past 64 bits raises instead of wrapping around:

$ echo 'print(2 ** 64)' > big.py
$ uv run p2w -r big.py
p2w: error: OverflowError: int too large

The known bugs page lists every refusal and run-time limit that is known.

Making it fast

Annotate the hot code with WebAssembly's own number types, i32, i64 and f64, and p2w keeps those values unboxed:

from __future__ import annotations


def count_primes(limit: i32) -> i32:
    count: i32 = 0
    n: i32 = 2
    while n < limit:
        d: i32 = 2
        prime: bool = True
        while d * d <= n:
            if n % d == 0:
                prime = False
                break
            d += 1
        if prime:
            count += 1
        n += 1
    return count


print(count_primes(100_000))

from __future__ import annotations keeps CPython from evaluating the annotations, so the same file still runs under python3. Performance and native types explains what the compiler does with them.