Skip to content

Getting Started

Three audience-routed entry points. Pick the one closest to your case.

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.md before writing nontrivial code.
  • For DOM patterns beyond what the bundled demos show, the js-bridge.js source is well-commented.
  • Cold-load measurements (parse + instantiate + first print) are recorded via make metrics-record if 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

make check       # native byte-exact corpus — reports 541/541

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**100 etc.): build with -Dconfig=full.
  • io module + 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

  1. Pick something small: a new str method, a builtin function, a collections helper. The v0.5 changelog has working examples (str.ljust/rjust/isalnum, full-Unicode chr(), collections.deque).
  2. Add the implementation in the appropriate src/objects/*.zig or src/modules/*.zig.
  3. Add at least one regression pin in tests/integration.py so the fix can't silently regress.
  4. 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.