Migrating from Cython to Pyvorin
What Cython's static typing bought you, what changes when the annotations come off, which modules should stay, and the commands to prove each migration.
Published Jul 2, 2026
A Cython module is a contract written twice: once as Python, once as cdef declarations, with a build system enforcing the second version. Pyvorin Native 1.0.9 removes both halves of that contract — the compiler reads plain Python and needs no build step — which makes migration less about translation and more about deciding which of your .pyx files were buying type safety and which were buying speed. This page covers that decision, the verified mechanics of dropping the annotations, and what to delete from your build system afterwards.
Everything below was run against the installed pyvorin-native 1.0.9 build; commands and outputs are captured, not assumed.
What Cython was actually doing for you
Strip the folklore and Cython delivers three concrete things. Static typing: cdef int total and typed memoryviews let the compiler keep values in C registers instead of Python objects, which is where the speed comes from. AOT packaging: cythonize() produces a compiled extension you ship as a wheel, so the machine code exists before any user imports it. And an export surface: cpdef functions are callable from both Python and C, which makes Cython the standard way to put a Python algorithm inside a larger C or C++ program.
Pyvorin buys speed a different way: whole-function compilation from plain source, locally and in-process, with no annotations and no wheel-time codegen. That trade is excellent for some of what Cython does and worthless for the rest. Knowing which is the migration.
When Cython still wins
Three cases should stay on Cython, and none of them is a performance argument.
You ship a C ABI. If other C code links against your extension, or you export cpdef entrypoints to a C caller, Pyvorin cannot replace that surface — its compiled artefacts are consumed in-process by Python callers, not linked as a library dependency.
You want AOT artefacts in the wheel. Pyvorin's compiled code lives in a local disk cache, created at compile time on the machine that runs it, deliberately not redistributed. If your deployment model forbids compilation anywhere at run time — sealed appliances, import-time audit gates — Cython's build-time .so remains the honest choice. The cache mechanics are documented on the artifact caching page.
Your team wants explicit types as documentation. A .pyx file is a specification. Pyvorin infers types instead of asking for them, which is less code but also less contract. For a module where the typing is the point — a reference implementation of a numeric routine, say — keep it.
Everything else — internal hot loops, batch transformations, the .pyx files that exist purely because CPython was slow — is migration territory. The fuller head-to-head lives on the Pyvorin vs Cython page.
The mechanical migration
Inventory first:
find src -name "*.pyx" -o -name "*.pxd" | sort
The translation itself is subtraction. A representative Cython function:
# Before — src/scoring.pyx
# cython: boundscheck=False, wraparound=False, language_level=3
import numpy as np
cimport numpy as cnp
def double_count(int[:] values, int target):
cdef int total = 0
cdef Py_ssize_t i
for i in range(values.shape[0]):
if values[i] == target:
total += 1
return total * 2
becomes, after dropping the directive header, the cimport, the typed memoryview and every cdef:
# After — src/scoring.py
def double_count(values, target):
total = 0
for i in range(len(values)):
if values[i] == target:
total += 1
return total * 2
The same function body, three declarations lighter. .pxd files, DEF/IF compile-time conditionals, and fused types have no direct equivalent — inline the constants, resolve the conditionals for your supported platforms, and pick the concrete type per function. None of these appeared in most codebases we have seen; check your own inventory before promising a deadline.
Then verify against the installed build:
python -m pyvorin support src/scoring.py
python -m pyvorin compile src/scoring.py --explain-fallback
python -m pyvorin bench src/scoring.py --function double_count --runs 5 --warmup 2
On our test run the migrated double_count reports COMPILED_FULL and answers well under a millisecond. When it does not, --explain-fallback names the construct in plain text — a caller falling back because it calls a compat-mode function, an optionally-typed variable, an unsupported call — and the fix is usually a small rewrite, documented in the debugging pages.
What changes under you: overflow and typing
One semantic difference deserves its own section, because it is the one that can silently change answers. In Cython, cdef int total is a C int: it overflows at 2,147,483,647 and wraps, by design. In plain Python, integers are arbitrary precision and grow forever. Dropping the annotations therefore changes arithmetic semantics — usually for the better, occasionally in ways that masked bugs now surface.
Pyvorin's benchmark report carries the field that lets you watch this: overflow_count appears in bench --json output alongside status, fallback_used and correct, and the compiler exposes an enable_overflow_checks option. For migrated financial or counting loops, check that field during the proof run; a non-zero overflow_count where Cython used to wrap silently is a correctness finding, not a performance one.
The reverse case — code that relied on C-level coercion of floats to ints — fails loudly in plain Python instead, which is the failure mode you want during migration rather than in production.
Build-system removal
This is where migration pays its biggest dividend: deleting machinery. A typical Cython build carries a pyproject.toml build requirement, a setup.py cythonize() call, pinned Cython versions, and CI jobs that compile .pyx files with a C toolchain. After migration all of it goes:
- Build requirements. Remove
Cythonfrom[build-system] requiresand thecythonize(...)call fromsetup.py. The package becomes a pure-Python wheel, which needs no C compiler on the consumer's machine and builds in seconds. - CI compile jobs. Delete the steps that invoke the Cython build and the matrix entries that exist only to test C-toolchain combinations. Replace them, if you want a gate, with
python -m pyvorin compile src/ --fail-on-fallback— exit code 2 on any fallback — so the pipeline fails when a claimed-native module regresses to the interpreter. - Distribution artefacts. Platform-specific wheels (
manylinux_x86_64and friends) collapse into one pure-Python wheel per Python version, since compilation now happens on the running host inside the install tree's cache, not at package time.
One thing to add: pyvorin-native itself, with its own constraints — CPython 3.11 or 3.12 and an x86_64 Linux wheel for the current build — documented in the support matrix. Migration moves the toolchain burden from your build farm to a pinned dependency; that is a good trade only if your deployment targets match the wheel platform, so check that matrix before deleting the C compiler from CI.
A verification protocol per module
As with any migration, prove per module, not per project:
python -m pyvorin support module.py— classify every function:COMPILED_FULL,COMPILED_PARTIAL,COMPATIBILITY_EXECUTED,FAILED.python -m pyvorin compile module.py --explain-fallback— read the reason chain for anything not full; fix the root construct the chain points at.python -m pyvorin bench module.py --function entry --json— assertcorrect: true, inspectoverflow_countfor arithmetic loops, and record the speedup with compile time reported separately.- Run the module's existing test suite unmodified. The migrated file is plain Python; every test that passed against the
.pyxversion must pass against the.pyversion before the old file is deleted.
A worked judgement call
The inventory says nine .pyx files. One of them exports cpdef entrypoints consumed by a C++ scoring service; the rest are internal batch loops. The temptation of a completed migration — a repo with zero .pyx files, one build system lighter — is real, and it is the wrong target. Migrate the eight internal loops, bank the simpler packaging, and leave the C ABI module on Cython with a comment explaining why it is exempt. A migration that keeps one honest exception is finished. A migration that forces a C++ service through a foreign-function shim to satisfy a zero-.pyx metric has traded a build system for an integration problem, and the scorecard that produced that trade should be retired.
Where to go next
- Pyvorin vs Cython — the full workload-fit comparison, stated fairly on both sides.
- Artifact caching, explained — where the compiled code lives now that it is not in your wheel.
- Quick start — install pyvorin-native and compile a migrated module.
- Benchmarks — the canonical numbers to sanity-check a migrated module against.
Last reviewed 2 July 2026 against pyvorin-native 1.0.9 installed at /root/pvfinal. Commands, status reports and JSON fields quoted on this page were captured from real runs on 13 September 2026; the support-matrix constraints are read from the installed package metadata.