how-to Beginner

How to Force the CPython Fallback

The verified ways to get pure-CPython behaviour in pyvorin-native 1.0.9: the cpython backend, PYVORIN_BACKEND, run --compare, and fallback in bench --json.

Published Aug 11, 2026

Sometimes you want the interpreter, deliberately. The honest CPython fallback is not only a safety net in pyvorin-native 1.0.9 — it is a selectable backend, and choosing it is the cleanest way to A/B compiled against interpreted behaviour on identical hardware, or to debug a suspected miscompile by rerunning the exact same code under CPython. This page lists the mechanisms that actually exist in the installed build, each one verified by running it, and names the two mechanisms that are commonly claimed but do not.

First, what does not exist

Two supposed switches circulate in old notes and secondary documentation. Verified against the installed 1.0.9 build, neither is real: there is no PYVORIN_FORCE_FALLBACK environment variable anywhere in the package, and the CLI has no --fallback flag — python -m pyvorin run --help lists every option the command accepts, and it is not among them. Setting the variable or flag silently does nothing, which is worse than an error, because a debugging session built on a fictional switch wastes time on the wrong hypothesis. The verified mechanisms below replace both.

Selecting the cpython backend in code

The unified compiler accepts a backend parameter, and one of the valid values is cpython:

from pyvorin.compiler import PyvorinCompiler

src = "def numeric_sum(n):\n    total = 0\n    for i in range(n):\n        total += i * i\n    return total\n"

compiler = PyvorinCompiler(backend="cpython")
fn = compiler.compile(src, "numeric_sum")
print(fn(100000))            # 333328333350000
print(fn.backend_used)       # pyvorin-cpython-fallback

Verified live against the installed build: the call returns the correct result and backend_used reports pyvorin-cpython-fallback. The complete set of backend choices in 1.0.9 is native, auto, cpython and columnar. Passing legacy raises a RuntimeError stating that the old stack was removed from production paths — if you have code still selecting it, that is the migration error to fix first.

The sibling parameter allow_fallback controls the opposite direction. It defaults to true for native and auto, which is what makes fallback transparent. Set allow_fallback=False with backend="native" and any function the native path cannot handle fails loudly at compile time instead of degrading — useful when you are auditing exactly which of your functions compile, with no silent interpreter executions diluting the answer.

Setting it process-wide with PYVORIN_BACKEND

The same backend choice reads from the environment. PyvorinCompiler defaults its backend to the value of PYVORIN_BACKEND when no explicit parameter is given, so this works process-wide without touching call sites:

PYVORIN_BACKEND=cpython python my_pipeline.py

Verified live: a compiler constructed with no arguments under PYVORIN_BACKEND=cpython reports backend_used: pyvorin-cpython-fallback for its compiled functions. The same variable accepts native and auto; any other value falls back to auto behaviour rather than erroring, so treat typos as a silent mode change and check backend_used when the setting matters.

Reaching for an explicit backend choice makes sense precisely because automatic fallback is reactive — it triggers when native compilation fails or a guard deoptimises — while an explicit backend choice is deterministic. For A/B measurement that difference is the whole point: you are comparing two complete, stable runs, not a native run against whatever fragments happened to divert mid-flight. For debugging it matters too. When output looks wrong, "run the whole process under PYVORIN_BACKEND=cpython and diff" isolates the compiler from your logic in one step; chasing individual fallback events after the fact rarely reconstructs a clean comparison.

When the fallback happens by itself

In auto mode — the default — the router sends every function to the native compiler first and diverts to the honest CPython fallback when the native path cannot handle it, whether at compile time or through a runtime guard failure. The fallback executes your original Python source through the ordinary interpreter, so results stay correct; what you lose is speed, and what you gain is a record of the decision. Fallback events are logged by the diagnostics layer with a reason, and every executed function exposes backend_used, which is never silent about which engine ran.

Verified example: a function built around a generator expression routes to the fallback through the auto router even though it runs correctly:

from pyvorin.compiler import PyvorinCompiler

src = ("def f():\n"
       "    g = (i * i for i in range(100))\n"
       "    return sum(g)\n")

fn = PyvorinCompiler(backend="auto").compile(src, "f")
print(fn())                # 328350
print(fn.backend_used)     # pyvorin-cpython-fallback

The full contract — which constructs divert, which imports are rejected outright at the frontend with an ImportPolicyError, and what the strict modes do — is documented on the unsupported code and fallback page.

Measuring the fallback

Getting pure-CPython numbers is only half the job; you also need to prove which engine produced them. Three verified routes, in increasing order of convenience.

For a direct CPython reference timing of a workload file, run --compare times the same warm-up and measurement loop under both engines and prints the ratio, verified live:

python -m pyvorin run benchmarks/numeric_sum.py --compare
CPython:  3.041 ms (result=333328333350000)
Pyvorin:  0.008 ms (result=333328333350000)
Speedup:  370.88x
Correct:  YES

The Correct: YES line is the ground-truth comparison — a speedup with a correctness failure is a bug report, not a result, and the command tells you which one you have.

For structured measurement, python -m pyvorin bench file.py --json emits a report whose fields include metrics.fallback_count, report.fallback_used and report.fallback_reason, alongside the timing block. These fields are how you detect that a captured timing came from the interpreter rather than native code — check fallback_used before trusting any median, and treat a fallback timing as belonging to a different distribution entirely. When a workload you expected to compile shows fallback_used: true with a reason attached, you have found either a compiler gap worth reporting or a workload misclassified into the wrong suite.

For the fully manual route — measuring a PyvorinCompiler(backend="cpython") function — time it with the same warm-up and run counts you would use in a bench report, and pair it with a native run of the identical source so the comparison holds the workload constant. The discipline of pairing matters more than the timing library; a CPython median collected under different input sizes or run counts is not a baseline, it is a distraction.

A working pattern for A/B debugging

Putting the pieces together, the workflow we recommend when results look wrong: run the process under PYVORIN_BACKEND=cpython; if the anomaly persists, the bug is in your code and the compiler is exonerated. If it disappears, minimise the function until the divergence reproduces under backend="native" with allow_fallback=False, which pins the failure to the native path loudly and early. Then capture both engines' timings with bench --json — or run --compare for the quick view — and record backend_used alongside every number. That sequence turns "the compiled version behaves oddly" from an anecdote into a reproducible, correctly attributed report, which is the difference between a fix next week and a shrug next quarter.

Where to go next

Last reviewed 11 August 2026 against pyvorin-native 1.0.9 installed at /root/pvfinal. Every mechanism above was exercised live: backend="cpython", PYVORIN_BACKEND=cpython, run --compare and the auto-router fallback on a generator-expression workload all produced the quoted outputs on the verification host. The absence of PYVORIN_FORCE_FALLBACK and --fallback was established by searching the package and reading the CLI help in full.