Python 3 → native code

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.

shapes.py
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))
terminal
$ pyc --strict shapes.py
$ ./shapes
11.853974999999998
$ python3 shapes.py
11.853974999999998
76/77
shedskin benchmark programs compile and run
58
print output byte-identical to CPython; 17 more can't be compared because CPython hits the time cap
488
end-to-end tests, checked against CPython on both backends
0
type annotations required

Why 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.

The strict dialect →

counter.py
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
terminal
$ 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 type

Fast 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.

ProgramWhat it doesCPythonpycSpeedup
fib(35)Naive recursive Fibonacci1.2 s24 ms48×
goMonte Carlo tree search Go player62.3 s7.5 s8.3×
sudoku1Constraint-propagation Sudoku solver9.9 s1.5 s6.5×
nbodyPlanetary orbit simulation73.3 s13.0 s5.6×
pystoneThe classic Dhrystone port18.0 s4.3 s4.2×
mandelbrot2Mandelbrot set renderer2.0 s716 ms2.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.

Library status →

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.

Read the plan →

The roadmap

**kwargs, property setters, complex, a wider re, packaging, and the tooling that turns a compiler into a language.

See the roadmap →