. ¶
About these notes
This post was generated by a LLM (generative AI) from an exploratory conversation. I guided the discussion, challenged the AI’s conclusions, and verified the technical content.
SPy is a statically typed, compiled variant of Python with a focus on performance. A natural question when encountering it is: how does it compare to Rust? Both languages aim at C-level performance with stronger safety guarantees than C. But this framing is misleading. SPy’s goal is not to replicate Rust in Python syntax. Its goal is to be the best possible companion language for Python — and those are very different targets.
This post explores that distinction, drawing on SPy’s design documents and blog posts. The central claim is:
SPy dramatically improves on Python’s type system weaknesses, and it can incorporate good ideas from Rust — but it neither needs nor wants to be Rust.
What makes Rust’s type system exceptional ¶
Before comparing, it helps to be precise about what Rust actually offers.
Compile-time enforcement with no escape hatches. Types are checked before the program runs, and there is no implicit way to bypass this. Every conversion is explicit.
Algebraic data types.
Rust enums can carry data:
Option<T>
,
Result<T,
E>
. The
compiler enforces exhaustive pattern matching — you
cannot
use a value that might be
None
without first handling that case. This eliminates an entire class of null-pointer
bugs.
The borrow checker. The type system tracks ownership and lifetimes of every value at compile time. No two parts of the program can mutate the same data simultaneously. Memory is never freed while a reference to it is still alive. All of this is verified statically, with zero runtime cost.
Traits with bounds.
Generic functions declare their constraints upfront:
fn
add<T:
Add>(a:
T,
b:
T)
->
T
documents and enforces that
T
must support addition.
The compiler checks the function body
in isolation
, without needing to know what
T
will be at the call site.
Zero-cost generics. Generics are monomorphized at compile time — a separate concrete version is generated for each type used. No runtime dispatch overhead.
These features make Rust suitable for operating systems, embedded firmware, hard real-time systems, and any domain where predictable latency and absence of a garbage collector are mandatory.
Python’s type system: the pain points ¶
Python’s approach is almost the opposite.
Type annotations exist but are
ignored at runtime
by design.
x:
int
=
"hello"
raises no error in Python — it is merely metadata for optional external tools. Type
checkers like mypy and pyright are add-ons, not part of the language.
Dynamic dispatch is always on.
a
+
b
in Python always performs a runtime lookup of
__add__
, regardless of whether the types are statically known.
dynamic
is the default, not the exception.
Everything is dynamically typed unless
you opt in to annotations, and even then the runtime doesn’t enforce them.
Metaprogramming breaks type checkers.
When a function uses decorators, closures that
return types, or other patterns that construct types at runtime, mypy typically infers
the result as
Any
and stops checking. You lose type safety for exactly the most
powerful Python patterns.
Any
is a silent hole.
Once a value is typed
Any
, type checking stops propagating
through it entirely —
and there is no warning that this happened.
How SPy improves the situation ¶
SPy directly addresses each of these weaknesses.
Annotations are enforced ¶
The most immediate departure from Python: in SPy, type annotations are enforced. Always. Running:
def main() -> None:
x: int = "hello"
raises a real
TypeError
with a precise error message:
TypeError: mismatched types
| x: int = "hello"
| |_____| expected `i32`, got `str`
This is not a linter warning. It happens at runtime in interpreted mode and at compile time in compiled mode.
Static operator dispatch via redshifting ¶
SPy introduces
redshifting
— a compile-time partial evaluation pass that resolves all
type-dependent operations before runtime. After redshifting,
a
+
b
for two integers
becomes a direct call to
i32_add
; for two strings it becomes
str_add
. The generic
operator no longer exists at runtime.
This has a crucial consequence: a type mismatch like
1
+
"hello"
is caught at compile
time, not at runtime, because the resolution of
+
is a
@blue
(compile-time) function
that raises a
TypeError
when it cannot find a valid implementation for the given types.
Dynamic dispatch is opt-in ¶
In Python, everything is dynamic and you opt in to static typing. SPy inverts this. Code
is statically typed by default. If you genuinely need dynamic dispatch, you annotate with
the explicit
dynamic
type:
def add(x: dynamic, y: dynamic) -> dynamic:
return x + y
The rationale, stated directly in the SPy documentation, is that dynamic dispatch “is costly and prevents many other optimizations.” Making it explicit makes the cost visible and intentional.
Metaprogramming is fully type-safe ¶
This is SPy’s most distinctive improvement over Python’s type situation. In Python, a
make_adder
factory that returns different function types depending on its argument will
cause mypy to give up and return
Any
. In SPy,
@blue
functions are evaluated by the
interpreter at compile time, with the concrete types fully known. The type checker is
aware of this:
@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
There is no
Any
leakage. The compiler knows the precise type of
add5
and
add_w
because it evaluated
make_adder
itself. If you then write
add5
+
"hello"
, you get a
compile-time
TypeError
— adding a function and a string — with no special support
needed.
This is a qualitative improvement over any Python type checker, not merely a quantitative
one. The fundamental reason Python checkers fail here is architectural: they analyse code
without running it. SPy’s
@blue
mechanism runs the metaprogramming code at compile
time, which is why typechecking can follow it.
Compile-time errors are programmable ¶
Because type checking flows through the same
@blue
mechanism as everything else,
library authors can raise
StaticError
from any
@blue
function to produce compile-time
errors with domain-specific messages:
@blue.generic
def SmallString(size: int):
if size > 32:
raise StaticError("SmallString must be at most 32 chars")
return str
def say_hello(name: SmallString[64]) -> None: # fails at build time
print(name)
This is a capability Python has no equivalent for.
Where SPy deliberately diverges from Rust ¶
Garbage collection instead of a borrow checker ¶
SPy’s memory model is explicitly two-level:
-
Safe high-level code uses a garbage collector. Memory is managed automatically; lifetimes are not tracked by the type system.
-
Low-level
unsafecode uses manual allocation (raw_alloc,raw_free) with no borrow checker — essentially C with Python syntax.
This is the single largest divergence from Rust, and it is a conscious trade-off, not an oversight.
What is lost: Hard real-time guarantees are impossible with a GC, since collection pauses can occur at any time. SPy cannot target operating system kernels, embedded firmware, or hard real-time audio pipelines — Rust’s home territory.
What is gained: The borrow checker is Rust’s steepest learning curve by far. By using a GC, SPy remains approachable to Python programmers. It also makes Python interoperability far simpler, since CPython objects are themselves GC-managed.
The honest scope: The workloads where a GC is disqualifying are also workloads where Python-like ergonomics would feel alien. SPy is not trying to replace Rust in Rust’s home territory. For server software, scientific computing, data pipelines, and most application code, a well-implemented GC is entirely adequate — as Go has demonstrated.
No borrow checker in
unsafe
code either
¶
In
unsafe
SPy, the programmer is responsible for avoiding use-after-free and
out-of-bounds access. The interpreter and debug compiled mode add runtime bounds checks
(pointers track their array length as a fat pointer). Release mode strips these checks
and emits plain C pointers — exactly as fast as C, and exactly as unguarded.
The CPython parallel is intentional: CPython’s C internals are unsafe, yet most Python
users never touch them. SPy’s
unsafe
layer is for library authors, not end users.
Features SPy could take from Rust ¶
Two Rust features are absent from SPy today but would fit its design naturally.
Algebraic data types ¶
Python 3.10 introduced
match
statements and union types (
int
|
str
|
None
). The
concept is already in the Python vocabulary. The question for SPy is whether to
enforce
exhaustive matching statically — and this hinges on a single design decision: whether
None
should be opt-in rather than universally valid.
In Rust, you cannot use a value that might be
None
without pattern matching it first,
because there is no implicit null. If SPy made nullability explicit — only
int
|
None
can be
None
, not bare
int
— then the static enforcement of pattern matching would
provide real safety guarantees. This would be a meaningful break from Python semantics,
but SPy already accepts breaking compatibility where the benefit is clear.
ADTs are compatible with having a GC : they are purely a type-system feature. Haskell has one of the most powerful ADT systems of any language, and it is garbage-collected.
The main design question is cultural: Python programmers are accustomed to
None
appearing anywhere. Making it opt-in would take adjustment. But it is technically clean
and would eliminate a large category of
AttributeError:
'NoneType'
object
has
no
attribute
'...'
bugs that Python programmers
encounter daily.
Traits as structural protocols ¶
Python already has
typing.Protocol
(PEP 544): a structural interface mechanism that
type checkers understand. This is the natural SPy equivalent of Rust traits, and it would
be far more Pythonic than Rust’s trait syntax.
SPy’s
@blue
operator dispatch already
implicitly
checks trait-like constraints. If
you pass a type to a generic
@blue
function and that type does not support the required
operations, you get a
StaticError
at compile time. What is missing is the ability to
declare
the constraint upfront:
# Hypothetical SPy syntax, following Python's Protocol
from typing import Protocol
class Addable(Protocol):
def __add__(self, other: Self) -> Self: ...
@blue.generic
def add(T: type[Addable]): # constraint declared, not discovered
def impl(a: T, b: T) -> T:
return a + b
return impl
The benefit is twofold: the API is self-documenting, and errors surface at the call site with a clear message (“T does not implement Addable”) rather than somewhere inside the function body.
The full Rust trait system — with associated types, blanket implementations, trait
objects, and the orphan rule — would be both overkill and un-Pythonic. But a lightweight
Protocol
-based constraint system would fit SPy naturally and significantly improve the
library authoring experience.
Crucially, like ADTs, traits are orthogonal to memory management . A GC-based language can have an excellent trait/protocol system.
Summary ¶
SPy is not Rust-for-Python-programmers. It is a statically typed Python variant that makes deliberate trade-offs: a GC instead of a borrow checker, simplicity instead of maximal safety. These choices make it unsuitable for the domains where Rust excels (systems programming, hard real-time, bare metal). But for the domains where Python is already used — and where the demand for performance is growing — SPy’s approach is coherent and well-targeted.
The improvements over Python are substantial and architectural, not cosmetic:
-
Annotations are enforced, always.
-
Operator dispatch is resolved at compile time.
-
Dynamic typing is an explicit opt-in, not the default.
-
Metaprogramming is fully type-safe through
@blueevaluation — a capability no Python type checker can match.
And the features it does not yet have — algebraic data types and trait-style protocol bounds — are not blocked by the GC choice. They are type-system features, orthogonal to memory management, and they fit SPy’s Pythonic direction well. The question is not whether they make sense for SPy, but when.
The right mental model for SPy is not “Rust with Python syntax” but rather “what if Python’s type system were taken seriously from the start?” That turns out to be a genuinely different language — and a promising one.