pyappdist
Build native installers for Python apps without hunting down hidden imports, data files, or plugins.
Warning
Alpha. pyappdist is under active development. It works end-to-end today, but the config schema, CLI, and output layout may still change without notice.
pyappdist does not freeze your code. Instead of bundling Python and your app
into a single executable (and fighting hidden imports, data files, and plugins
along the way), it installs your app into a real, dedicated Python runtime —
exactly the way pip would — and ships that.
Because the runtime is a normal Python environment, most apps run as-is: no
hooks, no --hidden-import, no --add-data, no per-library workarounds. If
it runs under uv run, it almost certainly runs after pyappdist build.
Shipping a full runtime without selecting files makes packages larger, but against modern storage sizes that is a favorable trade for an environment that just runs.
Output formats
One pyproject.toml can describe several output packages at once — each is a
target with its own platform and format:
- msi
windows-x86_64→.msiinstaller.- msix
windows-x86_64→.msix(Store / sideloading).- linux
linux-x86_64→.runinstaller.- macos
macos-aarch64→.runinstaller, for command-line tools.- macapp / dmg
macos-aarch64→ a signed/notarized.appbundle, optionally inside a.dmg, for GUI apps.- image
any platform → a
.zip/.tar.gzof the image tree, with no installer.
Why “just works”
Freezers reconstruct your app by static analysis, so anything dynamic tends to break and needs manual patching. pyappdist skips all of that:
Real install layout —
dist-info, entry points,.pthfiles, and package data are exactly where the package authors put them.importlib.metadata/importlib.resourcesbehave identically to a normal install.Real binary wheels — C extensions and platform wheels (incl.
abi3) are installed unmodified, with the real interpreter and DLL search paths. No bundling guesswork.Real GUI stacks — Qt plugins ship in PySide6’s normal wheel layout; matplotlib’s TkAgg backend uses the runtime’s bundled tkinter. They just load.
On Windows the launcher is a tiny C stub that starts the bundled python.exe /
pythonw.exe as a subprocess, so there is no pythonXX.dll embedding and no
C-API version risk — the stub never changes when the Python version does. The macOS
.app uses an equivalent compiled Mach-O stub; Linux and the macOS .run
use a relocatable shell wrapper. See How it works.
Getting started
- Installation
- Tutorial: installers from scratch
- Step 1 — Scaffold a package with uv
- Step 2 — Write the tkinter “hello world”
- Step 3 — Test that the project builds a wheel
- Step 4 — Test the launcher command from the installed wheel
- Step 5 — Add the pyappdist configuration
- Step 6 — Build the installer
- Step 7 — Confirm the
upgrade-code - Step 8 — Add a Linux
.runinstaller - Step 9 — Add a macOS
.dmg - Where to go next
Concepts
Shipping guides
Reference
Status
Alpha — the pipeline works end-to-end, but expect breaking changes to the config schema, CLI, and output layout as it matures.
Targets today are Windows x64 (msi, msix), Linux x64 (linux), and
macOS arm64/x64 (macos, macapp, dmg). Auto-update and code-signing
certificates are out of scope for now; optional code signing of the Windows
artifacts is available. Distributed apps are not obfuscated, and unsigned Windows
installers will trigger a SmartScreen warning.