how-to Beginner

How to Compile Your First Function

From a saved .py file to a natively compiled function in four verified commands, with the output you should expect at each step and the failure modes explained.

Published Mar 30, 2026

Compiling a Python function to native machine code with Pyvorin Native takes four commands and no configuration. You write a function, ask the compiler which functions in the file it can take, compile the file, and benchmark the result. Everything happens on your own machine, inside your own Python process; no source code is uploaded anywhere, because the compiler has no network code in it at all. This page walks the full path using commands we ran ourselves against the installed pyvorin-native 1.0.9 build, with the real captured output at every step.

One correction to older material you may have seen: there is no remote compile queue, no job ID and no job status command. The previous version of this page ended with pyvorin job status JOB_ID, which does not exist in the product. Compilation is local and synchronous — the command finishes when the compiled artefact exists on your disk.

What you need

The requirements are short. You need Python 3.11 or newer (the package metadata declares Requires-Python >=3.11), pip, and the pyvorin-native package installed. On this machine that is:

pip install pyvorin-native

The 1.0.9 wheel is built for CPython 3.12 on linux x86_64, so that platform combination is the one we can vouch for from direct inspection. If you are unsure whether your environment is sound, one command settles it:

python -m pyvorin doctor

We ran it while writing this page. It checks the Python version, llvmlite, gcc, the runtime libraries shipped inside the package, NumPy, AVX2 support, memory, disk and the cache directory, and prints [PASS], [WARN] or [FAIL] per line. Warnings are advisory; failures are the ones to fix before you continue. If you have just come from the quick start, you have already done all of this.

Step 1: Write a function worth compiling

Save this as math_ops.py:

def square_sum(n: int) -> int:
    total = 0
    for i in range(n):
        total += i * i
    return total

def entry():
    return square_sum(100000)

Shape matters here. A tight integer loop over a hundred thousand iterations is the kind of code the compiler exists for: pure Python, CPU-bound, dominated by an inner loop that CPython would otherwise execute as a long stream of bytecodes. The entry wrapper exists because the benchmark and validation machinery calls your target with no arguments, so anything that needs parameters gets a zero-argument shell around it.

Step 2: Ask the compiler what it can take

Before compiling anything, run the support report. It is the fastest ten seconds in this whole walkthrough:

python -m pyvorin support math_ops.py

Captured output:

Support report for math_ops.py
Function                       Status               Unsupported
------------------------------------------------------------
square_sum                     COMPILED_FULL        0
entry                          COMPILED_FULL        0

COMPILED_FULL means the function contains nothing the compiler cannot lower to native code — zero unsupported constructs. The status column is honest about the other outcomes too: COMPILED_PARTIAL for functions where some constructs stay on the interpreter path, and COMPATIBILITY_EXECUTED for functions that will simply run as ordinary CPython. There is no point timing a function until you know which of those rows it sits on. We will see a real partial case in the failure section below.

Step 3: Compile

python -m pyvorin compile math_ops.py --function square_sum

Output from our run:

Compile: math_ops.py
  square_sum                     COMPILED_FULL        256.055 ms | Runtime validation not performed: function requires arguments

Two things to read in that line. The status confirms the native path was taken. The trailing note is not an error: square_sum takes an argument, so the compiler could not execute it against the CPython oracle to compare results. Compile the zero-argument entrypoint instead and validation does run:

python -m pyvorin compile math_ops.py
Compile: math_ops.py
  square_sum                     COMPILED_FULL        0.000 ms | Runtime validation not performed: function requires arguments
  entry                          COMPILED_FULL        228.189 ms

Note the 0.000 ms against square_sum on the second invocation. That is the disk compile cache doing its job: the artefact from the first run was keyed by a hash of the source, the function name, the compile options and the toolchain's modification times, and the second run found it. Compilation happens once per source change, not once per command.

The full flag list is one command away — run python -m pyvorin compile --help to see it. The ones worth knowing on your first day are --json for machine-readable reports, --report <path> to write the report to a file, --fail-on-fallback to exit non-zero if any function falls back (we verified it exits 2), and --explain-fallback to print the per-function reasons. The optimisation toggles --no-vectorize, --no-parallel and --no-pgo isolate each stage when you are attributing a speedup later.

Step 4: Benchmark the compiled function

python -m pyvorin bench math_ops.py --function entry --runs 5 --warmup 2
Benchmark: math_ops.py (entry)
  warmup: 2
  runs:   5
  correct: True
  status: COMPILED_FULL
  compile_time: 238.903 ms
  min:    0.008 ms
  mean:   0.010 ms
  median: 0.009 ms

Read the fields in order. correct: True first — the compiled function returned the same result as the interpreted original, and if that ever reads False nothing below it matters. Then status, confirming the measured runs actually ran native code. Then the timings. On this machine the median call took roughly nine microseconds after compilation cost around 240 milliseconds. Do not quote our numbers as expectations: they came from one specific host and exist here to show you what the output looks like and how to read it. Your hardware will differ. The discipline for turning this into a number you can defend in front of a sceptic lives on the speed proof page, and the everyday version of the same exercise is documented in how to benchmark a function.

When the answer is not COMPILED_FULL

Real files contain constructs the compiler cannot take. Here is a genuine partial case we compiled while writing this page — a generator expression inside an otherwise ordinary loop:

def with_generator(n: int) -> int:
    total = 0
    for i in range(n):
        total += i * i
    gen = (x * 2 for x in range(10))
    return total + sum(gen)
Support report for partial.py
Function                       Status               Unsupported
------------------------------------------------------------
with_generator                 COMPATIBILITY_EXECUTED 0
entry                          COMPATIBILITY_EXECUTED 0

Running compile on that file reports COMPATIBILITY_EXECUTED — the function ran on the honest CPython fallback rather than failing. Nothing crashed, and your program's semantics were never at risk. That is the design: unsupported code paths fall back to ordinary CPython transparently, and the per-function reasons are available on demand:

python -m pyvorin compile partial.py --explain-fallback
with_generator:
  Fallback reasons:
    - unsupported_ast_pattern (with_generator): unsupported AST pattern; calls a compat-mode function
entry:
  Fallback reasons:
    - unsupported_ast_pattern (entry): calls a compat-mode function

And a generator function using yield gives a still more precise diagnosis through the explain command:

python -m pyvorin explain partial.py
  yielder
    Status:  COMPILED_PARTIAL
    Unsupported features:
      - Yield (yielder): Unsupported feature encountered; function executes in compatibility mode

The judgement call is straightforward once you can see the reasons. If the unsupported construct sits on a cold path — an error branch, a one-off setup step — partial compilation still leaves you the hot loop. If it sits in the hot path itself, rewrite the construct: a generator becomes a list, a while True with complex mutation becomes a bounded for. Fix the code, not the tool. The full taxonomy of these statuses, including what GuardRuntimeError means when a guard fails at runtime rather than at compile time, is covered in how to handle compilation failure and unsupported code and the fallback path.

Where the artefacts live

Two caches exist after your first compile. The compiled machine code is stored in <site-packages>/.pyvorin_cache/disk_compile, inside the install tree, as SHA256-named shared objects plus a cache_index.json index — on our machine, 159 of them at the time of writing. The cache is an LRU holding up to 500 entries, and each entry is checksum-validated on load, so a corrupted artefact is detected rather than executed. Separately, importing pyvorin at all creates ~/.pyvorin_cache/columnar_kernels/ in your home directory, before any licence activity — harmless, but worth knowing if you audit file-system writes. If you are running builds in CI and want the compile cost skipped on repeat runs, the CI caching page covers the cache-key strategy in detail.

Where to go next

Last reviewed 30 March 2026 against pyvorin-native 1.0.9 installed at /root/pvfinal. Every command and output block on this page was captured by running the command shown; the dead remote-compile workflow from the previous version has been removed entirely.