Repository navigation
pycc-next v0.6.0
Drop-in for real projects. pip install builds a project with pycc through a PEP 517
backend shipped in the release archive; strict mode gets a real int | float type with
CPython's semantics for every operation; a differential fuzzer compares generated
programs against CPython in four build modes (0 findings in its last 30-minute nightly
run, 18 mismatches found and fixed on the way); untyped classes compile under
--non-strict, so pypng decodes the PngSuite images its tests use 3.18x faster than
CPython (v0.5: ~1.6x). Also shipped, ahead of v0.7: a Windows x86_64 archive (MSVC
toolchain, abi3 .pyd modules, win_amd64 wheels), method-level fallback (only the
smallest enclosing function or method of a construct pycc cannot compile runs in
CPython) and classes inheriting library classes, which take pydantic 2.13.5 to 789 of its
1249 functions compiled with 5716 of its 5740 tests passing (its speed is not measured
yet), and pypng to 78 of 85. Not met: the real-world corpus target (ROADMAP v0.6 accept
5) -- strict compiles 188/200 (target 190) and --non-strict keeps 1 of 1191 top-level
statements in CPython (target 0); carried over to v0.7. Prebuilt archives: Linux x86_64,
Linux aarch64, macOS arm64 and, new, Windows x86_64.
Benchmarks (python3 bench/run.py, best of 7, shared 4-core Xeon 2.1 GHz, load average
6-11 from other jobs; process start included except for the numpy kernels):
| program | CPython | pycc-next | Rust | pycc vs CPython | pycc 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 |
Against 0.5.0's table (same machine type, different load): fib, sieve and the numpy
kernels read lower (fib 21.7x against 26.5x, sieve 20.7x against 37.5x, prefix_sum_np
95x against 135x), nbody and mandelbrot higher; this run shared the box with other
builds, so treat the differences as noise until re-measured on a quiet machine.
Added
-
Windows x86_64 (v0.7 accept 1):
pycc-vX.Y.Z-x86_64-windows.zip(bin\pycc.exe,
lib\pycc\pycc_rt.lib, the backend's wheel) built, packaged and smoke-tested by the
release workflow onwindows-2025with the MSVC toolchain, and verified by its publish
job with the other archives. Native programs, abi3.pydextension modules (linked
againstpython3.lib, CPython 3.13+) and programs embedding CPython;pycc.build
tags wheelscp313-abi3-win_amd64. pycc driveslink.exethroughclang.exe
(PYCC_CLANG,PATH, Visual Studio's clang component, or the LLVM installer's);
PYCC_PYTHONdefaults topython.exe. CI runs the test suite, the non-strict corpus
gate and the real-package checks (bench/package_check.py,bench/wheel_check.py) on
windows-2025; LLVM is LLVM's own 22.1.8 Windows release, pinned by sha256
(scripts/windows_llvm.sh). The memory tests measure the peak working set there. -
macOS CI also builds the real packages into wheels with
pip wheel
(bench/wheel_check.py). -
Method-level fallback (v0.7 accept 3): under
--non-strict, a construct pycc cannot
compile sends only the smallest enclosing function or method to CPython, not its
whole class or module statement. A class statement CPython runs keeps each method's
body compiled (itsdefforwards to a compiled function;selfcrosses as the
instance,super(),__class__and private-name mangling as in CPython); a
decorated top-level function keeps itsdefin CPython and its body compiled; in an
--extmodule a compiled class keeps its native type, a method that cannot compile
attached to it as a CPython function (src/fallback/lift.rs,src/fallback/attach.rs,
--report-fallbacksreports each). -
Classes from libraries (v0.7 accept 2):
--non-strictcompiles the methods of classes
inheriting CPython classes (threading.Thread,collections.Counter,
argparse.Action, anamedtuple, library exceptions,class M(lib.Base)), with
metaclasses,__init_subclass__, descriptors, class attributes of any type,
@property,@staticmethod,@classmethod,functools.cached_propertyand
functools.wraps-style decorators, all applied by CPython; annotations naming
library types (re.Match,mod.Type), general unions andX | Noneof objects
(objects); thewithstatement (PEP 343;async withstill runs in CPython), and
starred expressions in displays, assignment targets and calls. A compiled generator
handed to CPython supportssend(None),throw()andclose()(as
contextlib.contextmanagerneeds). A class Python code in the package subclasses
(its own fallback code or a sibling module) is created by CPython, since a published
native class is not subclassable; private module functions are published by
non-strict--extmodules, so sibling modules can import them; an--extfunction
whose signature the boundary cannot bind (**kwargs, positional-only parameters of a
public function) runs in CPython.from __future__ import annotations as xand
augmented assignment to a computed target (f()[k] += 1) compile. -
bench/pydantic_check.pybuilds pydantic 2.13.5's Python part (pinned by sha256)
module by module with--ext --non-strict, counts the functions that compile, and
optionally runs pydantic's own test suite against the compiled tree: 789 of its 1249
functions compile and every module builds (33 and 55 modules failing to build
before this release's work); 5716 of its tests pass, 24 fail (monkeypatched module
attributes compiled code does not see,sys._getframefrom compiled frames, and
four subprocess tests that fail in CPython too). -
pip installbuilds with pycc (v0.6 accept 1):pycc.build, a PEP 517 (and PEP 660)
build backend inpython/, shipped in the release archive as
python/pycc_build-X.Y.Z-py3-none-any.whl. A project selecting it compiles each of
its modules withpycc build --ext(strict by default;[tool.pycc]sets
--non-strict, per-module overrides, modules kept in Python, modules allowed to fall
back to Python, package discovery forsrc/and flat layouts, extra pycc flags) and
gets acp313-abi3-<platform>wheel (manylinux_<glibc>_<arch>, orlinux_<arch>when
a module links a library manylinux does not allow;macosx_<N>_0_arm64) with METADATA
from static PEP 621 metadata, entry points and a sha256 RECORD; also sdists and
editable installs (uncompiled). A module pycc refuses fails the build with pycc's
diagnostics. It finds the compiler as$PYCCorpycconPATH, needs no network,
and runs no project code. README: "Use pycc in your project". -
CI builds diff-match-patch, tomli and pypng into wheels with
pip wheeland the
release archive's backend (bench/wheel_check.py, Linux x86_64 and aarch64), installs
each into a fresh venv and runs the package's own test suite against it; the release
smoke test (scripts/smoke.sh, all three platforms) builds and runs a small project's
wheel the same way. -
A differential fuzzer (v0.6 accept 4,
fuzz/): generated typed programs run under
CPython 3.13 and as native strict,--non-strict,--extand--ext --non-strict
builds; stdout, the uncaught exception and the exit status must agree. Refusals are
counted per feature and mode, compiler crashes and mismatches are findings, each
reduced to a minimal program. CI runs it for 2 minutes per push and 30 minutes
nightly (.github/workflows/fuzz.yml, findings uploaded as an artifact). -
Untyped code compiled natively by
--non-strict(ROADMAP v0.6 accept 2, in progress):bytesliterals, as CPythonbytesobjects (strict mode still refuses them,C0001);- calls unpacking arguments into a CPython callee --
f(*args),o.m(a, *xs, b, k=v, **kw),zip(*rows)-- bound at run time with CPython's checks andTypeError
messages; a builtin called with keyword or unpacked arguments is CPython's own; exceptclauses whose type is a CPython object (except struct.error:,except mod.Error as e:,ethe CPython exception), tested in order with the other clauses;- an object tested against a tuple, list or set of scalars (
kind in (1, 2, 3, 4)); - a standard-library function pycc compiles (
math.ceil, ...) called with a CPython
object is CPython's own; - builtin exception and warning classes,
bytes,bytearray,getattr, ... as values
(warnings.warn(msg, RuntimeWarning)); - a builtin call inside an
--extmodule's public generator is routed through CPython
like one anywhere else (its diagnostic is now placed); - untyped classes: attributes a method creates outside
__init__(self.width = ...
in a header parser) areobjectslots, an unset one raising CPython's
AttributeError; a method's keyword calls of its own methods (self.chunk(lenient= True)), generator methods, a method read as a value (self._scaled(self.size, k)),
nested functions defined under anif(each branch'sdef newarray()), slice stores
on objects, and private methods CPython code looks up (getattr(self, "_process_" + kind)), published in non-strict mode when their parameters are all objects; - a module-level function used as a value crosses into CPython as a callable (
fn = (None, sub, up)[k]; fn(...)), one object per function; - a helper parameter defaulting to
Noneand called with anintis an optional
int; one rebound to an object is an object; - a CPython object compared with a list, tuple or set of scalars, indexing a tuple of
one element type, or put in a display together with a generator; - conditional expressions joining two generators;
bytearray(...)locals are native byte buffers (items,len, iteration,append
andextendofints run natively), and the guarded native specializations cover
private methods too, iteration of a byte buffer (for o in bs:), and the integer
attribute a method reads first (self.bitdepth), passed in as anint;- a constant set operand of a comparison (
t <= set(range(65, 91)) | set(range(97, 123))) is built once, as a module-levelfrozenset; standard-library functions an
object call reaches are looked up once. - a guarded specialization also guesses
bytearrayfor a parameter every call of the
function in the module passes abytearray(...)(or a local bound only to one):
pypng's_deinterlace(self, raw)used to be guessedstrfromraw[i]/raw[i:j]
alone, so its specialization never ran.
-
pypng 0.20220715.0: 64 of its 85 functions compile natively (15 in 0.5.0; 78 with
method-level fallback, above), its own suite passing;bench/package_check.py --timealso times decoding every PngSuite
image its tests use. -
int | float(v0.6 accept 3): a real union type in strict mode. A name bound to an
intand to afloat(afloat('inf')sentinelminreplaces with anint, an
accumulator that starts at0and receives floats, a branch binding each), a list
display or.appendof both, a dict display's values, anint | floatannotation,
parameter, return, instance attribute, dict key or--extparameter/result. Every
operation is CPython's for whichever value it holds: arithmetic including//,%
and**, exact mixed and big-intcomparisons,abs,round,int(),
float(),math.floor/ceil,str/repr/f-strings/format specs,hash,sum,
min/max,sorted, membership. The value is one machine word extending theint
encoding: a smallint, a heap bigint, or afloatstored inline when its
magnitude is between about1e-77and1e77(boxed otherwise), so pureintand
purefloatcode is unchanged. Tests:tests/num_union.rs(strict,--non-strict,
--ext, leak probe). -
Strict mode compiles constructs the fuzzer counted among its most frequent refusals
(tests:tests/strict_coverage_v06.rs, each fixture native strict,--non-strictand
--ext):break/continueout of atrywith afinally:clause (thefinally
block runs first, through handlers,else:and nestedtrys); the f-string=debug
specifier, with a conversion or format spec;itertools.takewhile/dropwhilewith a
lambda or function predicate; amatchon a tuple subject mixing literal, capture and
starred patterns (it was refused asT0021"literal pattern str does not match subject
type int"); an empty list display passed straight to a builtin that cannot observe its
element type (print([]),len([]),sorted([]),[] * n);str()of tuples,
sets and optionals andbool()of every native value;cls(...)in a@classmethod,
constructing the receiver's class also when a subclass inherits the classmethod (whose
return type is then that subclass, as fortype(self)(...)); and a class attribute
the program assigns through its class (Base.count += 1), as long as every access
spellsBase.count(docs/DEVIATIONS.md); andf(**{'name': 'c', 'z': 3}), a dict
display of identifier keys unpacked into a call of a module function, bound as the
keyword arguments it spells (whatever its value types), raising CPython'sTypeError
at run time when it cannot bind. -
Real-world corpus (v0.6 accept 5, strict 183 -> 188/200,
--non-strictfallback
statements 2 -> 1 of 1191), each tested native strict,--non-strictand--ext
against CPython:- containers own big
ints and boxedint | floatfloats: a list, tuple, dict or set
holds them (they raisedOverflowError, and a literal wasT0051); tuples whose
numbers differ in kind compare by value, and afloatkey finds the equalint
key of adict[int, ...](tests/num_union.rs); T | None(tests/optional_values.rs): in strict modestr | None, container and
instance optionals as locals, parameters and returns; lists and dict values of
int | Noneand pointer optionals (parents = [None] * nthenparents[v] = u,
["a", None]);x == None/x != v;while x is not None:andif x:narrowing.
An unnarrowedint | Noneused as anintstays a compile error;tuple(xs)of a list orstr: a nativetuple[T, ...]that a list can hold,
unpacked with CPython's length check (tests/tuple_of_list.rs);--non-strict: a dict comprehension with a CPython object key or value, or over a
CPython object, builds the CPython dict natively (tests/object_dict_comprehension.rs).
- containers own big
Removed
- The v0.5
intinfinity:float('inf')in a variable anintlater replaces is now
anint | float, sox // 2,x - xand storing it into a container give
CPython's results instead ofOverflowError.
Changed
- CI runs
cargo clippy --release --workspace --all-targets -- -D warnings(Linux
x86_64), and the tree is clippy-clean. The runtime's closure, generator and exception
incref/decrefentry points andpycc_rt_hash_strare nowunsafe extern "C"with
documented pointer contracts (they dereference their argument); their symbols and ABI
are unchanged. - Faster
intcode: a function'sintlocals whose every value is proven a smallint
(an interval analysis over its assignments,range()loops, byte buffer items,
& k,% k,abs(), ...:pycc_codegen'sint_range) compute on the tagged words
directly -- no tag tests, overflow checks, runtime fallbacks or bigint releases --
andabs()of a number no longer checks for a pending exception. pypng's
undo_filter_paethloop runs about 3x fewer instructions. - Faster non-strict code calling CPython: a module function used as a value is boxed
without rebuilding its closure each time, and called through CPython's vectorcall
protocol (no argument tuple); a constant tuple or list a membership test reads
(kind not in (1, 2, 3, 4)) is built once per site; a standard-library function
called with a CPython object (math.ceil(o)) is found again by its site;
bytearray.extendof a native list stores its bytes in one pass, and
b.extend([e for v in xs])reuses one list per site wheneruns no other code. - Faster runtime helpers: a list comprehension over a named list allocates its result at
the source's length,list.appendstores in place when there is room, freeing a list
of smallints releases nothing,int ** intof machine-sized operands and a bare%d
of an inlineintskip the bigint and general formatting paths.
Fixed
int | floatmemory safety: afloatneedle or key for anint | floatcontainer
(1e100 in xs,d[1e-100],d.get(x, ...), and a refused store) leaked the boxed
float it was converted to; the runtime's refcount paths read a boxed float through a
bigint-sized reference (undefined behaviour), and anint-only reader handed one would
have read itsf64and the memory past it as bigint limbs -- it now panics instead.- Non-strict: compiled code reading a module global that fallback code binds, before it
is bound, raisedKeyError: 'name'; it now raises CPython'sNameError: name 'name' is not defined. floatfloor division and modulo by zero raisedZeroDivisionError: float floor division or modulo by zero; the messages are now CPython 3.13'sfloat floor division by zeroandfloat modulo by zero.
Found by the fuzzer, each with a regression test in tests/fuzz_regressions.rs:
- A
--non-strictnative program under the C/POSIX locale: code running in the
embedded CPython printed non-ASCII text asUnicodeEncodeError(its locale is now
configured aspython3's, with UTF-8 Mode). deque.extend,deque += ...anddeque.insertignoredmaxlen.- An
--ext --non-strictmodule's compiledexcept AppError:missed an exception of
a subclass running in CPython (one defining its own__init__): the bridge into
compiled code now maps a CPython exception to the most derived exception class the
module publishes. - A
KeyErrorfor anintor tuple key leaving an--extmodule (or the native
program embedding CPython) printedKeyError: '0'instead ofKeyError: 0. repr()of astrleft charactersstr.isprintable()rejects beyond Latin-1
unescaped ('\xa0','','\U000e0001').pow(a, b, m)with a bigint operand andround()of a float beyond 63 bits raised
"not supported yet".float ** floatoverflow raisedOverflowErrorwith pycc's own text instead of
CPython's(34, 'Numerical result out of range').- A refused
f(**mapping)call near the end of a file panicked the diagnostic renderer. --ext --non-strict: a public exception class with its own__init__, methods or
class attributes was published without them (SubErr(5)from Python skipped
__init__); it now runs in CPython.- A set display of three or more constants iterated in insertion order instead of the
order of CPython's folded frozenset constant ({-9, 6, 2, 7}prints{2, -9, 6, 7}).
The same held for such a display built as a CPython object (--non-strict:
str({...}),{...} | o). repr()of a float with two shortest round-tripping strings picked Rust's instead of
CPython's (nearest the exact value, ties to even:137931034482758.62).for a, b in zip(xs, ys)read the lengths once: a body shrinking a list raised
IndexError, one growing it stopped early.--non-strict: astrmethod run through CPython inside another call's arguments
(xs.append(s.removeprefix(p))) was called as the outer method onstr.match:case [x, *rest]bound the whole subject torest; an or-pattern inside a
sequence, mapping or class pattern required every alternative; a mapping pattern
raisedKeyErrorfor a missing key. A star before the end of a sequence pattern and
**restare now refused instead of miscompiled.print(d, d.popitem())printed a dict or set argument before later arguments changed
it.--non-strict: a set display meeting a CPython object crossed as afrozenset.--ext --non-strict: a twinned function was defined at the end of the module when no
blank line followed itsdef, so CPython code right after it raisedNameError.int()andfloat()of astrrefused Unicode decimal digits (int('١٢')).- A
for ... elseover astror tuple ran itselse:clause afterbreak. - A function returning a generator without yielding itself (
return lengths(rows)) ran
as a generator that ended at once, so its caller saw no items. - The guarded specializations of an
--extmodule were all dropped when any function
bound the nametypelocally (length, type = struct.unpack(...)).
Known gaps
- Real-world corpus (ROADMAP v0.6 accept 5, not met): strict 188/200 (target 190);
--non-strict200/200 with 1 of 1191 top-level statements in CPython (problem 188's
mat_pow). The strict refusals are heterogeneous containers, anint | Noneused as
anintwithout aNonetest, path-dependent rebinding anditer/rangeobjects as
values. Carried over to v0.7 (issues #5, #6, #7). - Windows x86_64 is new: a native program's stdout is
\n-terminated UTF-8 whatever the
console code page,--static-libpythonis refused, and the compiler uses the system
allocator (docs/DEVIATIONS.md). The benchmark table above is Linux x86_64 only. - pydantic under
--ext --non-strict: 24 of its 5740 tests fail (monkeypatched module
attributes compiled code does not see,sys._getframefrom compiled frames, four
subprocess tests that fail in CPython too); 460 of its 1249 functions still run in
CPython. - Open rows of
docs/DEVIATIONS.md, among them: anintbeyond 63 bits as a slice bound
andint ** negative int; container element and key types strict mode refuses
(T0034-T0039); unsupported constructs refused in strict mode (C0001) or run in
CPython under--non-strict; a published native class is not subclassable from
Python outside its package;async withruns in CPython; a compiled generator handed
to CPython accepts onlysend(None); an uncaught exception in compiled code prints
only the last traceback line;raise X from Nonekeeps__suppress_context__false;
reference cycles are never freed and__del__is never called; a bigintheld by a
tuple is never released; enum members at the--extboundary (C0003). See
docs/DEVIATIONS.mdfor every known difference from CPython 3.13.