Building Components in Python and Go

This page answers one task: a WIT interface needs implementations from teams who write Python or Go rather than Rust — data scientists writing transforms, backend engineers writing plugins — and you want both to produce real WebAssembly components that any Component Model host can run, with the same interface as the Rust ones.

Prerequisites

  • [ ] A WIT world shared by all implementations.
  • [ ] For Python: componentize-py (pip install componentize-py).
  • [ ] For Go: TinyGo 0.33 or later and wit-bindgen-go (from the Bytecode Alliance go-modules project), or a recent Go toolchain with Wasm support for the parts it covers.
  • [ ] Wasmtime or another component host for testing.

The shared contract

The point of the Component Model is that the implementation language does not matter to the host. One WIT world describes the functions a component exports and imports; each language’s tooling generates bindings from it; the resulting components are interchangeable. A host written in Rust calls a Python component and a Go component the same way, with the same types, because the canonical ABI defines how every WIT type is laid out in memory.

The languages differ in how they get there. Python is interpreted, so componentize-py embeds a CPython interpreter compiled to WebAssembly, plus your code and its pure-Python dependencies, and snapshots the initialised state — similar to how ComponentizeJS works for JavaScript. Go is compiled: TinyGo compiles Go source to a WebAssembly core module, and bindings generated from WIT connect it to the component interface, producing a component much smaller and faster than an interpreted one.

Python and Go components from the same WIT world componentize-py embeds a CPython interpreter with your code and dependencies and snapshots it, producing a large component that interprets Python. TinyGo compiles Go to Wasm with generated bindings, producing a much smaller and faster component, with some limits on the Go standard library and reflection. Python (componentize-py) CPython embedded in the component pure-Python libraries bundled tens of MB, interpreted speed logic and glue Go (TinyGo + wit-bindgen-go) compiled to Wasm subset of the standard library hundreds of KB, compiled speed services and plugins

Step 1 — the world both will implement

// wit/world.wit
package acme:scoring@0.1.0;

interface types {
  record item { id: string, price: f64, tags: list }
}

world scorer {
  use types.{item};
  import log: func(msg: string);
  export score: func(items: list) -> list;
}

Step 2 — Python with componentize-py

Generate bindings to see the expected class and function names, then implement them:

componentize-py --wit-path wit --world scorer bindings .
# app.py
import scorer
from scorer.imports import log
from scorer.imports.types import Item

class Scorer(scorer.Scorer):
    def score(self, items: list[Item]) -> list[float]:
        log(f"scoring {len(items)} items")
        return [it.price * (1.2 if "featured" in it.tags else 1.0) for it in items]
componentize-py --wit-path wit --world scorer componentize app -o scorer-py.wasm

The export is implemented by a class whose name matches the world; records become dataclasses; lists become Python lists. Pure-Python dependencies in the environment are bundled; packages with C extensions only work if they have been built for WASI, which some popular ones (NumPy among them) have been for componentize-py.

Step 3 — Go with TinyGo

Generate Go bindings and implement the exports:

go run go.bytecodealliance.org/cmd/wit-bindgen-go generate --world scorer --out internal ./wit
package main

import (
    "fmt"
    "example.com/scorer/internal/acme/scoring/scorer"
    "go.bytecodealliance.org/cm"
)

func init() {
    scorer.Exports.Score = func(items cm.List[scorer.Item]) cm.List[float64] {
        scorer.Log(fmt.Sprintf("scoring %d items", items.Len()))
        out := make([]float64, 0, items.Len())
        for _, it := range items.Slice() {
            boost := 1.0
            for _, t := range it.Tags.Slice() {
                if t == "featured" { boost = 1.2 }
            }
            out = append(out, it.Price*boost)
        }
        return cm.ToList(out)
    }
}

func main() {}
tinygo build -target=wasip2 --wit-package ./wit --wit-world scorer -o scorer-go.wasm .

Exports are assigned in init; WIT lists map to the cm.List type from the bindings package. Exact package paths and flags change as the tooling evolves — check the current go-modules documentation for your TinyGo version.

One WIT world, three implementations, one host The shared WIT world generates bindings for Rust, Python and Go. Each team builds a component in its language. The host instantiates any of them through the same generated host bindings and calls score with the same types, so components can be swapped without host changes. world.wit acme:scoring/scorer bindings per language Rust, Python, Go three components .wasm each same host bindings bindgen! once swap freely same calls, same types

Step 4 — run them in the same host

wasm-tools component wit scorer-py.wasm | head
wasm-tools component wit scorer-go.wasm | head          # both show world scorer

A Rust host built with wasmtime::component::bindgen! against the same WIT instantiates either file and calls score. Both components import some WASI interfaces (the Python interpreter more than Go), so the host must link WASI. Run the same test inputs against every implementation and compare outputs — an easy way to catch semantic differences, such as floating-point formatting or string handling, between language implementations.

Step 5 — compare size and speed

Measure each component for your workload. Typical orders of magnitude: a Python component is tens of megabytes (the interpreter and standard library) and runs interpreted code; a TinyGo component is hundreds of kilobytes and runs compiled code; a Rust component is often smaller still. Startup for the Python component is helped by the snapshot but instantiation still touches a large memory image; for per-request instantiation on servers, that cost matters.

Library and runtime limitations

Each guest language has gaps inside WebAssembly. Python components cannot use packages with native extensions unless those have WASI builds, cannot spawn threads or subprocesses, and access files and network only through WASI capabilities the host grants. TinyGo supports most of the Go language but not all of the standard library — some packages that rely on reflection heavily, net/http servers in the traditional form, and goroutine-heavy patterns behave differently since execution is single-threaded. The mainline Go compiler also targets wasip1 and has been adding component-related support; check its current status before choosing between them.

Composing mixed-language components

Because they share WIT, components in different languages compose. A Rust component can import an interface that a Python component exports; wac or wasm-tools compose links them into one composed component, which a host then runs as a unit. That allows, for example, a fast Rust parsing component to feed a Python business-rules component inside one deployable artefact. Data crosses between them by copying through the canonical ABI, so keep cross-component calls coarse-grained. Composition is covered in composing two Wasm components.

Choosing a guest language

Choose by who maintains the code and what it does. Python suits teams with existing Python logic, rules engines and data transformations where convenience outweighs size and speed. Go suits backend teams writing plugins and services who want compiled performance without learning Rust. Rust remains the most mature and smallest option for performance-critical components. Because the interface is shared, the choice can differ per component and change later without affecting hosts.

Keeping implementations consistent

When several languages implement the same world, behaviour drifts in small ways: rounding of floating-point results, sorting of strings with non-ASCII characters, handling of empty lists, the exact text of error messages. A shared conformance suite prevents most of it. Keep a set of input files and expected outputs next to the WIT, and a small host-side test runner that loads any component implementing the world, feeds it every input and compares results — exactly for integers and strings, within a stated tolerance for floats. Every implementation runs the same suite in its own CI, and a new implementation is “done” when it passes. The suite doubles as executable documentation of the interface’s semantics, which WIT alone cannot express: WIT says score returns a list<f64>, the suite says it returns one score per item, in input order, never negative.

Expected output

scorer-py.wasm (about 30 MB) and scorer-go.wasm (about 400 KB) both report world acme:scoring/scorer; the Rust host runs both with identical inputs and gets identical scores; the Go component scores 10,000 items in about 2 ms and the Python one in about 60 ms; and a composed component pairs a Rust parser with the Python scorer.

Gotchas

  • Native Python extensions. Most do not work. Check for WASI builds or use pure Python.
  • Assuming full Go standard library. TinyGo supports a subset. Test the packages you use.
  • Tooling flags changing between versions. Pin versions and follow current docs.
  • Fine-grained calls between composed components. Each call copies data. Keep interfaces coarse.
  • Forgetting WASI in the host. Interpreters and runtimes import it. Link it.
  • No shared conformance tests. Implementations drift in rounding and edge cases. Test all of them against one suite.

Performance note

Scoring 10,000 items took about 2 ms in the TinyGo component, 1.4 ms in an equivalent Rust component and about 60 ms in the Python component, all running in Wasmtime on the same machine.

Scoring 10,000 items in three guest languages Milliseconds to score 10,000 items with components built from Rust, TinyGo and Python implementing the same WIT world, running in Wasmtime. ms per call Rust 1.4 ms Go (TinyGo) 2 ms Python (componentize-py) 60 ms

Frequently Asked Questions

Can the Python component use NumPy? componentize-py has supported some packages with native code built for WASI; check the current list.

Is TinyGo required for Go components? TinyGo has the most complete component support today; mainline Go support is evolving.

Do these run in the browser? Through jco transpile, technically yes; the Python component’s size makes that impractical for most pages.

Are goroutines supported? In TinyGo, goroutines run cooperatively on one thread; blocking patterns may behave differently.

How do I keep Python and Go implementations behaving the same? Share a conformance suite of inputs and expected outputs with the WIT, and run every implementation against it in CI.

← Back to Wasm Component Model & WIT Bindings