pycc-next 0.6.0: pip install compiles your project, pydantic goes 63% native, and a corpus goal I missed
0.6.0 is out. The theme was drop-in for real projects: you shouldn’t have to rewrite anything, or even run a new command, to get compiled code. Four of the five goals are met. The fifth isn’t, and it’s in the table below exactly as measured.
🏁 The scorecard
| goal | target | result |
|---|---|---|
pip install builds with pycc |
a PEP 517 backend, wheels of 3 real packages pass their suites | met: diff-match-patch, tomli, pypng built by pip wheel, suites pass on Linux x86_64 + aarch64 |
| untyped classes | pypng >= 60/85 functions, PngSuite decode >= 3x CPython | met, thinly: 64/85 (78 with method-level fallback), 3.18x |
real int \| float type |
CPython semantics for every operation | met: one machine word, pure int/float code unchanged |
| no hidden differences | a differential fuzzer, nightly >= 30 min | met: 3314 programs in 30 min, 0 findings; 18 earlier mismatches fixed |
| corpus, strict | >= 190/200 | ❌ 188/200 (from 183) |
| corpus, non-strict | 0 statements in CPython | ❌ 1/1191 (from 2), 200/200 correct |
Two misses, both in the corpus goal. They move to v0.7 with three tracked issues.
📦 pip install . now runs the compiler
The release archive ships a wheel of pycc.build, a PEP 517 (and PEP 660) build backend.
Point a project at it:
[build-system]
requires = ["pycc-build"]
build-backend = "pycc.build"
[tool.pycc]
strict = false # build every module --non-strict
pure-python = ["app.cli"] # keep this one as .py
and pip install . compiles every module to an abi3 extension (--ext), producing a
real cp313-abi3-manylinux_<glibc>_x86_64 wheel with a proper RECORD and METADATA. Modules
can be kept in Python, or allowed to fall back, one by one. A module pycc refuses fails the
build with pycc’s own diagnostics: no silently slower wheel.
The proof is in CI: diff-match-patch, tomli and pypng are built by plain pip wheel,
installed into fresh venvs, and their own test suites run against the installed wheels.
The backend needs no network and runs no project code.
🔢 int | float, for real
Python code mixes int and float all the time: best = float('inf') later replaced by an
int, an accumulator starting at 0 that receives floats. 0.5 handled the infinity case with
a trick and a list of documented differences. 0.6 replaces the trick with a real type.
An int | float is one machine word extending the int encoding: a small int, a heap big
int, or a float stored inline when its magnitude is roughly between 1e-77 and 1e77 (boxed
otherwise). Every operation is CPython’s: //, %, **, exact mixed comparisons with big
ints, round, hash, f-string formatting. Pure int and pure float code compiles
exactly as before. The old DEVIATIONS rows are gone.
🎲 A fuzzer that argues with CPython
New in fuzz/: a generator of typed Python programs. Each runs under CPython 3.13 and in
four pycc builds (native strict, --non-strict, --ext, --ext --non-strict). Stdout,
the uncaught exception and the exit status must all agree; a mismatch is reduced to a
minimal program automatically.
About five hours of local runs found 18 real mismatches: deque.extend ignoring
maxlen, repr() of floats with two shortest representations, a set display iterating in
the wrong order, for ... else running else after break, and more. Each is fixed with a
regression fixture. The last 30-minute nightly run: 3314 programs, 0 findings.
🖼️ pypng: classes compile, 3.18x on PngSuite
0.5 made pypng’s scanline filters fast, but its Reader class stayed in CPython, so tiny
images decoded only ~1.6x faster. Now untyped classes compile under --non-strict: chunk
handlers looked up by getattr(self, "_process_" + kind), generator methods, bytes
literals, *args calls, except struct.error:.
Decoding every PngSuite image its tests use: 3.18x faster than CPython (18.8 ms vs
59.8 ms, best of 15 interleaved runs), 4.57x fewer instructions under callgrind. Honest
footnote: the target was 3x and the median is 3.16x. The margin is thin. What’s left is
mostly CPython itself: zlib, struct, array.
🧬 Method-level fallback, and pydantic at 63%
This is v0.7 work that landed before the release, so it ships in 0.6.0.
Before, one construct pycc couldn’t compile sent its whole class to CPython. Now only the
smallest enclosing function or method goes there. The class keeps every other method
compiled: its defs forward to compiled bodies, with self, super() and name mangling
behaving as in CPython. Classes inheriting from library classes compile too:
threading.Thread, collections.Counter, argparse.Action, metaclasses, descriptors,
@property, cached_property, the with statement.
So I pointed it at pydantic 2.13.5 (its Python part, pinned by sha256), built module by
module with --ext --non-strict:
- 789 of its 1249 functions compile natively, and every module builds (dozens failed to build before this release’s work).
- 5716 of its 5740 tests pass against the compiled tree. The 24 failures: tests that
monkeypatch module attributes compiled code doesn’t see,
sys._getframefrom compiled frames, and four subprocess tests that fail under plain CPython too. - Speed: not measured yet. I’m not quoting a number I don’t have.
pypng went from 64 to 78 of 85 functions native with the same change.
⚡ Benchmarks
Fresh run on this commit, python3 bench/run.py, best of 7, on a shared 4-core Xeon 2.1 GHz
that was busy with other builds:
| program | CPython | pycc-next | Rust | vs CPython | vs Rust |
|---|---|---|---|---|---|
| collections_bfs | 0.900s | 0.144s | 0.049s | 6.3x faster | 2.92x slower |
| fib | 0.282s | 0.013s | 0.007s | 21.7x faster | 1.91x slower |
| mandelbrot | 1.545s | 0.043s | 0.042s | 35.5x faster | 1.03x slower |
| nbody | 2.530s | 0.067s | 0.025s | 37.7x faster | 2.68x slower |
| prefix_sum_np (kernel) | 2.798s | 0.029s | 0.013s | 95.4x faster | 2.19x slower |
| sieve | 1.000s | 0.048s | 0.021s | 20.7x faster | 2.35x slower |
| spectral_norm | 0.836s | 0.018s | 0.011s | 45.5x faster | 1.69x slower |
| stencil2d (kernel) | 3.751s | 0.007s | 0.003s | 530.5x faster | 2.17x slower |
Read the ratios carefully: sieve shows 20.7x here against 37.5x in 0.5.0’s notes, and fib 21.7x against 26.5x. pycc’s own times are about the same or better (sieve 0.048 s vs 0.057 s). CPython just ran faster on this run, so the ratio shrank. I’m keeping the number as measured anyway.
❌ The goal I missed
The corpus is 200 real competitive-programming solutions. The target was >= 190 compiling unchanged in strict mode, and zero top-level statements falling back in non-strict mode. I got 188 and 1.
The remaining strict refusals are mostly legitimately dynamic Python: mixed-type
containers, an int | None used as an int without a None check, a variable rebound
along different paths. The last fallback is one function, problem 188’s mat_pow. It
rebinds its list parameter to a list of lists under an isinstance test, and its nested
helper is called with both. Fixing it needs three separate compiler changes (issues #5,
#6, #7). I’d rather ship 0.6 with an honest ❌ than hold four finished goals hostage to one
function.
🪟 Bonus: Windows
Also early from v0.7: a Windows x86_64 archive. pycc builds native .exe programs and
abi3 .pyd extension modules with the MSVC toolchain, and pycc.build tags wheels
cp313-abi3-win_amd64. CI runs the full test suite, the corpus gate and the real-package
wheel checks on windows-2025, and the release workflow smoke-tests the zip before
publishing. It’s new, so it has its own documented differences (stdout is \n-terminated
UTF-8, no static libpython). The benchmarks above are Linux only; I haven’t timed Windows.
🔭 Next: v0.7
The carried-over corpus goal, more framework-shaped code, and measuring pydantic’s speed now that most of it compiles.
👉 Release v0.6.0: Linux x86_64, Linux aarch64, macOS arm64 and Windows x86_64 archives, each with its sha256.