33 comments

[ 5.0 ms ] story [ 55.1 ms ] thread
I can't view the site on my mobile without accepting cookies.
I like the idea of a compiled language that takes the look and ethos of Python (or at least the "looks like pseudocode, but runs"-ethos)

I don't think the article gives much of an impression on how SPy is on that front.

If you want different parts of your code to be a statically typed Python lookalike Cython is a mature option
This seems to be going for a somewhat similar goal to Mojo [0] - anyone here who used both and is willing to offer a comparison?

[0] https://www.modular.com/mojo

Mojo is such an interesting project but it's disappointing they pivoted so hard to focusing on GPU kernel programming and seemingly abandoned the objective of a general purpose python alternative
Looks very interesting!

I remember chatting with one of the creators of PyPy (not the author of TFA) a number of years ago at HPI. He had just given a talk about how RPython was used in PyPy development, and I was fascinated.

To me, it seemed completely obvious that RPython itself seemed like a really interesting standalone language, but he would have none of it.

Whenever I suggested that RPython might have advantages over PyPy he insisted that PyPy was better and, more strangely, just as fast. Which was sort of puzzling, because the reason given for RPython was speed. When I then suggested that they could (after bootstrap) just use PyPy without the need for RPython, he insisted that PyPy was too slow for that to be feasible.

The fact that both of these statements could not really be true at the same time did not register.

Good level of detail (for me) to understand (some things).
Common Lisp also allows you to redefine everything at runtime but doesn't suffer from the same performance issues that Python has, does it?

Doe anyone have insight into this?

This looks like a very interesting approach bringing comptime to a static version of python. This version of comptime can then be used to define new types in the same way Zig does it.

I absolutely hate the terminology though red/blue redshifting etc. Why do blue functions disappear when redshifting? If you red shift blue then it goes down in frequency so you might get green or red. Perhaps my physics brain is just over thinking it!

> The other fundamental concept in SPy is redshifting.

> Each expression is given a color:

> blue expressions are those which can safely be evaluated ahead of time, because they don't have side effects and all operands are statically known.

> red expressions are those which needs to be evaluated at runtime.

> During redshifting we eagerly evaluate all the blue parts of the code: it's a form of partial evaluation. This process plays very well with the freezing that we discussed above, because a lot of operations on frozen data become automatically blue: for example, if we statically know the type of an object, the logic to look up a method inside the frozen class hierarchy is a blue operation and it's optimized away, leaving just a direct call as a result.

Please just rename it comptime then at least people who have learnt Zig will know what it means immediately.

In FORTH these would have been called IMMEDIATE words. Namely functions which run at "compile" time rather than run time.

> SPy is [...] a compiler

> SPy is not a "compiler for Python"

I think it's funny how it's confusing from the first paragraph

Neat idea! Author’s ideas about different subsets of Python are worth the price of admission. What you can express in the type system, what performs well under JIT, what’s basically same and reasonable, may not be precisely specified, but are still useful and distinct ideas.
The problem with this is that the main value of Python is its ecosystem. SPy aims to be able to import Python libraries, but also not implement all Python features. If you are not 100% compatible how can you reliably import libraries?

SPy seems most likely to be more likely to be appealing as a more Pythonic alternative to Cython rather than a Python replacement.

It's weird, but the pain of moving to a whole new language seems less than the pain of using one of these language subsets. I guess it might be because of "loss aversion": we dislike losing something we have more the value we put on gaining it. In a new language each newly added feature is a gain, but in a language subset you're always encountering things you would have in the full language. So no matter how good the rational case is, there is a bias against them. I can't think of a successful one actually - anyone?
Overall this looks very interesting - I always thought more could have been done with RPython, but there was never any documentation for it. I do have some nits though:

> The following silly example happily passes mypy

In fairness that's because Mypy is shit. Pyright catches this mistake.

> As such, we need to treat Python type checkers more like linters than actual theorem provers

Again I think this is true of Mypy but Pyright is much closer to a sound type checker.

> redshifting

This is just constant propagation isn't it? C compilers have been doing this for decades. I don't think we need a silly new term for it. Let's not cause another "tree shaking" situation.

> So far, this is not different than usual constant folding, with the difference that it's guaranteed to happen. What makes it more powerful is the ability to mark some functions as @blue.

That's not different. It's just constant folding with C++'s `consteval`. And @blue is an absolutely abysmal name.

It would be much clearer if @blue were changed to @consteval or @comptime (either's good I think), and you just call it "compile time evaluation" or "constant propagation" instead of "redshifting".

i thought this was super cool and informative thank you so much for writing it.

i work a lot in python and tried to build a runtime type checker for it that basically acted as automatic assertions on all stated types at function boundaries, and it worked really well to bring all TypeErrors in my code to the surface on first run.

but, my knowledge is woefully short on compilers and languages. this definitely gave me a great frame of understanding, so thanks again for writing it

++spy ethos and ideas

But why limit to an interpreter? Translate to other excellent compiled languages and benefit from the optimization work there.

Giving up on C-API and the dynamic parts of python that 1% of the people use is a good trade-off.

In the age of cursor and windsurf it's not hard to auto replace incompatible code with something that works in the static-py ecosystem.

Would love to participate in an effort to standardize such a subset.

I sympathise with the motivations for this, though I don't use Python much. I occasionally work on a toy Ruby compiler that started as a long blog series. More recently I've picked it up again with heavy AI use - I set Claude working on improving Rubyspec pass rates (which are atrocious). It's chugging along right now, actually.

One of the things I've spent a lot of time thinking about are the ways to avoid a lot of the dynamic features of Ruby without affecting actual, real code much.

There's a lot that can be done there - e.g. all of the research on Self and JS VMs is highly applicable.

But I say "real code" because a lot of the "worst" dynamic features of Ruby (and Python) either doesn't appear in production code very often, or at all (there are still aspects of Ruby I have never seen used in real-life use despite having used Ruby for 20 years), or could be mitigated trivially, so I still believe you can do quite decently without a lot of the more complex optimisations.

As an example (from Ruby): You can re-open the Integer class and override +:

  ruby -e 'class Integer; def +(other); 42; end; end; p 2 + 2'
(don't do this in IRB; it crashes)

But nobody does that. The problem with a lot of these features isn't that people use them, but that people might use them.

That leaves two main avenues: We can do like the author of thise post and strip away the costly features that are rarely used.

Or we can provide ways of promising not to use them through code or options.

The first option is perfectly valid, but I quite like the second:

In Ruby, it turns out a lot of the optimisation challenges goes away if you get an app to freeze the most important system classes after setup, because even most of the most horrific examples of Ruby monkeypatching tends to do most of it only during startup, and you then tend to get to a stable state where you can let applications opt in to additional optimisations just by calling "freeze" on a number of objects.

Ruby programs will also do things like dynamically decide which files to load based on reading the directory, but if you compile an application, most of the time you want that to happen ahead of time with a few exceptions (e.g. plugins), and so similarly, if you freeze as many classes as possible at a given point, you can partially evaluate manipulation of the runtime up until that point, and treat it as mostly static afterward, and fall back to slow paths for anything you can't statically resolve the names of, and still end up with lots of optimisation potential for most of the low level code.

I think a lot of the same would work for Python, and might bridge the gap between the categories of alternative implementations the author mentions with more predictability than relying on a JIT doing the right analysis.

E.g. your compiler can eat least potentially guarantee under which circumstances it can statically determine that an Integer can inline the fast path if Integer is frozen so that you can in fact reason about the code.

This is really cool and what I thought mojo would be. The subset of Python that is easy to use, read, and removes the dynamic nature that we don't use.

Excited to see where this goes.

I'm still looking for a python-ish (at least the syntax) embeddable library for C that is easy to include in a project, a library that does not have its own build system with dozens of files... but I don't think one exists.
RustPython seems like the fulfillment of this for Rust but unfortunately haven't seen an equivalent for C
While obviously this would need CPython to SPy `import` support for it to displace CPython for me, it seems like you can `import` SPy to CPython, which makes it an attractive solution for implementing fast libraries where Rust is perhaps too heavy duty.
The feels like JS -> TS type of change on JS side, except that was done with transpiler (sp ? ).
Statically typed. That's just Java with python syntax again, which the article tries to talk it's way out of but that's what it is.
If in the end we can just have .spy on some files that have performance critical functions in them and the rest is just normal python, this could be down right amazing.

We recently swapped out mypyc optimised module for a rust implementation to get a 2-6x speed up, and not having to do that would be great.

As a prospective user the first thing I look for is lists, list comprehension, list slices, string methods, zip, zip*, tuple destructuring, generators, dicts and sets. These data structures, their syntax, and associated methods are what make Python a joy to use. They are what make it possible to solve a puzzle in a one-liner.

The examples in the playground are neat but I don’t see any of the Python bread and butter usage. I see C with Python syntax.

I’d like to see Python syntax solving a problem in a pythonic way, albeit with type annotations. And then I’d like to see the compiled version run 30x-100x faster. Is that ultimately the promise of this project?