SPy πŸ₯Έ & the Python Native Interface




Will Python keep its edge in the next decade?



Pierre Augier

JDEV 2026

Who am I? Pierre Augier

CNRS researcher in fluid mechanics β€” LEGI, Grenoble


  • Co-author of the draft Python Native Interface (Py-NI) PEP
  • Active contributor to SPy
  • Author of a Jupyter Book on SPy

The Big Picture

Python in 2026

Why Python dominates

  • Scientific computing, data science, ML
  • Unmatched ecosystem (NumPy, SciPy, PyTorch, …)
  • Wonderful developer experience
  • Massive community

But Python has real structural problems.

Where Python falls short

  • 🌐 Browser deployment
  • πŸ“¦ Small self-contained binaries
  • ⚑ Low-level performance
  • πŸš€ Fast startup time
  • πŸ”¬ Microcontrollers / edge computing
  • 🏷️ Typing as good as static languages

Without structural improvements, Python risks losing ground β€” new projects in critical domains may start elsewhere.

The Two-Language Wall

Python provides the developer experience; C/C++/Fortran/Rust provides the performance.

  • NumPy, SciPy, PyTorch, Polars: fast core + Python interface
  • Inconvenient β€” but manageable. Cython, cffi, PyO3 help.
  • Not the deepest problem.

The Python C API: the foundation and the bottleneck

The C API exposes CPython internals β€” refcounts, object layout.

Consequences:

  • Extensions are coupled to CPython β€” PyPy and GraalPy struggle to run the ecosystem
  • CPython cannot restructure its own internals without breaking thousands of extensions
  • CPython’s JIT is constrained by what the C API promises

Note

CPython is still significantly slower than PyPy and GraalPy for pure Python code β€” yet the ecosystem’s dependency on the C API prevents most projects from switching.

This is not a developer experience problem. It is a structural problem.

πŸŽ™οΈ These issues are discussed in depth in my PyConFr 2025 talk: Python Native Interface for faster and stronger Python

Current Solutions and Their Limits

Tool Approach Limits
Cython Hybrid C/Python syntax No interpreter, no standalone binary, C feel
Pythran AOT compile annotated Python Limited scope, no interactive dev
Numba / JAX JIT compilation Incompatible with AOT tools
PyPy / GraalPy Full meta-JIT Can’t run C extensions well
CPython JIT Copy-and-patch Blocked by C API constraints
Julia Other language Leave the Python ecosystem
Mojo Python-inspired Leave the Python ecosystem, closed source (compiler)

No existing tool lets you write something like NumPy without direct use of the Python C API.

Julia and Mojo are excellent individual choices β€” but they don’t fix Python’s ecosystem. NumPy, SciPy, the notebooks, the researchers: they stay in Python/C. Real fixes must come from within.

Two Structural Remedies

1. Python Native Interface (Py-NI)

A modern C API and universal ABI for CPython extensions.

  • Extensions built against Py-NI run across CPython versions and alternative runtimes without recompilation
  • Unlocks CPython to restructure internals (JIT, GC, …)
  • Draft PEP: github.com/py-ni/peps


2. A new companion compilable language

  • Py-NI alone does not provide a good tool to implement NumPy-like libraries without writing C
  • We need a Pythonic language that compiles to native code
  • This is where SPy πŸ₯Έ comes in

I presented Py-NI and related Python C API issues at PyConFr 2025 β€” for this talk, let’s focus on SPy.

SPy πŸ₯Έ: A New Companion Language for Python

What is SPy?

SPy πŸ₯Έ is a 🚧 new 🚧 language, created by Antonio Cuni (PyPy, PyScript, Anaconda).

SPy is a variant of Python specifically designed to be statically compilable while retaining a lot of the β€œuseful” dynamic parts of Python.

SPy has:

  1. An interpreter β€” usual Python development experience
  2. A debugger β€” spdb, works at the SPy level
  3. A compiler β€” for native speed

SPy is designed to become a companion language for Python and its community.

SPy is not

  • an efficient Python interpreter
  • another acceleration tool for Python code

SPy Design Goals (Antonio Cuni)

  1. Easy to use and to implement
  2. Has an interpreter
  3. Has a compiler
  4. Static typing
  5. Performance matters
  1. Predictable performance
  2. Rich metaprogramming capabilities
  3. Zero-cost abstractions
  4. Opt-in dynamism
  5. One language, two levels

Consequences

  • Simple β€” very close to Python
  • But also notable differences with Python

β‡’ SPy is a Python variant.

SPy Name β€” Origin

  • Static Python

  • β€œS” is the letter after R

    Successor of RPython (used to implement PyPy)

    RPython is a restricted subset of Python (2.7) that is amenable to static analysis.

SPy is a direct consequence (and the continuation) of the PyPy project.

SPy Design β€” Simple and similar to Python, but …

  • Easy to learn and use for Python developers
  • Simple implementation

Choices:

  • Memory management: Garbage Collector (GC)

  • The compiler is a transpiler to C

  • On many aspects, just like Python:

    Functions, for/while, range(), slice(), print(), open(), tuple, list, dict, decorator, …

    Not yet supported but soon: f-strings, try/except, with

Also quite fundamental differences and novelties

  • First: β€œstatic” β‡’ type annotations for β€œred” functions & proper type errors

    def add(x: int, y: int) -> int:
        return x + y
  • …

Under the Hood β€” Simple, Clean and Robust

Currently implemented in:

  • Python (CLI, interpreter, transpiler) β€” later in SPy
  • A small C runtime library (libspy)
  • Stdlib types (list, dict, tuple) written in SPy itself

WASM-centric architecture:

WebAssembly (WASM) is a binary instruction format designed to run at near-native speed in a sandboxed environment.

  • Interpreter loads libspy.wasm via wasmtime
  • Same libspy is statically linked in compiled executables
  • Works on top of Pyodide (in-browser) without wasmtime

SPy Design β€” Two Levels

High level / safe

Typically what most Python users use.




Low level / unsafe / advanced metaprogramming

For library developers and advanced users

from unsafe import gc_alloc, raw_alloc

# GC allocation + unsafe pointer
ptr = gc_alloc[i32](10)

# manual memory management
ptr_raw = raw_alloc[f64](100)



In CPython, low level means C (or specified and limited tools). In SPy, low level is still SPy.

SPy Design β€” Two Execution Times

  • Compile time

    Dynamic

    Novel compilation phases: β€œImports” and β€œRedshift”

  • Run time

    Static & fully typed

β‡’ differences with Python

  • semantics for module level code (executed at import time)

  • main() function

    def main() -> None:
        print("Hello, World!")
  • Blue/Red expressions & @blue functions

The Compilation Pipeline

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '20px', 'edgeLabelBackground': 'transparent'}, 'flowchart': {'rankSpacing': 20, 'nodeSpacing': 20, 'padding': 0}}}%%
graph LR
    SRC["*.spy source"]
    MOD["Untyped AST<br/>+ symtable<br/>+ modules"]
    RED["Typed AST<br/>(Redshifted)"]
    C["C source (.c)"]
    OUT1(["output"])
    OUT2(["output"])
    NAT["native exe"]
    WASI["WASI exe"]
    EM["Emscripten"]
    OUT3(["output"])

    SRC -->|parse<br/>+ scope analysis<br/>+ import| MOD
    MOD -->|redshift<br/>+ type checking| RED
    MOD -->|interp.| OUT1
    RED -->|interp.| OUT2
    RED -->|cwrite| C
    C -->|cc| NAT
    C -->|cc| WASI
    C -->|cc| EM
    NAT --> OUT3
    WASI --> OUT3
    EM --> OUT3

    classDef output fill:#1D9E75,stroke:#0F6E56,color:#E1F5EE
    classDef exe    fill:#BA7517,stroke:#854F0B,color:#FAEEDA
    classDef src    fill:#534AB7,stroke:#3C3489,color:#EEEDFE
    classDef ast    fill:#185FA5,stroke:#0C447C,color:#E6F1FB
    classDef c      fill:#993556,stroke:#72243E,color:#FBEAF0

    class OUT1,OUT2,OUT3 output
    class NAT,WASI,EM exe
    class SRC src
    class MOD,RED ast
    class C c

AST: Abstract Syntax Tree β€” a structured representation of the source code used by compilers and interpreters.


Commands to inspect the pipeline:

spy pyparse hello.spy     # Python AST
spy parse hello.spy       # SPy AST
spy colorize hello.spy    # blue/red expressions
spy symtable hello.spy
spy imports hello.spy     # recursive list of imports
spy redshift hello.spy    # typed AST

Commands to run SPy code:

spy hello.spy             # interpret the untyped AST
spy redshift -x hello.spy # interpret the typed AST
spy build -x hello.spy    # native build and execute (fast)

Build for an alternative target:

spy build --target emscripten hello.spy

Core Innovation: the Blue/Red Distinction and Redshifting

πŸ”΅ Blue expressions & functions

  • Values known at compile time

  • Functions can be annotated as @blue:

    • run (interpreted) at compile time
    • do not need to be annotated


redshift phase (compile time)

  • Full Python dynamism available
  • Partial evaluation of blue expressions
  • Base of the type system
  • Concretize generic types
  • Static dispatch & type checking

πŸ”΄ + πŸ”΅ redshifted typed AST

  • Executed at run time
  • What goes into the compiled binary
  • Fully static / no Python dynamism
  • Statically typed and dispatched

Example Blue/Red and Redshifting

@blue.generic: a @blue functions that has to be called with brackets

adder.spy

@blue.generic
def add(T):
    def impl(x: T, y: T) -> T:
        return x + y
    return impl

def main() -> None:
    add_i32 = add[i32]      # evaluated at compile time
    result = add_i32(3, 4)  # fast red execution at runtime
    result_f = add[f64](3.0, 4.0)


adder2.spy (syntactic sugar)

def add[T](x: T, y: T) -> T:
    return x + y

def main() -> None:
    add_i32 = add[i32]       # evaluated at compile time
    result = add_i32(3, 4)   # fast red execution at runtime
    result_f = add[f64](3.0, 4.0)

Example Blue/Red and Redshifting

Result of spy colorize adder2.spy

def add[T](x: T, y: T) -> T:
    return x + y

def main() -> None:
    add_i32 = add[i32]       # evaluated at compile time
    result = add_i32(3, 4)   # fast red execution at runtime
    result_f = add[f64](3.0, 4.0)

Result of spy redshift adder2.spy

def main() -> None:
    result: i32 = `add[i32]`(3, 4)
    result_f: `f64` = `add[f64]`(3.0, 4.0)

def `add[i32]`(x: i32, y: i32) -> i32:
    return `operator::i32_add`(x, y)

def `add[f64]`(x: `f64`, y: `f64`) -> `f64`:
    return `operator::f64_add`(x, y)

SPy in the Landscape of Compiled Languages

SPy πŸ₯Έ is a proper language, that should be compared with other languages!

C++ Rust Mojo Go SPy
Memory model Manual Ownership Ownership GC GC + Manual
Interpreter ❌ ❌ βœ… ❌ βœ…
Pythonic syntax ❌ ❌ βœ… ❌ βœ…
Python interop via bindings via bindings βœ… ❌ βœ… (goal)
Compile-time metaprogramming templates macros Parameters/comptime/alias limited blue/red
WASM first-class ❌ partial ❌ partial βœ…
Status mature mature beta mature alpha

SPy versus Mojo

  • Mojo: Python-like syntax with C++/Rust-level control β€” ownership, lifetimes, MLIR backend.

    A replacement strategy: you leave Python’s world for a more powerful but far more complex language.

  • SPy: stay close to Python, keep the implementation approachable, use a GC.

    Companion language that Python and its community can actually adopt.

Note: Blue/red is similar to Zig’s comptime β€” compile-time execution of the same language, enabling zero-cost generic programming.

SPy πŸ₯Έ Quick Syntax Tour

(focussed on differences with Python)

Programs and main function

hello world

def main() -> None:
    print("Hello, World!")

argv

def main(argv: list[str]) -> None:
    print(argv[0])

return value

def main() -> i32:
    print(1)

Module level code frozen after import

Module level code very limited (definitions and assignments).

Global variables are by default constant but…

New var keyword

# global variable declared `var`
var my_var: i32 = 2

def print_my_var() -> None:
    print("my_var:", my_var)

def main() -> None:
    print_my_var()
    my_var = 10
    print_my_var()

Output

my_var: 2
my_var: 10

Local blue constant with const

New const keyword

# module level variables are constant and blue
MY_CONST = 2

def main() -> None:
    # one can also declare local constant
    const MY_LOCAL_CONSTANT = 42
    print(MY_CONST + MY_LOCAL_CONSTANT)

    my_var0 = 2
    print(MY_CONST + my_var0)

    my_var1 = 0
    my_var1 = 1
    print(MY_CONST + my_var1)

Result of spy redshift const.spy

def main() -> None:
    `_print::println[str]`('44')
    `_print::println[str]`('4')
    my_var1: i32 = 0
    my_var1 = 1
    `_print::println[i32]`(`operator::i32_add`(2, my_var1))

Structs (like C and Rust structs)

@struct
class Point:
    x: f64
    y: f64

    def length(self) -> f64:
        return (self.x**2 + self.y**2)**0.5
  • __new__, __make__
  • special methods (e.g. for operator dispatch with __add__ and co.)
  • @classmethod, @staticmethod, @properties, class variables

immutable/mutable

  • immutable when defined on the stack (Point(1, 2))
  • mutable with heap allocation (gc_alloc[Point](1))

Note

  • currently, all arguments passed by values (copied)
  • currently, no Python like classes (β€œheap allocated”) & no heritage

Generic functions and structs

Generic functions ( @blue.generic) defined with parameters in []

def dot[T](a: T, b: T) -> T:
    return a.x*b.x + a.y*b.y

Same for generic types (structs)

@struct
class NdArray[DTYPE, NDIM]:
    ...

Example:

from unsafe import gc_alloc, gc_ptr

@struct
class ArrayData[DTYPE]:
    length: i32
    capacity: i32
    items: gc_ptr[DTYPE]

@struct
class Array1d[DTYPE]:
    __ll__: gc_ptr[ArrayData[DTYPE]]

    def __new__(length: i32) -> Self:
        ll = gc_alloc[ArrayData[DTYPE]](1)
        ll.length = length
        ll.capacity = length
        ll.items = gc_alloc[DTYPE](length)
        return Self.__make__(ll)

Advanced SPy

Very interesting but no time for that here!

See Antonio’s presentation at PyConIt 2026!

  • @blue.metafunc and (variadic) meta-args

  • @force_inline

  • __convert_from__, __convert_to__

  • unroll for blue iterators

Note

  • Advanced metaprogramming without macros.
  • Used in the stdlib to implement list, tuple, dict, array, print, …

SPy πŸ₯Έ in Action: Three Demos


  1. Server on AWS Lambda
  2. SPy in the Browser
  3. Array computing with loop fusions

Demo 1 β€” SPy serving web requests on AWS Lambda

Antonio Cuni’s demo: github.com/spylang/demos/tree/main/aws-lambda

  • Python-like ergonomics (like FastAPI)
  • Startup time and performance matching Go or Rust (no Python runtime to initialise)

This is the promise of SPy: write it like Python, deploy it like Go.

Demo 2 β€” SPy in the Browser (JSFFI, PR #484)

No PyScript, no Pyodide, no hand-written JavaScript.

Two browser demos:

  • particles β€” 60fps bouncing particle simulation canvas 2D API: arc, fillRect, createRadialGradient
  • image_data β€” colour animation driven by RGB sliders rendered via putImageData

⚑ Performance & tiny binaries

Compiled binary sizes:

File Size
.wasm ~27 KB
Emscripten glue .mjs ~64 KB
Total ~91 KB

Compare to:

  • PyScript/Pyodide: ~10 MB
  • A typical webpage image: often larger

Demo 3 β€” Loop Fusion

Efficient array computing Γ  la NumPy/Pythran, written in SPy.

  • Implement minimal array libraries with loop fusion

  • Multiple operations on an array merged into a single pass

    # User writes this:
    result = 2 * x + sin(x)
    
    # SPy generates a single fused loop:
    # for i in range(n): result[i] = 2 * x[i] + sin(x[i])
  • Avoids intermediate allocations

  • Enables the C compiler to use SIMD instructions

  • Same technique as Pythran with C++ expression templates

Advanced SPy code: requires blue.metafunc, __convert_from__, @force_inline, …

SPy’s Impact on the Python Ecosystem

What a Mature SPy Could Enable

Six major use cases:

  1. NumSPy β€” Python-native scientific packages
  2. A better Cython / Pythran β€” wrapping and numerical kernels
  3. Fast native entry points β€” small binaries, fast startup
  4. A better MicroPython β€” Python on microcontrollers and edge devices
  5. Large applications β€” fast prototyping, speed & production robustness
  6. A better RPython β€” implementing runtimes and interpreters

1. NumSPy: The Core Vision

Write packages like NumPy in SPy β€” not C with a Python wrapper.

NumSPy (planned) = reimplementation of the Python Array API standard in SPy.

  • Whole-program optimisation. C compiler sees both app logic and array library.
  • Not coupled to CPython internals. With Py-NI’s universal ABI: runs unchanged on any CPython version, PyPy, GraalPy, any conforming runtime.

Unlocks CPython to evolve. Less ecosystem coupling to C API internals

  • β‡’ more freedom for CPython developers
  • β‡’ faster CPython

2. A Better Cython

Cython SPy
Syntax Hybrid C/Python Python-like
Output Python extensions only Extensions + native + WASM
Standalone binary ❌ βœ…
Developer experience Develop β†’ compile β†’ test Develop β†’ interpret β†’ compile
Interpreter ❌ βœ…
Debugger Via gdb (C level) spdb (SPy level)

SPy + NumSPy: also a better Pythran!

3. Fast Native Entry Points

The startup time problem: Every Python script invocation initialises the Python runtime β€” noticeable for short-lived commands.

  • Mercurial (hg): great VCS written in Python (with C and Rust extensions)
  • uv: Python package manager written in Rust, embraced for its speed
  • PDM: great Python project manager written in Python and following packaging PEPs

SPy path: write CLI tools in SPy β†’ compile to native binary β†’ startup like Go or Rust

  • No Python runtime needed on the target machine
  • Deployable: Linux, AWS Lambda, browser (WASM), microcontrollers

A tool like PDM reimagined in SPy: self-contained, fast, Pythonic.

4. A Better MicroPython

MicroPython brings Python to microcontrollers β€” but with significant trade-offs:

Interpreted only: limited performance, energy inefficiency, large binary.

β‡’ For serious embedded applications, C or Rust remain unavoidable.


SPy’s advantages for embedded / edge targets:

  • Compiles to tiny native binaries via C
  • Targets WASI (portable) and bare-metal
  • Native performance (time and energy)
  • One language usable both for development (interpreter) and deployment (compiler)
  • Code shared with the rest of the SPy ecosystem

5. Fast prototyping, Speed & Production Robustness

Python is great for prototyping. SPy bridges the gap to production:

  1. Prototype in Python β€” full dynamism, fast iteration
  2. Partly or fully convert in SPy β€” add type annotations
  3. Compile with SPy β€” types enforced at compile time, native speed
  4. No rewrite in C/Rust/C++

Attractive for large scientific or engineering applications where the same codebase needs to be both approachable and performant.

6. A Better RPython

RPython (used to write PyPy) demonstrated: a statically compilable Python variant is excellent for writing language runtimes.

  • Expressiveness of Python for complex runtime logic
  • Static compilation for fast results
  • Meta-tracing JIT nearly for free from an interpreter written in RPython

But RPython was a by-product of PyPy β€” rough interfaces, not a designed tool

SPy is the designed successor:

  • One of SPy’s stated goals: implement its own interpreter in SPy
  • Future runtimes, DSL engines, bytecode compilers β€” written in something that looks like Python

The Road Ahead

SPy Today: Between Alpha and Beta

What already works:

  • βœ… Interpreter + compiler + debugger
  • βœ… list, dict, tuple
  • βœ… for/while loops, range(), slice()
  • βœ… Classes (structs) & generics
  • βœ… Simple I/O and file operations
  • βœ… CLI argument support
  • βœ… Boehm GC
  • βœ… WASM / Emscripten target
  • βœ… Playground
  • βœ… print() with multiple arguments

Coming soon (Q2 2026 and beyond):

  • 🚧 f-strings
  • 🚧 *args / **kwargs for red functions
  • 🚧 unroll function
  • 🚧 try/except
  • 🚧 with / context managers
  • 🚧 Heap-allocated classes (Python-style)
  • 🚧 SPy/C integration (CFFI for SPy)
  • 🚧 Full SPy/Python interop
  • 🚧 Refcounting memory model
  • 🚧 NumSPy

The key next step: SPy/C integration β€” C library wrapping (BLAS, FFTW, …), sockets, and the foundation for full Python interop.

Already usable by standard Python developers for real things. Now is a good time to get interested.

Getting Involved β€” How to Help SPy Succeed

Try SPy:

# Install (editable, from git)
# Pixi handles all deps
pixi run make-libspy
pixi shell

# Run in interpreter mode
spy examples/hello.spy

# Compile to native binary
spy build examples/hello.spy
./examples/build/hello

Contribute:

  • Good first issues on GitHub
  • Codebase mostly Python & SPy
  • Welcoming community
  • Discord server

Follow:

Deep dive into SPy:

My SPy Book

⭐ Star SPy on GitHub β€” 2 seconds, real impact.

Stars determine whether a project gets noticed by companies and funding bodies.

~700 today β†’ help reach 5000: github.com/spylang/spy

Summary

  1. Python has structural problems that cannot be fixed by CPython alone
  2. Py-NI (Python Native Interface) is a necessary step β€” but not sufficient
  3. SPy πŸ₯Έ is the missing companion language: Pythonic & compilable
  4. A mature SPy with NumSPy could become: a better Cython, Pythran, RPython, MicroPython β€” and more
  5. SPy is between alpha and beta β€” real, tryable, and growing
  6. The Python community needs to get involved now, before the trajectory is set

That’s all β€” Interested? Tutorial/sprint at JDEV: Thursday 25/06, 10:00 AM, room 5

Thank You

Pierre Augier CNRS researcher β€” LEGI, Grenoble

Slides produced for JDEV 2026, Montpellier.

Written with the assistance of a language model; all content reviewed and validated.