Contributing¶
For developers modifying the runtime or adding features. If you just want to use nanopython, Getting Started is shorter. If you want to understand the architecture before reading this, start with Architecture.
The one rule¶
make must end with exit 0. This is the strangler invariant + the four-gate test harness combined. Any commit that breaks it is broken; either fix it or revert.
That's it. If your change passes make, it's compatible with the project; otherwise it isn't.
Workflow¶
- Make your change.
- Run
make. - If green, commit. If red, debug or revert.
For larger changes:
If a gate regresses but you're not sure which test:
make verify # per-test PASS/FAIL list — names each native miss
make wasm-check # same idea for the wasm arm
Diff the output against a known-good run to localize the regression.
Project conventions¶
Distilled from CLAUDE.md and the notes/lessons-learned/ library:
Surgical changes¶
Touch only what your change requires.
- No "improving" adjacent code, comments, or formatting.
- No refactoring of things that aren't broken.
- Match existing style even if you'd do it differently.
- If you notice unrelated dead code, mention it; don't delete it as part of an unrelated change.
The test: every changed line should trace directly to the request.
Surgical commits¶
One conceptual change per commit. The commit history is the project's actual journal; commits like fix(strings): str.splitlines(False) now strips line endings — flag value bug are easy to bisect against. Don't bundle unrelated changes.
Commit-with-hooks-disabled is the project convention (hooks have corrupted byte-exact corpus/nano/basics/*.out fixtures in the past):
Do not add Co-Authored-By trailers unless explicitly asked.
The _ = &func; rule¶
When you write a comptime force-analyze block, use the address-of variant:
// CORRECT — forces full Zig body analysis
comptime { _ = &myFn; }
// WRONG — silently fake-green
comptime { _ = myFn; }
The wrong form silently passes the compile without checking the body. This has caused undetected regressions.
Corpus golden files are sacred¶
corpus/nano/basics/*.out and corpus/full/basics/*.out are byte-exact binary fixtures. Never run formatters, trailing-whitespace tools, or line-ending normalizers over them. make lint and make fmt deliberately exclude them.
Regenerating goldens is the make freeze target, only after a deliberate config change. Re-freezing from a broken binary destroys the oracle.
Don't touch sandbox/ or vendor/¶
sandbox/micropythonis a symlink to a 2.6 GB external clone. Gitignored.vendor/is generated bymake port. Gitignored. Regenerable fromsandbox/micropython+port/mpconfigport.h.
If you need to change MicroPython config behavior, edit port/mpconfigport.h and regenerate via make port.
Don't push problems under the rug¶
The project surfaces work-in-progress explicitly. If you discover a real failure that's deferred work:
- Surface it in
CLAUDE.md's known-issues area AND in the relevant ADR. - Reference where the work is tracked.
- Don't add a SKIP-style guard or a quiet
// TODOto keep the suite quietly green.
The rule of thumb: would a future maintainer be surprised by this? If yes, surface it.
Markdown soft-wrap¶
All markdown in this project uses soft-wrap. One paragraph = one long line. Editor word-wrap is what handles display.
Common patterns¶
Adding a new builtin or builtin method¶
The shortest path:
- Add the implementation in the appropriate
src/objects/*.zigorsrc/modules/*.zig. - Wire any new qstrs through the
__qstr_shadow_begin__/__qstr_shadow_end__block convention sotools/zig_qstr_shadow.pyregisters them. - Add at least one regression pin in
tests/integration.py. - Run
make.
Working examples to copy from: str.ljust/str.rjust/str.isalnum (added in v0.5), chr() Unicode range (also v0.5), collections.deque's CPython-friendly (iterable) signature.
The PyError convention¶
If your fn can raise a Python exception, return PyError!T (rather than T with a polled side-channel):
const pyerr = @import("../pyerr.zig");
const PyError = pyerr.PyError;
// Per distinct exception+message, a small raise helper at file top:
fn raiseValueErrorBadX() PyError!void {
pyerr.pending_exc = mp_obj_new_exception_msg(&mp_type_ValueError, "bad x");
return error.PyRaise;
}
// Internal fn returns PyError!T:
fn xImpl(arg: Obj) PyError!Obj {
if (bad(arg)) try raiseValueErrorBadX();
return compute(arg);
}
// C-ABI export wraps with pyerr.bridge:
pub export fn npy_x(arg: Obj) callconv(.c) Obj {
return pyerr.bridge(Obj, xImpl(arg));
}
For void exports: pyerr.bridge(void, xImpl(...)).
For exports referenced via @ptrCast in ROM tables: split into Impl + thin C-ABI shim.
Adding a new module-registered Zig module¶
Use the __qstr_shadow_* block convention so tools/zig_qstr_shadow.py registers the qstrs + module:
//! my module ...
//!
//! __qstr_shadow_begin__
//! #if MICROPY_PY_MYMODULE
//! MP_QSTR_mymodule
//! MP_QSTR_myfunc
//! MP_REGISTER_MODULE(MP_QSTR_mymodule, mp_module_mymodule);
//! #endif
//! __qstr_shadow_end__
The shadow generator picks this up at make port time. See src/modules/modgc.zig and src/modules/modmath.zig for working examples.
Adding a unit test¶
Append a test "..." { ... } block in-line in any module under src/:
test "vstr_init produces a usable buffer" {
const std = @import("std");
const testing = std.testing;
var v: VstrLocal = undefined;
vstr_init_len(&v, 10);
try testing.expect(v.alloc >= 10);
}
Test discovery is automatic via build.zig's addTest step. Run with make test-unit-vm or make test-unit-compiler.
Adding an integration smoke test¶
Edit tests/integration.py:
Or, if exit code matters, the 4-tuple form:
Run with make test-integration (native) or make test-integration-wasm (wasm). Every bug fix should ship with a regression pin here.
What if I want to land a big change?¶
Bigger changes often need to land in multiple commits, each individually green. The pattern:
- Split the change into commits that each pass
makeindependently. - Order them so the green sequence makes sense (e.g., add the new helper first, then the migration that uses it).
- Verify at each commit.
If you can't split a change into green intermediates, it's probably too coupled and needs a design rethink.
Where to ask¶
The project is built mostly via solo + AI-assisted development. There's no formal community forum yet. Issues + discussion welcome at the upstream repo.
License¶
Contributions are accepted under the MIT license, inherited from MicroPython.