Setu (सेतु, "bridge"): a generic Zig <-> CPython binding layer. One build loads on
CPython 3.14, 3.14t, 3.15, 3.15t and later: a single <name>.so in one
cp314.cp315-abi3.abi3t.cp314t.cp315t wheel per platform (the 3.15 stable ABI for free-threading,
PEP 803, plus a 3.14 entry point). Any pip installs it on 3.14, 3.14t, 3.15 and 3.15t; 3.16t and later
need pip 26.2+ (abi3t). PEP 697 type data, critical sections. No Python.h: the C API is declared by hand in
src/capi.zig.
Zig 0.17.0, Linux.
Setu is mainly for the Shakti and Liburing projects, which use it as a dependency (it never imports either). For your own extensions, use PyOZ instead: it fills the same role and is documented and maintained for public use.
Documentation: https://yostealth.github.io/Setu/ (the guide to writing an extension, the test extension's API).
Note: Setu is not on PyPI and has no Python package: it is a Zig dependency (
build.zig.zon), used at build time.
src/root.zig # the module root (@import("setu"))
src/capi.zig # hand-declared C API
src/py.zig # functions, conversions, errors, the module (exportModule)
src/scan.zig # setu.From: which declarations of a Module.from namespace Python gets
src/class.zig # setu.Class: a struct becomes a Python type
src/exception.zig # setu.Exception: an exception class of the extension
src/stub.zig # .pyi text (stub.module writes an extension's whole .pyi)
src/stubgen.zig # the stub generator addStubs builds (no libpython)
test/ # setu_class, a test extension with a class per hook family (the model root file)
test/wheel/ # setu_class as a project, for the tool/setu_build.py wheel checks
test/doc/ # setu_class as a docs site, for the tool/setu_doc.py checks
test/doc_md/ # a site of Markdown pages only (no Python module), for the same checks
tool/setu_build.py # the wheel build backend (PEP 517) projects copy into their tool/
tool/setu_doc.py # the docs site builder projects copy into their tool/
tool/setu_doc_web.zig # the site's behaviour (WebAssembly, zig build doc-web); app.js only runs it
tool/check_coverage.py # line coverage under pytest (kcov), run by zig build coverage (addCoverageSteps)
# and of Zig unit tests (addZigTestCoverage: zig build coverage-zig)
tool/check_api.py # every public runtime function compiled into setu_class (after zig build coverage)
tool/check_profile.py # instructions per call, per Zig line (callgrind), run by zig build profile
# (addProfileSteps)
tool/check_symbol.py # CPython symbols exported by an interpreter, abi3t on 3.15+, run by
# zig build check-symbol (addSymbolCheck)Commands:
zig build test # type-check every declaration + comptime and doc-web unit tests
zig build develop # test extension (setu_class.so) + stubs into test/
python3 tool/check_doc.py # the docs site of test/doc/ (after develop)
tool/check.py # every check, on 3.15t, 3.15, 3.14t and 3.14
python3 tool/bench.py --ref HEAD # per-call cost, A/B against a commit (`-Doptimize=fast`)
zig build coverage -Dpython=python3 # line coverage of the test extension (kcov), zig-out/coverage.json;
# then tool/check_api.py
zig build profile -Doptimize=fast -Dcpu=x86_64_v3 # instructions per call (callgrind), zig-out/profile.json
python3 tool/setu_doc.py serve # Setu's docs site (zig-out/site): guide, API, coverage, profilellms.txt is the index to read first; doc/setu-guide.md is the guide to writing an extension: the rules,
every parameter type, every class hook signature, every compile error Setu raises. A project using Setu keeps a
copy of the guide at doc/setu-guide.md (zig build setu-guide-sync / setu-guide-check, see
addSetuGuideSteps), so a session working on its bindings finds it inside the project.
tool/check_guide.py keeps the guide in step with the source. A project's own docs site gets llms.txt and
llms-full.txt for its Python API from tool/setu_doc.py.
The extension's root file lists its Zig files once. Setu finds their lowercase pub fns (functions),
setu.Classes, setu.Exceptions, UPPER_CASE constants and enums, creates them in its Py_mod_exec
(classes bases first), adds __version__, runs init, and writes the .pyi from the same declaration:
const setu = @import("setu");
pub const module: setu.Module = .{
.name = "point",
.version = "1.0",
.from = &.{
.{ .ns = @import("point.zig"), .source = @embedFile("point.zig") },
.{ .ns = @import("kernel.zig"), .prefixes = &.{"O_"} }, // constants only: no `source`
.{ .ns = @import("enum.zig"), .enums = .int_enum }, // `enum.IntEnum` classes
},
.init = init, // after Setu's part
};
fn init(_: *setu.c.Object) setu.py.Error!void {}
comptime {
setu.py.exportModule(module);
}// build.zig: the .pyi, written by Setu's stub generator (built for the host, no libpython)
const pyi = @import("setu").addStubs(b, setu_dep, .{ .root_source_file = b.path("src/root.zig") });_-named declarations, camelCase pub fns, plain structs and lowercase values are skipped; a name
found twice is a compile error. src/scan.zig (top) has the rules, test/setu_class.zig an example of
each filter (.only, .exclude, .prefixes, .functions = false).
Setu's own code follows Zig style. In a file that Module.from, py.exportFns or setu.Class exports, a lowercase
pub fn, snake_case included (prep_recv, get_label), is a Python function, method or property under
that name; camelCase pub fns are Zig-only helpers. So snake_case function names there are on purpose,
not a slip from Zig's camelCase rule: they are the Python names. Hooks keep their Python names too
(__init__, __repr__).
A PEP 517 build backend (no setuptools) that builds a Zig + Setu extension's wheels with zig build.
A backend must sit in the project's own tree, so each project keeps a byte-identical copy in its
tool/, never edited there: what differs per project goes in [tool.setu_build] in its
pyproject.toml (the keys are listed in the script's docstring). Setu tests the script itself:
tool/check.py builds the glibc and musl wheels of test/wheel/ and pip-installs them on every
interpreter (tool/check_wheel.py).
A project keeps its copy in step with the Setu it pins with two zig build steps, from its
build.zig (setu must be in its build.zig.zon):
const setu_dep = b.dependency("setu", .{});
@import("setu").addSetuBuildSteps(b, setu_dep);zig build setu-build-sync copies the pinned Setu's tool/setu_build.py into tool/ (run it after
bumping Setu); zig build setu-build-check fails if the copy differs (run it in the project's checks).
A static HTML site built by Setu's own builder, Python's standard library only: the API pages from the imported
module's docstrings (the /// docs; typed signatures from the .pyi), grouped by a [tool.setu_doc] table in
the project's pyproject.toml (the keys are in the script's docstring), the project's Markdown pages, the
constants, its example scripts, llms.txt / llms-full.txt, and a runner for the >>> examples
(setu_doc.py check; project fixtures in a hooks file). Everything the pages do (search, Run, the theme menu,
Copy, the source views) is Zig compiled to WebAssembly (tool/setu_doc_web.zig); setu_doc.py serve serves
the site on 127.0.0.1 with Run buttons that run an example and show its session (only on examples that can run:
none that read a name nothing defines; the published site has no Run buttons). A Markdown table after a
<!-- chart --> line becomes a colored bar chart (GitHub shows the table). The project keeps a byte-identical copy
of setu_doc.py in tool/, like setu_build.py, in step with addSetuDocSteps (zig build setu-doc-sync /
setu-doc-check), which also adds zig build doc-web (the Wasm module, from the pinned Setu; setu_doc.py runs
it). A Zig project with no Python module gets the same site from its Markdown pages (no module, a name; a
static folder such as Zig's autodoc output copied in). test/doc/ is a working model, test/doc_md/ a
module-less one (tool/check_doc.py). The workflow runs zig build develop (the extension
and its stub), then python tool/setu_doc.py (writes site/), and uploads site/ to GitHub Pages.
Versions are <year>.<month>.<day>[-<n>], no zero padding: 2026.10.20 for a day's first release, then
2026.10.20-2, 2026.10.20-3, ... Tags are v<version> (the build.zig.zon version). Each tag's notes say whether
tool/setu_build.py changed since the previous tag: if it did, projects run zig build setu-build-sync
after bumping to it. git diff --quiet <previous tag> <tag> -- tool/setu_build.py tells (exit 1: changed).