Getting Started¶
Three audience-routed entry points. Pick the one closest to your case.
- For web developers: Python in the browser
- For embedded and systems developers: small native Python
- For contributors: clone, build, test the full stack
The cross-cutting Prerequisites section is at the bottom.
For web developers¶
You're here because you want to run Python in a browser tab without 30 MB of Pyodide. The shipping artifact is web/nanopython.wasm (270 KB raw, post-wasm-opt -Oz). It boots in well under a second on a typical connection and can drive the DOM directly via a js module.
Five minutes: try the bundled demos¶
git clone <repo> && cd nanopython
make port # one-time: regenerate vendor/
make web # build wasm + run wasm-opt; writes web/nanopython.wasm
cd web && python3 -m http.server 8000
# Open http://localhost:8000/playground/
The bundled demos under web/:
| Demo | What it shows |
|---|---|
playground/ |
Python REPL in the browser, with a script library backed by localStorage |
life/ |
Conway's Game of Life: DOM events + canvas updates driven from Python via the js module |
dashboard/ |
Live dashboard with setInterval-driven Python computation |
simulation/ |
Particle simulator in pure Python, rendering to a <canvas> |
snippets/, validation/ |
Smaller examples (string ops, form validation) |
Thirty minutes: write your own page¶
The js module (ADR 006) gives Python code direct DOM access via a synchronous host-call bridge:
import js
doc = js.document
btn = doc.getElementById("go")
btn.addEventListener("click", lambda e: js.alert("hello from Python!"))
The bridge wiring lives in web/lib/js-bridge.js and web/lib/loader.js; copy one of the bundled demos as a starting template, swap in your Python code, and serve. The Python-side idioms (how js proxies behave, when to convert to/from JS arrays, async-via-callbacks patterns) are in notes/03-nano-python-cheatsheet.md. Read it before writing nontrivial code, because the wasm/nano build has a few platform constraints worth knowing up front.
What you can write today¶
| ✓ Works | ✗ Out of scope today |
|---|---|
| Floats, dicts, classes, generators, comprehensions, f-strings, walrus | eval() / exec() / compile() from user code (vendor longjmp wall, closing in v0.6, ADR 010 §A1) |
All common builtins, try/except/finally |
2**100 overflows: wasm uses 64-bit LONGLONG rather than MPZ |
import json / heapq / bisect / random / collections / functools / types (pure-Python frozen-tiny) |
Multiple inheritance |
import js for DOM access |
async/await runtime |
gc module surface |
File I/O (open()) |
Per-program coverage of 823 curated real-world Python programs in COMPATIBILITY.md.
Going further¶
- Read
notes/03-nano-python-cheatsheet.mdbefore writing nontrivial code. - For DOM patterns beyond what the bundled demos show, the
js-bridge.jssource is well-commented. - Cold-load measurements (parse + instantiate + first print) are recorded via
make metrics-recordif you want to track changes to your own page.
For embedded and systems developers¶
You're here because you want a small Python binary you can ship on a Linux/macOS system, embed in a Zig project, or trim down to fit a flash budget. nanopython matches MicroPython's behavior byte-for-byte on 541/541 of MicroPython's basic test suite, with per-feature build flags that let you decompose the runtime into the slice your use case needs.
Build a native binary¶
git clone <repo> && cd nanopython
make port # one-time: regenerate vendor/
make build # Debug native binary at zig-out/bin/nanopython (3.5 MB)
make build-release # ReleaseFast (~994 KB)
make build-small # ReleaseSmall (~436 KB) — the smallest native shipping size
Run a program¶
./zig-out/bin/nanopython examples/fizzbuzz.py
echo 'print(2**10)' | ./zig-out/bin/nanopython /dev/stdin
./zig-out/bin/nanopython # interactive REPL (Ctrl-D to exit)
Verify the build¶
Anything less is a regression; make verify names the failing test. See Contributing for the regression-localization workflow.
Per-feature builds: pick exactly what you ship¶
The build is decomposed into orthogonal feature flags (ADR 002). Each flag toggles one builtin module independently of the others.
# Example: ship only the language core + math, no struct/array/collections
zig build -Doptimize=ReleaseSmall \
-Dfeature-collections=false \
-Dfeature-struct=false \
-Dfeature-array=false
| Flag | What it controls | Default (nano) |
|---|---|---|
-Dfeature-math |
math module |
on |
-Dfeature-warning |
the warning subsystem | off |
-Dfeature-collections |
collections (namedtuple, OrderedDict, deque) |
on |
-Dfeature-struct |
struct (binary pack/unpack) |
on |
-Dfeature-array |
array.array |
on |
-Dfeature-errno |
errno constants |
on |
-Dfeature-micropython |
micropython module (heap_lock, opt_level) |
on |
-Dfeature-gc-module |
gc module surface |
on |
-Dfeature-js |
JS interop bridge (forced off for non-wasm) | wasm only |
Flipping a flag away from its preset default emits a build-time warning that the corpus invariant won't be enforced for that build: the byte-exact make check is calibrated against the default nano slice.
Cross-compile¶
Zig's cross-compilation works without extra toolchains. The Linux ELFs are produced from a macOS host equally well:
zig build -Dtarget=x86_64-linux-gnu -Doptimize=ReleaseSmall # → ELF
zig build -Dtarget=aarch64-linux-gnu -Doptimize=ReleaseSmall # → ELF
zig build -Dtarget=x86_64-macos -Doptimize=ReleaseSmall # → Mach-O
zig build -Dtarget=wasm32-wasi -Doptimize=ReleaseSmall # → .wasm
When you need more than nano ships¶
- MPZ arbitrary-precision ints (
2**100etc.): build with-Dconfig=full. iomodule +open()+ filesystem-backed scripts: build with-Dconfig=full.- Real microcontroller ports (STM32, ESP32, RP2040, etc.): use upstream MicroPython. Bare-metal targets are out of scope here.
The trade with -Dconfig=full is that the binary pulls in additional vendor C carveouts (MPZ, IO, complex, builtin_help, warning) for the parity surface. Soft target instead of the strict 541/541 invariant.
Embedding in a Zig project¶
The compile + execute entry point is a stable Zig export: npy_zig_compile_source(src: [*:0]const u8) → c_int. Link nanopython as a library and call it directly. See vs MicroPython for the precise compatibility statement.
For contributors¶
You're here because you want to understand or improve the runtime itself. The codebase is the result of an incremental C→Zig strangler of MicroPython under a byte-exact differential test corpus; the methodology is documented in TR-001 (notes/tech-reports/TR-001.md), the scientific-paper form of the lessons.
Five-minute orientation¶
git clone <repo> && cd nanopython
make port # one-time: regenerate vendor/
make # the aggregate gate (4 test gates + lint, both arms) — ~37s
make is the "I'm about to commit" gate. It runs four independent test gates (corpus native, corpus wasm, integration native, integration wasm) plus lint plus build. Each gate answers a different question; read the "What 'green' means" section in CLAUDE.md for the breakdown.
The test pyramid¶
make test # native-only test pyramid (faster, used while iterating)
make check # native byte-exact corpus — the strangler invariant gate
make verify # same, but lists each failing test
make wasm-check # wasm corpus + baseline regression detector (strict both ways)
make test-integration # 37 hand-written cases, native
make test-integration-wasm # 37 hand-written cases, wasm via wasmtime
make test-unit # Zig unit tests for the compiler + VM tracks
make test-coverage # kcov-driven coverage reports → coverage/
Code orientation¶
| Path | What |
|---|---|
src/compiler/ |
Pure-Zig lexer, parser, scope, bytecode emit; also ships as the zigpy-compile standalone tool |
src/runtime/ |
VM dispatch, error propagation, allocator, GC |
src/objects/ |
Type implementations (str, list, dict, int, …) |
src/modules/ |
Builtin modules (sys, math, gc, …) |
src/mp_mirrors.zig |
Zig types mirroring vendor MicroPython C structs (~40 extern struct declarations) |
src/pyerr.zig |
PyError!T error union + polled-sentinel bridge |
nanopython-lib/ |
Pure-Python frozen-tiny stdlib (json, heapq, bisect, etc.) |
port/mpconfigport.h |
The two MicroPython config arms (nano + full) |
corpus/nano/basics/ |
541 byte-exact frozen goldens for native |
golden-programs/ |
823 real-world Python programs bucketed by what they need |
tests/integration.py |
37 hand-written integration cases, runner-pluggable |
tools/ |
corpus.py, metrics.py, wasm_gate.py, gen_compatibility.py, … |
Read these first¶
| Document | Purpose |
|---|---|
CLAUDE.md |
Maintainer + AI-agent invariants. The hard rules. |
notes/02-detailed-design.md |
Build system, shim convention, FFI boundary |
notes/03-nano-python-cheatsheet.md |
Platform constraints when writing Python for the nano/wasm build |
notes/tech-reports/TR-001.md |
Methodology paper: the byte-exact differential oracle + the two technical primitives |
docs/adrs/ |
Architecture decision records; ADR 010 is the current roadmap |
Your first change¶
- Pick something small: a new
strmethod, a builtin function, acollectionshelper. The v0.5 changelog has working examples (str.ljust/rjust/isalnum, full-Unicodechr(),collections.deque). - Add the implementation in the appropriate
src/objects/*.zigorsrc/modules/*.zig. - Add at least one regression pin in
tests/integration.pyso the fix can't silently regress. - Run
make. Green = commit. Red = debug.
The complete workflow + conventions are in Contributing.
Prerequisites¶
| Tool | Version | Notes |
|---|---|---|
| Zig | 0.16.0 | Newer versions may work; the build script targets 0.16's std.Build API |
| Python 3 | 3.9+ | for the test runners (tools/*.py) |
| GNU Make | any | for the top-level Makefile |
git |
any | the project assumes a git checkout |
| Upstream MicroPython | matching embed.mk API |
symlinked at sandbox/micropython (gitignored) |
| wasmtime | any recent | for running the wasm binary locally |
| binaryen | any recent | for wasm-opt -Oz (optional; make web degrades gracefully without it) |
The MicroPython symlink feeds make port, which regenerates vendor/micropython_embed/. It's gitignored because it points outside the project tree. If you don't have a checkout:
git clone https://github.com/micropython/micropython.git ~/micropython
ln -s ~/micropython sandbox/micropython
make port
Common issues¶
make port fails with "sandbox/micropython not found": create the symlink as above.
make reports < 541/541 on a fresh checkout: likely a Zig version mismatch or a stale vendor/. Try make rebuild.
make wasm builds, but wasmtime web/nanopython.wasm errors: check that wasmtime is on the path and recent enough to support the wasi_snapshot_preview1 ABI.
Debug build is 3.5 MB: expected. Zig's Debug mode keeps every function alive and emits DWARF for nice panic traces. Use make build-small for the shipping size.
Cold first build takes 1-2 minutes: Zig has to compile + link a chunk of vendor C the first time. Incremental builds are seconds.
Where things live¶
| Path | What |
|---|---|
zig-out/bin/nanopython |
The interpreter (Debug or Release) |
zig-out/bin/zigpy-compile |
Standalone .py → .mpy compiler |
zig-out/bin/mpy-roundtrip |
.mpy round-trip test tool |
web/nanopython.wasm |
The shipping wasm artifact |
.zig-cache/ |
Zig's build cache (regenerable, safe to delete) |
vendor/micropython_embed/ |
Generated MicroPython C package (via make port) |
metrics/history.ndjson |
Append-only size/coverage/perf history (via make metrics-record) |
coverage/ |
Generated coverage reports (via make test-coverage) |
All generated directories are gitignored.