Getting started¶
This tutorial takes you from installing PPy to running your first native kernel. Run every command below in your project directory.
1. Install PPy¶
If you don't use uv, run pip install "ppy-lang[llvm]" instead.
The distribution is called ppy-lang. It installs three packages: ppy,
ppy_compiler, and ppy_runtime. Your code writes import ppy.
The native backend needs the llvm extra (llvmlite). The fastest call
boundary is built when the CPython headers (python3-dev) are present.
Installing has the details.
Check what PPy found on your machine:
2. Write your first file¶
A .ppy file is valid Python. The compiler reads the decorators and
annotations of the ppy package. Under plain CPython they do nothing.
Save this as collatz.ppy:
# collatz.ppy
import ppy
@ppy.pure
@ppy.opt(3)
def longest(limit: int) -> int:
best: int = 0
for start in range(1, limit):
n: int = start
steps: int = 0
while n != 1:
n = n // 2 if n % 2 == 0 else 3 * n + 1
steps += 1
best = max(best, steps)
return best
print(longest(ppy.input[int]()))
What the ppy parts do:
@ppy.pureis a contract that the function has no observable effects. The checker verifies it.@ppy.opt(3)sets this function's optimization level.ppy.input[int]()reads one line of standard input as an integer. The value goes straight into memory, without a Python object per field.
3. Run it three ways¶
echo 300000 | uv run python collatz.ppy # 1. plain CPython
echo 300000 | uv run ppy collatz.ppy # 2. the optimized Python backend
echo 300000 | uv run ppy run collatz.ppy # 3. LLVM native
All three must print the same answer. If they differ, that is a compiler bug.
The first run of the third command builds into the cache (about 0.6 s). Later runs only start the launcher, which serves the built artifact. If you edit any source under the project, the artifact's name changes and a fresh one is built.
4. Check and explain¶
uv run ppy check collatz.ppy # static checking only; strict is the default
uv run ppy explain collatz.ppy:longest # why it went native, or what blocked it
ppy check runs the checker without running your code. An implicit Any
is an error (E1201). So is a decorator the checker cannot vouch for
(E1204). Diagnostics lists every code.
ppy explain tells you why a function went native, or what blocked it.
5. Build an executable¶
ppy build writes the objects, libppy_<project>.so, a manifest, and a
launcher executable. The launcher starts an embedded interpreter, imports
ppy_runtime, and runs your program. It keeps working after you uninstall
the compiler.
run and build produce the same program. Both keep Python-integer
semantics: overflow is guarded and falls back to arbitrary precision. Pass
--unsafe to either one to drop the guards and get 64-bit wrap semantics
like C's.
To get an executable with no CPython inside:
6. Use it in an existing project¶
You don't need to move a whole Python project to PPy. Carve out the kernels instead:
- Put the loops where the time goes into a
.ppymodule. - Add one line to it:
import ppy. - Import that module from your plain
.pycode. Where the compiler is installed, the module is served natively.
Interop and Migrating a real project cover this in depth.
Two commands help you turn existing Python into PPy:
uv run ppy convert kernel.py # untyped Python to strict PPy, inferred from the call sites
uv run ppy migrate legacy.py # the permissive form: dynamic features go behind boundaries