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 Alliancego-modulesproject), 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.
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.
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.
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.
Related
- Building a component in Rust with cargo component — the Rust version.
- Building components in JavaScript with ComponentizeJS — another interpreted guest.
- Running components in Wasmtime — the host.
- Other Languages in the Browser — Python and Go in the browser.
← Back to Wasm Component Model & WIT Bindings