Python, compiled.
pyc is a whole-program compiler for Python 3. It infers every type with no annotations and emits a native binary that behaves exactly as CPython does, or it tells you at compile time why it can't.
class Shape:
def area(self):
return 0.0
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r * self.r
class Square(Shape):
def __init__(self, s):
self.s = s
def area(self):
return self.s * self.s
shapes = [Circle(1.5), Square(2.0), Circle(0.5)]
print(sum((s.area() for s in shapes), 0.0))$ pyc --strict shapes.py
$ ./shapes
11.853974999999998
$ python3 shapes.py
11.853974999999998Why pyc
pyc treats the whole program as one unit. Because it sees every call and every allocation, it can compile dynamic-looking Python into monomorphic native code.
No annotations
Write ordinary Python. pyc infers a type for every value in the whole program, then compiles a specialized version of each function and class for the types that actually reach it.
Native binaries
The output is a stand-alone executable built from C or LLVM IR. There is no interpreter and no libpython at run time, and no CPython at compile time either.
CPython semantics
A compiled program prints what CPython prints, or pyc refuses it at compile time. It never deviates silently, and the end-to-end suite checks every test against CPython.
Two backends
The C backend is the default. The LLVM backend (-b) emits IR directly and also drives a JIT and an interactive REPL. CI runs the full suite on both.
Demand-driven precision
The analysis starts from the fewest contours possible and splits a function or a data type only when a real type distinction demands it. Code size tracks the program, not the call graph.
Real Python
Classes and inheritance, closures, exceptions, generators, async/await, match/case, comprehensions, f-strings, packages and relative imports.
Never silently different
Some Python can't be typed statically without boxing. pyc never quietly papers over that. By default it applies a few named accommodations, each of which warns at the line it affects. Under --strict the program has CPython's semantics or it doesn't compile.
In this example the analysis also finds that d only ever holds integers. It gives d its own integer-typed layout of Counter, so d.total prints 1 exactly as CPython does.
class Counter:
def __init__(self):
self.total = 0
def add(self, v):
self.total = self.total + v
c = Counter()
c.add(1)
c.add(2.5)
print(c.total) # 3.5
d = Counter()
d.add(1)
print(d.total) # 1$ pyc counter.py
counter.py:3: warning: 'total' holds both int and float; widened to
float64, so an int value in it prints as a float (CPython keeps the int).
...
Permissive numeric coercion, refused under --strict (issues/171)
$ ./counter
3.5
1
$ pyc --strict counter.py
counter.py:5:69: error: expression has mixed basic types:( int64 float64 )
...
fail: program does not typeFast because it's typed
Once every value has a concrete type, an attribute read becomes a field load, a method call becomes a direct call, and an int is a machine word. These are wall-clock times for unmodified benchmark programs.
| Program | What it does | CPython | pyc | Speedup |
|---|---|---|---|---|
fib(35) | Naive recursive Fibonacci | 1.2 s | 24 ms | 48× |
go | Monte Carlo tree search Go player | 62.3 s | 7.5 s | 8.3× |
sudoku1 | Constraint-propagation Sudoku solver | 9.9 s | 1.5 s | 6.5× |
nbody | Planetary orbit simulation | 73.3 s | 13.0 s | 5.6× |
pystone | The classic Dhrystone port | 18.0 s | 4.3 s | 4.2× |
mandelbrot2 | Mandelbrot set renderer | 2.0 s | 716 ms | 2.8× |
Wall-clock time of a single run, CPython 3.12 vs. pyc with its default C backend, on an AMD Ryzen 9 3950X. Programs are unmodified from the shedskin example corpus, except fib(35), which is the four-line program on the install page. Speedups depend heavily on the program: code dominated by calls and integer arithmetic gains the most, and code that mostly allocates gains less.
Where pyc is going
The compiler core is solid on a 77-program benchmark corpus. The next phase is breadth and definition: a much larger standard library, and a strict mode developed into a fully specified dialect of Python.
More libraries
22 standard-library modules ship today, from math and random (true MT19937) to re, struct and heapq. Each one is real or it raises. None returns made-up values.
A strict dialect
Strict pyc is already “CPython or a compile error.” The goal is to make it a language in its own right, with a specification, precise diagnostics and a typed standard library.
The roadmap
**kwargs, property setters, complex, a wider re, packaging, and the tooling that turns a compiler into a language.