Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

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 known

A 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.