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.

SPy and Go: a comparison

Go and SPy share a surprising amount of philosophy: both rely on a garbage collector rather than an ownership model, both aim for simplicity and approachability, and both produce fast native binaries without requiring the programmer to reason about memory lifetimes. The analogy is useful — SPy can reasonably be described as Go with a Pythonic syntax — but the two languages have quite different design goals, compilation models, and target audiences. This note unpacks the similarities and the differences.

What they share

The deepest similarity is the choice to use a garbage collector rather than an ownership-and-borrowing system à la Rust or Mojo. Both languages accept a small runtime cost in exchange for a dramatically simpler programming model. The programmer does not need to annotate lifetimes, think about moves, or manage reference counts explicitly. This is a deliberate trade-off: some performance is left on the table, but the language stays approachable.

Both languages also favour simplicity of implementation as a first-class goal. Go’s specification is famously small and its compiler straightforward. SPy shares this instinct — Antonio Cuni has repeatedly stated that keeping the implementation simple and understandable is a core objective, and that the language should eventually be able to implement its own compiler and interpreter.

Finally, both produce fast native binaries and support deployment without a heavy runtime. A compiled SPy program, like a Go binary, can be dropped onto a target machine with no interpreter or virtual machine required.

Where SPy goes further

Blue/red and compile-time metaprogramming

This is SPy’s most distinctive feature and has no equivalent in Go. SPy operates at two levels simultaneously:

Go’s generics (introduced in 1.18) allow type-parameterised functions and data structures, but they are relatively constrained: type parameters are resolved at compile time, but there is no mechanism for arbitrary compile-time computation. SPy’s blue layer is much closer in power to C++ templates, Zig’s comptime, or D’s static if — but expressed in Python syntax. This enables patterns such as loop fusion (see Array computing like C++: expression templates and SIMD) that would require C++ expression templates in Go or a similar compiled language.

The interpreter and debugger

Go is always compiled. There is no Go interpreter, and debugging Go code typically means working at the level of the compiled binary. SPy, by contrast, ships both an interpreter and a compiler as first-class components. The interpreter provides a normal Python-like development loop — fast iteration, immediate feedback, notebook compatibility — while the compiler produces native-speed binaries when needed. The debugger (spdb) operates at the SPy source level, not at the C or assembly level. This matters enormously for scientific computing, where exploratory development in an interactive environment is the norm.

WASM as a first-class target

SPy’s architecture is built around WebAssembly from the ground up. The interpreter itself loads libspy.wasm via wasmtime, and Emscripten (browser) and WASI (portable standalone) are first-class compilation targets. The compiled browser demos total around 91 KB — orders of magnitude smaller than a PyScript or Pyodide deployment. Go has WASM support, but it was added after the fact and Go’s WASM binaries are notoriously large due to the runtime they bundle.

Python ecosystem integration

SPy is designed from the start to interoperate with Python: the long-term goal is that SPy libraries can be imported from Python and Python libraries can be imported from SPy. Go has no analogous relationship with any dynamic language. For a Python developer, this means SPy is not a replacement for Python but a companion — the two can coexist in the same project, with SPy handling the performance-critical parts and Python handling everything else.

Scientific computing as a target

Go was designed for networked server infrastructure. SPy explicitly targets scientific and numerical computing: array libraries, loop fusion, SIMD-friendly code generation, and eventually NumSPy (a reimplementation of the Python Array API standard in SPy). This focus shapes the language’s priorities in ways that Go does not share.

Where Go is ahead (for now)

This comparison would be incomplete without acknowledging what Go has that SPy does not — at least not yet.

Maturity and ecosystem. Go is a production language with over a decade of real-world use, a comprehensive standard library, stable tooling, and a large community. SPy is between alpha and beta. Its standard library is minimal, several important language features are still on the roadmap (try/except, context managers, heap-allocated classes, SPy/C integration), and breaking changes are still possible.

Concurrency. Goroutines and channels are central to Go’s identity. SPy has no concurrency model yet.

Error handling. Go’s explicit error returns are opinionated, but they enforce a consistent discipline. SPy currently has no try/except — all raise statements are compiled to panics.

Compilation backend. Go ships its own compiler backend and produces binaries directly. SPy compiles to C and relies on an external C compiler (gcc, clang, emcc). This is mostly a strength — SPy inherits decades of C compiler optimisation (LTO, PGO, auto-vectorisation) essentially for free — but it introduces a dependency on the C toolchain and gives SPy less control over low-level code generation.

Value semantics and memory model

Both languages use a GC for heap-allocated objects, but their value semantics differ in an important way today. In SPy, @struct instances are stack-allocated and passed by copy. There are no shared references to a stack struct, so SPy enforces immutability on them — mutation would be invisible to callers and therefore misleading. Heap allocation is explicit (gc_alloc, raw_alloc) and gives reference semantics through a pointer.

Go, by contrast, allows structs to be passed either by value or by pointer, and mutable pointer receivers are idiomatic. The programmer chooses; the language does not enforce immutability.

Once SPy gains heap-allocated classes (on the roadmap), its model will converge toward Python’s — objects on the heap with reference semantics, passed by sharing. At that point the main remaining difference from Go’s memory model will be the absence of an ownership/escape-analysis system (Go’s compiler does escape analysis to decide whether to stack- or heap-allocate; SPy’s story here is still evolving).

Summary

Aspect

Go

SPy

Memory management

GC (tracing)

GC (Boehm) + raw alloc

Value semantics

By value or pointer (programmer’s choice)

Structs by copy; heap objects by reference (future)

Compile-time metaprogramming

Limited generics

Full blue/red system

Interpreter

Debugger

C/binary level

SPy source level

WASM support

Added later, large binaries

First-class, small binaries

Concurrency

Goroutines + channels

Not yet

Error handling

Explicit error returns

Panics only (for now)

Compilation backend

Own backend

Compiles to C

Python interop

Planned (core goal)

Scientific computing focus

Maturity

Production

Alpha/beta

The Go analogy captures something real about SPy’s philosophy — simplicity, GC, approachability, fast binaries — but undersells what makes SPy novel: the blue/red distinction, the interpreter, the WASM-centric design, and the deep integration with the Python ecosystem. SPy is not trying to be Go for Python developers. It is trying to be the companion language that Python itself has always needed.