Skip to main content
Back to posts

🥐 Python-style code in a 1.47 MB executable

niklas-heer/kipferl

Kipferl — Bake Python CLI apps into fast, standalone binaries.

17 1 Rust MIT Updated 2 days ago

I want writing a little command-line tool to be easy. I also want giving it to somebody else to be easy. Annoyingly, these are two different wishes.

Python is lovely for the first one. For the second, I would like to hand over one small file and say “run this”. No Python installation, no virtual environment, no conversation about which pip belongs to which interpreter.

That is the idea behind Kipferl: Python-style code, packaged into one small executable.

How small? The status app below, with a box, a table and colored output, builds to 1,467,617 bytes on Apple Silicon. About 1.47 MB.

Build a small CLI, then run the standalone executable. An actual recording with Kipferl 0.6.0 on macOS ARM64. The final command runs the built application, not the Python source.

The services in the table are made up. The file size is measured.

What actually goes into the file

Putting a Python program into an executable is not a new idea. What I wanted was the combination: a pleasant high-level language, useful terminal UI pieces, and an output file small enough to feel right for a small tool. A five-line script should not need a removal company.

Kipferl uses PocketPy, an embeddable Python implementation, with a host written in Rust and a curated set of modules for command-line apps. It packages your program together with the runtime it needs. It does not compile arbitrary CPython code to machine code, and it does not promise that every package on PyPI will work.

That trade-off is where the size comes from. You get a focused way to build CLIs instead of a suitcase with an entire Python installation in it.

The whole program in the recording

This is the complete program:

import tui
tui.box("Three services. One small executable.", title="Release check")
tui.table([
["Service", "Status"],
["api", "ready"],
["worker", "ready"],
["coffee", "critical"],
], headers=True)
tui.success("Application ready. Coffee needs attention.")

Build it, then run the result:

Terminal window
kipferl build status.py -o status
./status

Whoever receives status needs neither Kipferl nor Python. The file still targets one operating system and architecture, so “standalone” does not mean the same file runs everywhere.

Two runtime profiles

Kipferl 0.6 looks at the imports and picks a prebuilt runtime profile, erring on the side of caution. The status app fits the smaller core profile. Building the exact same source with --full-runtime includes the complete runtime instead.

SAME SOURCE · TWO RUNTIME PROFILES

The table fits. Most of the runtime stays home.

Core profile1.47 MB
Full runtime4.83 MB
69.6% smaller. Core: 1,467,617 bytes. Full: 4,834,705 bytes. Both are standalone builds of the recorded status app, measured on macOS ARM64 with Kipferl 0.6.0.

These are measured outputs of the example above with the released 0.6.0 builder on macOS ARM64. MB means one million bytes. The source, the profile and the target platform all change the result, so 1.47 MB is a number you can reproduce, not a ceiling for every app.

The 0.6 release notes list a separate minimal-app baseline of 1,450,837 bytes. The Kipferl builder itself is a larger, separate artifact. It is not the file you give to your users.

This is profile selection. Unused functions do not disappear one by one. When the analyzer cannot safely pick the smaller profile, keeping everything working wins over saving a few bytes.

Why I wanted this

For me the appeal is practical. A repository helper, a status report, a small interactive utility: useful enough to share, not worth turning into an installation project of their own.

I want the easy part of writing Python and the easy part of handing over a small native tool to meet in the middle.

A Kipferl is a pastry. Handing someone the app should be about that much trouble.