Compiling Python to Native Code: A Measured Walkthrough
One real Python function compiled to native code end to end — verified commands, captured output, a measured before-and-after, and the honest boundary cases.
Published Aug 28, 2026
This page takes one ordinary Python function through the full journey to native machine code: written as pure Python, run under CPython, checked against the compiler, compiled, and measured — every command shown with the output we captured from a live run of Pyvorin Native 1.0.9. The point is not the example itself. It is that everything you need to repeat this on your own code is two commands and about five minutes, and that the failure cases are as instructive as the success.
"Native code" here means machine code in a shared library, loaded into your running CPython process. Your program keeps importing, raising and calling extensions exactly as before; the functions the compiler accepts simply execute as compiled machine code lowered through LLVM, with CPython as the fallback wherever compilation cannot hold.
The example
A trimmed version of a shape we see constantly in prospect calls: a risk-scoring kernel that walks two parallel lists of floats, applies a cap, and accumulates a result. No libraries, no I/O, just a loop over data — the kind of function a profiler flags and a compiler is built for.
# risk_kernel.py
def entry():
"""Weighted cumulative risk score with a cap, pure Python."""
n = 200000
returns = [(i % 200 - 100) / 4000.0 for i in range(n)]
weights = [(i % 50 + 10) / 40.0 for i in range(n)]
limit = 4.0
total = 0.0
count = 0
for i in range(n):
score = returns[i] * weights[i] * 100.0
if score > limit:
score = limit
if score < -limit:
score = -limit
total += score * score
count += 1
return total / count if count else 0.0
if __name__ == "__main__":
print(f"risk score: {entry():.4f}")
Two design choices in this file are deliberate, and both came from running earlier versions. The data generation lives inside entry(), and the whole computation is one function. An earlier draft split the loop into a helper called by a wrapper; the support report marked both functions COMPILED_PARTIAL, with an internal function call listed as the unsupported construct, and the benchmark executed on the compatibility path — full interpreter speed, no compilation at all. Keeping the hot loop whole let the compiler see everything at once. That is the first engineering judgement of this page, and it cost nothing but a refactor of a demo.
Step 1: run it under CPython
A benchmark can only measure code that runs, and the harness enforces this itself. Plain interpreter first:
python risk_kernel.py
risk score: 1.8290
Baseline timing, measured with the standard library on the same host:
CPython min: 33.474 ms
CPython median: 34.184 ms
Thirty-four milliseconds for two hundred thousand iterations. Keep both numbers — the answer, 1.8290, and the time, 34.184 ms median. The first is the correctness oracle; the second is the yardstick.
Step 2: ask the compiler what it can take
Before measuring anything, ask which functions qualify for native compilation:
python -m pyvorin support risk_kernel.py
Support report for risk_kernel.py
Function Status Unsupported
------------------------------------------------------------
entry COMPILED_FULL 0
COMPILED_FULL with zero unsupported constructs: the type inference, compatibility analysis and AST passes all accepted the function, nothing was left on the interpreter path. If this report had shown COMPILED_PARTIAL or a compatibility routing, the next stop would be the fallback documentation rather than the stopwatch — measuring a number you cannot explain is how benchmarking goes wrong.
Step 3: measure
python -m pyvorin bench risk_kernel.py --function entry --runs 5 --warmup 2
Captured output from the installed build:
Benchmark: risk_kernel.py (entry)
warmup: 2
runs: 5
correct: True
status: COMPILED_FULL
compile_time: 346.879 ms
min: 3.955 ms
mean: 4.010 ms
median: 3.999 ms
Read the fields in this order. correct: True means the compiled result was verified against the interpreter's own output before any timing was trusted — 1.828977851562689, matching the CPython answer to the printed precision. status: COMPILED_FULL confirms the timed runs executed as native code, not the fallback. compile_time: 346.879 ms is reported separately from execution precisely so you can judge amortisation: at 34 ms saved per call, the compile step pays for itself on roughly the tenth call. Then the timings: median 3.999 ms against the CPython median of 34.184 ms — about 8.5x on this host, on this day.
Treat that ratio as what it is: one function, one host, one measurement session. It is not a marketing number and it is not a prediction. Our published suite, which exists to answer the prediction question, measured a median of 1.35x and a geomean of 3.16x across 71 workloads, with 17 of them slower than CPython. The honest range of outcomes is wide; the only figure that matters for your code is the one you measure on your machine.
What happened inside
The speedup has a specific anatomy, and it is worth naming because it generalises. Type inference proved the loop variables were floats, so values stayed in registers instead of heap boxes. Escape analysis classified the lists and let the code generator hoist their pointers out of the loop. Bounds-check elimination removed per-index safety checks it could prove redundant, with a guarded slow path where it could not. LLVM then did what LLVM does — instruction selection, register allocation, vectorisation for your exact CPU — after Pyvorin's passes had already removed the Python-level overheads LLVM cannot see through. Every stage of that pipeline, with the module names, is documented in Compiler pipeline overview; the short version is that the interpreter's three big costs — dispatch, boxing, reference-count churn on temporaries — were all present in this loop, and all three were removable here.
Which loops qualify for this treatment follows a pattern, and our measured suite maps it precisely. The categories at the top — numerical computing at a 10.91x geomean, core integer and loop kernels at 16.24x, object manipulation at 42.28x — are pure-Python, CPU-bound loops like this one. The categories at the bottom tell you where the anatomy above does not apply, which brings us to the boundary cases.
What gets compiled in a mixed program
Real programs are rarely one function. The granularity of compilation is the function, and knowing what that means in practice saves confusion when you point support at a larger file. Recall the earlier draft of this walkthrough, where the loop lived in a helper called by a wrapper. The captured report:
Support report for risk_kernel.py
Function Status Unsupported
------------------------------------------------------------
risk_scores COMPILED_PARTIAL 1
entry COMPILED_PARTIAL 1
Both functions qualified only partially, and the benchmark executed them on the compatibility path — interpreter speed, no compilation — because a call between two ordinary Python functions in the same file was listed as the unsupported construct in our run. Inlining the loop into the entrypoint changed the verdict to COMPILED_FULL and produced the 3.999 ms median. The general lesson: the support report is a map of your program's seams. Functions it marks partial or compatible still run — correctly, at interpreter speed, with the fallback recorded — while the functions it marks full carry the acceleration. A mixed program is the normal case, not a failure state, and the report exists so you can see the split at a glance before you measure.
The compile cache and repeat runs
Compilation in this walkthrough happened inside the benchmark run, and its 346.879 ms showed up in the report because the harness reports it separately. In day-to-day use, compiled results are cached on disk, keyed by a hash of the source, the function name, the compilation options and the runtime and compiler timestamps, with an LRU bound of 500 entries and a checksum validated per entry. Unchanged code pays no compile cost on subsequent runs; the cache entry is reused. That is what makes the economics work in batch and service settings alike — the compile step is paid when the code changes, not on every start, and the benchmark report's separate compile-time field exists precisely so you can verify that amortisation on your own schedule rather than ours.
Reading your result against the suite
About 8.5x is a satisfying number, and it deserves the context that keeps it honest. Our published suite of 71 workloads measured a median of 1.35x and a geomean of 3.16x across many program shapes, with 54 workloads faster than CPython and 17 slower. The walkthrough function sits in the winning territory because it was chosen to: a typed, float-heavy, branch-light loop with no library calls — the shape the compiler is built for. Your functions will land where their shapes put them, which is why the workflow on this page ends with a measurement on your code rather than a projection from ours. The suite's value is the map of what to expect — parsing- and string-shaped functions near or below parity, numerical loops in the tens — and your benchmark is the territory itself.
Boundary cases, captured honestly
A variant of this same function failed to compile. An earlier version accumulated with total += (abs(score) + 1.0) ** 0.5. The support report was clean — COMPILED_FULL — but the benchmark refused to time it:
[ERROR] Warmup execution failed: integer overflow in native code - value exceeded 64-bit range
Hint: Run `pyvorin explain <file>` to see unsupported features for this function.
The harness ran the function as ground truth, caught a wrong-typed intermediate, and declined to report a number. Rewriting the accumulation as a plain multiply — total += score * score — produced the clean run captured above. The lesson generalises: the compiler reasons about concrete numeric types, and formulations that mix numeric families can defeat its inference. The failure was loud, safe and fixable, in that order.
The harness will not benchmark code that does not run. Before the wrapper convention, we pointed the benchmark at a function that required arguments. Verbatim:
[ERROR] Ground-truth execution failed: risk_scores() missing 3 required positional arguments: 'returns', 'weights', and 'limit'
Hint: Run with `python <file.py>` to confirm the script works in CPython.
Compilation can decline, and the program still works. Functions that use constructs the compiler cannot lower are not rejected — they are routed to an honest CPython fallback and the run report records that. A parsing-heavy or I/O-bound function may show exactly this behaviour, and for good reason: our suite's parsing category measured a 0.86x geomean and string manipulation 0.85x, both fractionally slower than the interpreter, because there is little dispatch-heavy loop work in them to reclaim. The fallback exists so that mixing one compiled kernel into a larger program is always safe, never a fork in behaviour.
Repeating this on your code
The whole workflow, start to finish:
# 1. Confirm the script runs under plain CPython
python your_script.py
# 2. See which functions compile
python -m pyvorin support your_script.py
# 3. Benchmark an entrypoint (zero-argument wrapper)
python -m pyvorin bench your_script.py --function entry --runs 5 --warmup 2
# 4. Machine-readable record for CI
python -m pyvorin bench your_script.py --function entry --json
Run it against the function your profiler flags, on hardware that resembles production, with data that resembles production. The --no-vectorize, --no-parallel and --no-pgo flags isolate each optimisation stage if you want to understand where your speedup comes from rather than simply having it.
Two habits turn the workflow into an asset rather than a one-off. Keep the JSON output from every serious run — it is machine-readable, it records the options and the correctness verdict alongside the timings, and it becomes the baseline against which later code changes are judged. And measure on hardware that resembles production at least in CPU generation; a laptop and a server VM can disagree on vectorised code by more than the run-to-run noise, and you want your amortisation arithmetic built on the conservative machine.
Where to go next
- What AOT compilation means for Python — the concepts behind what you just ran: interpreters, JITs and AOT, and where each fits.
- Compiler pipeline overview — every stage between your source and the machine code, with the module names.
- How to benchmark a function — the measurement discipline: warm-up, runs, and the checks before you trust a number.
- Unsupported code and the fallback path — what happens when compilation declines, in detail.
Last reviewed 28 August 2026 against Pyvorin Native 1.0.9 (installed package, commands run locally; output captured verbatim from those runs). Suite-level figures are from the canonical benchmark artefact dated 13 September 2026 (71 workloads: 54 faster, 17 slower; geomean 3.16x, median 1.35x). The before-and-after on this page is a single-host live measurement, labelled as such, not a suite figure.