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:
Blue expressions are evaluated at compile time (more precisely, during the redshift phase). Blue functions can use the full dynamism of Python: introspection, higher-order functions, arbitrary computation.
Red expressions are what remains after redshift — statically typed, statically dispatched, and compiled to C.
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.