One root file declares pub const module: setu.Module = .{ .name, .version, .from = &.{...}, .init } and ends with comptime { py.exportModule(module); }. Nothing else registers anything; build.zig writes the .pyi from the same declaration (addStubs).
A lowercase pub fn in a from file is a Python function under that name (prep_recv, get_label). camelCase pub fns are Zig-only helpers. There is no opt-out: the case picks the audience.
Give an int parameter the exact type the kernel or callee takes (fd: i32, flags: u32): Setu raises OverflowError naming the parameter. A value no parameter type can bound goes through py.narrow(T, v, "`f(x)`"). Every @intCast / @truncate carries a // cast: <why it fits> comment on its line or the line above (tool/check.py checks).
?T is optional: omitted or None gives null. A positional parameter after one with a default needs a default too (compile error); make it keyword-only after _: py.KwOnly.
Return Error!R and raise with return py.raise*(...). error.PythonError means "an exception is set"; any other Zig error becomes RuntimeError (error.OutOfMemory becomes MemoryError).
Buffers: py.Buffer / py.WritableBuffer are locked for the call and released after it. To keep one past the call, try buf.take() (a second view, released by your deinit) or take py.OwnedBuffer / py.OwnedWritableBuffer (yours to deinit). Never store a plain Buffer.
A struct becomes a type with pub const XType = setu.Class(X, @embedFile("x.zig")). Fields are the instance data and need defaults; a ?*Object field is an owned reference Setu traverses and clears.
Hooks are pub fn __name__ with the signatures of section 5. __init__ / __new__ / __call__ take named parameters or one setu.CallArgs, never py.Rest / py.Kwargs (compile error).
Docs are /// lines above the declaration: a description, then sections (Type, Example, Note, Flags, ...) as a header on its own line with content indented 4 spaces. Type entries override the stub's types (return: Iterator[T]); Example is a real >>> session, each result on the next line.
Hot paths: generated code must benchmark like hand-written code. Fill arguments in place, keep slow paths out of line, measure before and after with tool/bench.py (section 9).