python-to-binary
py2bin turns Python into machine code using nothing but the Python standard
library. No Cython, Nuitka, mypyc, Rust, C, C++, PyInstaller, PPCI, bootloader,
assembler, linker or SDK - and no gcc or clang at any point. The only thing
a build needs is an interpreter.
pip install python-to-binary
py2bin compile-capi app.py --target darwin-arm64 -o app
Source, issues and the full documentation: https://github.com/yu314-coder/python_to_binary
Platforms
What compile-capi - the tier that turns your program into machine code that
drives CPython - can target today.
| x86-64 | arm64 | |
|---|---|---|
| macOS | ✅ works | ✅ works |
| Windows | ✅ works | ⬜ future work |
| Linux | ⬜ future work | ⬜ future work |
Each working target is held to the same standard: an 889-program corpus is compiled for it and every program's output and exit code compared against CPython's. macOS agrees on 878 and differs on 7; a 100-program slice run through Wine agrees on 93 and differs on 5. The differences are the same on every platform and are inherent rather than open - CPython's "Did you mean" needs a Python frame to suggest from, and the repr of a compiled function really is a builtin function's.
The native tier (py2bin compile, no CPython at all) targets all six.
The paths through it
Three ways to turn a program into an artifact. They trade the same three things against each other, and which one you want depends on which you care about.
compile |
compile-capi |
freeze |
|
|---|---|---|---|
| speed on a 30M-iteration loop | 0.062 s | 1.27 s | 0.66 s |
| artifact | 48 KB | 66 KB | tens of MB |
| needs Python on the machine? | no | yes, or bundle it | no, it carries one |
| how much Python works | a small subset | most of it: 878 of an 889-program corpus | everything |
| third-party packages | none | any the interpreter can import | carried inside |
| what actually runs your logic | machine code | machine code | CPython, interpreting |
compile is the fastest and the smallest. Python AST → py2bin IR →
optimizer → handwritten x86-64/ARM64 → ELF, PE or Mach-O. There is no
interpreter in the artifact and none on the machine: 11× faster than CPython on
that loop, in 48 KB that runs on a bare system. You pay for it in what it will
accept - integers, floats, strings, control flow, your own functions - and it
will not import a package at all.
freeze is the most complete. It ships your program beside an interpreter
that runs it, so NumPy, Torch and a GUI toolkit all work exactly as they do
now. Nothing is translated, so nothing is faster; the artifact is the largest
of the three because an interpreter and every dependency are inside it.
compile-capi is the middle, and the one under active work. It translates
ordinary Python into C that drives the CPython C API, then compiles that C with
py2bin's own C compiler - the tier Nuitka occupies, with Nuitka's dependency on
clang removed. Almost the whole language goes through, and anything the linked
interpreter can import still works, so a real application with pywebview and
Pillow compiles. Integer loops beat CPython because their locals are held in
registers; most other operations are slower, because each one is a real C-API
call where the interpreter has specialised bytecode. The per-feature table is below.
The loop above is deliberately unkind to compile-capi: its accumulator is
compared against a parameter, which the register analysis cannot claim, so the
fast path is off. On a loop it can claim, the same tier is 1.67× faster than
CPython.
Using it
pip install python-to-binary
| command | what it does |
|---|---|
py2bin compile-capi |
Python → C driving the CPython C API → machine code |
py2bin compile |
Python → machine code, no CPython anywhere |
py2bin compile-c |
py2bin's own C compiler, on your C |
py2bin freeze |
ship the program beside an interpreter |
py2bin targets |
list the targets this build knows |
Bundling a real application into a macOS .app that carries its own
interpreter and packages:
py2bin compile-capi app.py --target darwin-arm64 \
--app --name "My App" --icon icon.icns \
--embed-python --site ../Resources/site-packages \
--bundle-site /path/to/venv/lib/python3.14/site-packages \
--prune-unused --zip-stdlib \
-o dist/MyApp.app --clean
How it works
Nothing wraps a toolchain; each stage is a module you can read.
capi_emit.py Python AST -> C that calls the CPython C API
capi_ints.py which locals may live in a machine register
c_preprocessor.py #include, macros, conditionals
c_frontend.py C -> py2bin IR
native/ir.py the IR itself
native/optimizer.py constant folding, dead code, write merging
native/arm64.py IR -> ARM64 instructions
native/x86_64.py IR -> x86-64, System V and Microsoft x64
native/formats/ Mach-O, PE32+, ELF
freezer.py bundling: interpreter, packages, pruning, archives
cabi.py the vetted CPython entry points
So compile-capi is five stages, all of them in this package: capi_emit →
c_preprocessor → c_frontend → native.x86_64/native.arm64 →
native.formats.macho/pe.
There is no import ctypes anywhere on that path, which a test asserts by
compiling in a fresh interpreter and listing what got loaded. ctypes is
standard library and would pass an imports-only-stdlib check, but it pulls in
subprocess - and there are Pythons where a subprocess is not something a
program may have.
What compile-capi supports
Every row is checked by compiling it, running it, running the same source under CPython, and requiring identical stdout and exit status.
| feature | |
|---|---|
| int, float, str, bytes, bool, None | ✅ |
unbounded integers (2 ** 200 exact) |
✅ |
f-strings, format specs, !r/!s/!a |
✅ |
| list, tuple, dict, set, slicing, subscripts | ✅ |
| comprehensions and generator expressions | ✅ |
if / while / for / else, break, continue |
✅ |
chained comparison, ternary, and / or |
✅ |
functions: defaults, *args, **kwargs |
✅ |
| lambdas and closures | ✅ |
classes, __init__, methods, inheritance, super() |
✅ |
dunder methods (__repr__, __eq__, …) |
✅ |
| decorators | ✅ |
try / except / finally, with |
✅ |
import, from … import, relative imports |
✅ |
global / nonlocal, tuple unpacking |
✅ |
the whole program: every .py beside the entry is compiled in |
✅ |
__name__, __file__, inspect.signature on compiled functions |
✅ |
walrus (:=) |
✅ |
raise … from … |
✅ |
starred unpacking (a, *b, c = …) |
✅ |
match: values, | alternatives, captures, sequences, guards |
✅ |
match: mapping and class patterns |
❌ |
generators (yield) |
❌ |
async / await |
❌ |
What is left needs machinery this tier does not have rather than more
translation. A generator has to suspend and resume, which means a frame to
suspend into; async needs the same thing and a scheduler besides. A mapping
or class pattern needs the match protocol (__match_args__) walked, which is
tractable and simply not written.
A refusal is a file:line:col error, never a silent approximation. On an
889-program corpus, 878 programs produce byte-identical output to CPython; the
7 that differ do so inherently (CPython's "Did you mean" needs a Python frame,
"v" is "v" depends on interning) and 4 are refused outright.
How fast each one is
300,000 iterations, median of 5, against the interpreter it links. Higher is
better; 1.00× means the same speed as CPython.
| feature | py2bin | CPython | |
|---|---|---|---|
| integer arithmetic | 5.1 ms | 8.5 ms | 1.67× faster |
| comparisons | 3.8 ms | 4.7 ms | 1.24× faster |
while loop |
7.5 ms | 6.5 ms | 0.87× |
| direct function call | 7.9 ms | 6.4 ms | 0.81× |
| comprehension | 5.2 ms | 4.0 ms | 0.77× |
| dict store | 10.9 ms | 7.8 ms | 0.72× |
| subscript | 7.1 ms | 3.9 ms | 0.55× |
| attribute read | 7.5 ms | 3.8 ms | 0.51× |
| exception raise/catch | 13.8 ms | 6.7 ms | 0.49× |
| closure call | 14.0 ms | 6.5 ms | 0.46× |
| f-string | 13.6 ms | 5.1 ms | 0.38× |
| float arithmetic | 10.6 ms | 3.4 ms | 0.32× |
| list append | 19.1 ms | 5.3 ms | 0.28× |
| string concatenation | 24.3 ms | 3.3 ms | 0.14× |
| instantiation | 177 ms | 16.3 ms | 0.09× |
| method call | 142 ms | 6.7 ms | 0.05× |
Integer loops win because a local the analysis picks out is held in a machine register, with an overflow check that falls back to unbounded arithmetic when the value leaves the word. That is what CPython's specialising interpreter does, and doing anything less was what made this tier slower than not compiling at all.
Everything else loses, by a factor that tracks how many C-API calls the operation costs. Each one is a real call with the reference-count discipline around it, where the interpreter's specialised bytecode does the same work inline. Floats are not held in registers at all yet, which is the same job as the integers and not done.
Method calls and instantiation are far worse than the pattern predicts - 21× and 11× rather than the 2-4× everything else pays. That is not the C-API overhead; something in the class path is doing work per call that it should do once. It is the clearest thing to fix next and it is measured here rather than left out.
Measured against Nuitka
manim_app: 10,100 lines, pywebview + Pillow + pyobjc, built both ways on the same machine.
| py2bin | Nuitka | |
|---|---|---|
whole .app |
61.2 MB | 72.6 MB |
| main binary | 9.2 MB | 29.6 MB |
| bare interpreter start | 9.5 ms | 16.2 ms |
| start with the app's imports | 52.1 ms | 44.8 ms |
| compile time | 16.7 s | minutes |
Run time, median of 5, seconds:
| workload | py2bin | CPython | Nuitka |
|---|---|---|---|
| integer arithmetic | 0.050 | 0.084 | 0.095 |
while loop |
0.045 | 0.070 | 0.045 |
| nested loops | 0.022 | 0.035 | 0.042 |
| function calls | 0.065 | 0.023 | 0.020 |
| string building | 0.025 | 0.011 | 0.009 |
Loops beat both because a local the analysis picks out is held in a register rather than on the heap, with the overflow check that falls back to unbounded arithmetic when it leaves the word. Calls still lose: an argument is boxed at the call and unboxed inside, where the interpreter's specialised call pays neither.
Licence
MIT. Full documentation, source and issues: https://github.com/yu314-coder/python_to_binary
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_to_binary-0.4.0.tar.gz.
File metadata
- Download URL: python_to_binary-0.4.0.tar.gz
- Upload date:
- Size: 703.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51b781b1e24b718856e00ac644f022bca8b2200b4a5dbc91a1fe623340837968
|
|
| MD5 |
1c2ee7400cd28827ff47d9fde76cb98a
|
|
| BLAKE2b-256 |
b65b17aea8839a6f51d9ed5f5477745bc3b4b506f08d5ce08e9009be1658d2af
|
File details
Details for the file python_to_binary-0.4.0-py3-none-any.whl.
File metadata
- Download URL: python_to_binary-0.4.0-py3-none-any.whl
- Upload date:
- Size: 476.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4424bc378f43ce719cd871949dcff902821741ff040670fe00f7facdead22157
|
|
| MD5 |
df1128c94a5d6b56d56c0bb4321c739a
|
|
| BLAKE2b-256 |
0377f0f49b8fc84da072888565497cc44d8b10f9d58689bd0aca78f191703348
|