What Zig teaches us about SPy
SPy is a statically typed, compiled variant of Python. Zig is a systems programming language designed as a modern replacement for C. At first glance these two languages inhabit completely different worlds: one targets scientific Python developers who want performance without leaving Python’s mental model; the other targets systems programmers who want a cleaner C. Yet looking at Zig turns out to be one of the most illuminating ways to understand what SPy is doing and why.
This note is written for Python developers who may never have encountered Zig. It does not ask “should I use Zig or SPy?” — these languages are not alternatives to each other. It asks: what can Zig teach us about SPy?
What is Zig?¶
Zig is what you get if you redesign C from scratch. It keeps C’s core value proposition —
direct control over memory, no garbage collector, predictable performance, no hidden
costs — while removing the most dangerous footguns: there are no null pointer
dereferences (null is an explicit ?T optional type), no implicit integer overflow
(arithmetic wraps or panics, never silently corrupts), no uninitialized variables passing
unnoticed, and no header-file macro system.
But Zig also adds one genuinely novel idea that C never had: comptime.
comptime is Zig’s mechanism for compile-time computation. In most languages,
metaprogramming requires a separate tool: C has the preprocessor, C++ has templates, Rust
has procedural macros. These are all different sub-languages with their own syntax and
rules. In Zig there is no such sub-language. Compile-time computation is written in
ordinary Zig. You can pass values marked comptime to functions, branch on types,
construct new types, and generate specialized code — all using the same language you
write at runtime. As Bun’s creator Jarred Sumner put it: “A lot of languages have
templating-like syntax. In Zig, it uses the same language — it’s just Zig.”
A concrete example: in Zig, the standard library’s ArrayList type is not a built-in
generic container. It is an ordinary function that takes a comptime type argument and
returns a struct specialized for that type. Generics are just functions.
This is the key idea to carry into the SPy discussion.
comptime and @blue: the same insight, different contexts¶
SPy’s @blue functions are the direct parallel to Zig’s comptime. Both are answers to
the same question: how do you give library authors the ability to write code that runs at
compile time, in the same language as runtime code, without a separate macro system?
In SPy, a @blue function is evaluated by the interpreter at compile time, with concrete
types fully known. This is what makes SPy’s operator dispatch work: when the compiler
sees a + b where both operands are i32, it calls a @blue function that resolves +
to i32_add — a direct integer addition with no runtime dispatch overhead. The generic
+ operator does not exist at runtime. This process is called redshifting.
The consequence for metaprogramming is significant. In Python, a factory function that
returns different function types depending on its argument will cause any static type
checker to give up and return Any. In SPy, because @blue functions are evaluated with
full type information at compile time, the types of the returned functions are fully
known:
@blue
def make_adder(y: dynamic):
T = type(y)
def add(x: T) -> T:
return x + y
return add
add5 = make_adder(5) # type: def(i32) -> i32 — fully known
add_w = make_adder(" w") # type: def(str) -> str — fully knownA Python type checker cannot follow this. SPy can, for the same structural reason Zig can: the metaprogramming code is executed at compile time, not merely analyzed.
The two mechanisms differ in scope. Zig’s comptime is primarily about code generation
and specialization. SPy’s @blue is also the mechanism for type inference and operator
dispatch — it is more deeply woven into the language’s semantics. But the underlying
insight is identical: make the compile-time layer a first-class part of the language, not
a bolted-on extra.
The safety model: an honest picture¶
Python developers coming to SPy often ask: is SPy safe? The most useful answer comes from placing SPy on a spectrum with C, Zig, and Rust.
C provides almost no systematic safety guarantees. Memory bugs are invisible until they cause crashes or security vulnerabilities, and there is no mechanism to prevent them by construction.
Rust sits at the other extreme. Its borrow checker tracks ownership and lifetimes of every value at compile time. Two parts of the program cannot mutate the same data simultaneously; memory is never freed while a live reference exists. These guarantees are verified statically, with zero runtime cost. They make Rust uniquely suited for operating systems, embedded firmware, and hard real-time systems — but at the cost of a steep learning curve and significant friction when writing unconventional memory patterns.
Zig and SPy occupy the same middle tier. Both remove the worst C footguns through runtime checks: bounds-checked slices, no implicit null, explicit uninitialized memory. But neither has a borrow checker. Use-after-free, use-after-realloc, and dangling interior pointers remain possible in both. In Zig’s debug mode and in SPy’s interpreted mode, additional runtime checks catch many of these problems. In release mode, both strip those checks for maximum performance — and maximum responsibility.
This is a conscious trade-off, not an oversight. SPy’s rationale, stated directly in its documentation, is that the borrow checker is Rust’s steepest learning curve, and that using a garbage collector for safe high-level code keeps SPy approachable to Python programmers while making CPython interoperability far simpler.
The honest scope: for server software, scientific computing, and data pipelines, a well-implemented GC is entirely adequate, as Go has demonstrated. SPy is not trying to replace Rust in Rust’s home territory — operating system kernels, embedded firmware, hard real-time systems. Those workloads are also workloads where Python-like ergonomics would feel alien.
WASM: a shared first-class target¶
Both Zig and SPy treat WebAssembly as a genuine compilation target, not an afterthought. But they use it differently.
For Zig, WASM is primarily a deployment target: small binaries, fast startup, sandboxing, edge and browser portability. Zig produces very compact WASM modules with no runtime dependency, which makes it attractive for serverless functions and browser-side code.
For SPy, WASM serves an additional and more unusual role: it is the interpreter
substrate. In development mode, SPy runs libspy.wasm inside the Python process via
wasmtime. This architectural choice buys sandboxing of unsafe code (a crash in
libspy.wasm becomes a Python exception rather than a process segfault), isolated VM
instances, and portability — the same artefact that gets deployed to edge and browser
runtimes is the one running inside the development interpreter.
There is also a practical connection: Zig is a dependency of the SPy Python package, used
as a hermetic cross-compiling C compiler (zig cc) to compile SPy’s C backend output.
This is one of Zig’s most immediately useful real-world features — zig cc as a drop-in
replacement for a C compiler, with built-in cross-compilation — and SPy takes direct
advantage of it.
C interop: a solved problem in Zig, an open question in SPy¶
One of Zig’s most practical features is @cImport. Given a C header file, you can write:
const c = @cImport({
@cInclude("sqlite3.h");
});and call c.sqlite3_open(...) directly, with Zig types automatically generated from the
C header at compile time. There is no separate binding-generation step, no manual
transcription of structs. The C header is the interface definition. This is a comptime
operation: Zig’s compile-time layer runs a C parser and type translator.
SPy will need something equivalent — the roadmap explicitly references @cImport as
related work. But as of 2026, “CFFI for SPy” is scheduled for early experiments, not a
finished design. The challenge is harder for SPy than for Zig because SPy must make C
interop work in two modes: in the compiler (where zero-overhead static dispatch is
straightforward) and in the WASM-based interpreter (where calling a native C function
means crossing a sandbox boundary, which is non-trivial).
This is one of the most consequential open questions in SPy’s roadmap. Nearly every
high-performance Python library ultimately wraps C or Fortran. A clean @cImport-style
mechanism would be the foundation for reimplementing those libraries in idiomatic SPy —
which is precisely the “Pythonic C++” vision that motivates the project.
Maturity: the most important asymmetry¶
Any honest comparison must acknowledge that Zig and SPy are at very different stages.
Zig is a mature language with a stable release, a production-quality compiler, a
comprehensive standard library, and real-world adoption in projects like Bun and
TigerBeetle. Its design questions — how comptime works, how errors are handled, how C
interop is structured — have been answered, debated, and settled. You can use it today
for serious work.
SPy is a research prototype in alpha. It can be tried by Python developers to do real things, and part of SPy’s own development is now written in SPy itself — which is a meaningful milestone. But many of the most consequential design decisions are still open. Error handling in compiled and WASM mode is not yet fully specified. C interop is pre-experimental. The packaging and distribution story does not exist yet. The language Antonio Cuni ships in 2027 or 2028 may differ significantly from what is describable today.
This asymmetry should not discourage interest in SPy — quite the opposite. It means that the design space is still open and that contributions can shape the language. But it does mean that comparisons like this one are comparing a language that has answered its questions with one that is still asking them. The conceptual parallels are real and illuminating. The implementation maturity is not yet comparable.
Summary¶
Zig is useful for understanding SPy not because the two languages compete, but because
they share a core insight: compile-time computation should be a first-class part of the
language, written in the same language as runtime code. comptime and @blue are
independent discoveries of the same idea, applied to different audiences and different
problems.
Beyond that parallel, Zig and SPy occupy the same honest tier on the memory-safety spectrum — meaningfully safer than C, without Rust’s systematic compile-time guarantees — and both treat WebAssembly as a genuine target rather than an afterthought.
The differences are equally real. Zig is a systems language with no GC, designed for programmers who want a better C. SPy is a Python companion language with a GC, designed for programmers who want a better Python. Zig is production-ready; SPy is a promising research prototype. And C interop, one of Zig’s most practical strengths, remains one of SPy’s most important open questions.
The right mental model is not “Zig vs SPy” but rather: Zig proves that the comptime
idea is powerful enough to replace generics and macros in a systems language. SPy asks
what the same insight means for a language whose users think in Python.