Turn a Python app into a native installer (Windows MSI/MSIX, Linux, macOS)
Project description
pyappdist
Turn a Python app into a native installer — and it just works.
pyappdist packages your Python application into a native installer:
- Windows: MSI / MSIX
- macOS: DMG(notarization supported)
- Linux: Self-extracting installers
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. C extensions, abi3 wheels, Qt plugins,
and tkinter-based GUIs work unmodified because the install layout is real.
📖 Documentation: https://pyappdist.readthedocs.io/
What it produces
One pyproject.toml can describe several output packages — each is a
[[tool.pyappdist.targets]] entry with its own platform and format:
format |
Platform | Output |
|---|---|---|
msi |
windows-x86_64 |
.msi installer (per-user or machine-wide) + portable .zip |
msix |
windows-x86_64 |
.msix package for the Microsoft Store / sideloading |
linux |
linux-x86_64 |
.tar.gz + self-extracting .run installer (per-user, no root) |
macos |
macos-aarch64 |
.tar.gz + self-extracting .run installer (per-user, no root) |
dmg |
macos-aarch64 |
.dmg disk image (code-signing / notarization supported) |
macapp |
macos-aarch64 |
.app bundle (code-signing / notarization supported) |
Quick start
Add a [tool.pyappdist] section to your app's pyproject.toml:
[tool.pyappdist]
name = "My App"
python = "3.12"
[[tool.pyappdist.launchers]]
name = "myapp" # produces myapp.exe (or a shell wrapper on Linux/macOS)
entry = "myapp:main" # module:callable
# gui = true # use pythonw.exe (no console window) on Windows
# icon = "assets/app.ico" # launcher icon
# args = "--serve" # fixed leading arguments
[[tool.pyappdist.targets]]
name = "windows"
platform = "windows-x86_64"
format = "msi"
manufacturer = "Example Inc."
# scope = "user" # "user" (default, no admin) or "machine" (Program Files)
Then add pyappdist and build:
uv add --dev pyappdist
uv run pyappdist build # builds the sole target: wheels -> runtime -> image -> launcher -> wix -> MSI
The result lands under appdist/<target>/dist/.
Multiple targets
Declare several targets to ship more than one package from the same config:
[[tool.pyappdist.targets]]
name = "windows"
platform = "windows-x86_64"
format = "msi"
manufacturer = "Example Inc."
[[tool.pyappdist.targets]]
name = "linux"
platform = "linux-x86_64"
format = "linux"
[[tool.pyappdist.targets]]
name = "macos-arm"
platform = "macos-aarch64"
format = "macos"
When several targets are defined, build requires you to name the one(s) to build (so it
doesn't build them all at once); the individual pipeline stages default to all targets:
uv run pyappdist build linux # build just the "linux" target
uv run pyappdist build windows-x86_64 # build the Windows MSI
Samples
Runnable example apps live under samples/, each with its own
[tool.pyappdist] config. They double as smoke tests for tricky cases (C
extensions, GUI stacks, data files, per-target extras):
| Sample | Kind | What it shows |
|---|---|---|
helloworld |
CLI | Smallest possible config — no dependencies. A good starting template; builds for every format (msi/msix/linux/macos/dmg). |
pandascli |
CLI | pandas + numpy (C extensions) collected as binary wheels and installed into the runtime. Console launcher (gui = false). |
datafiles |
CLI | Ships a bundled data file (data/ebi.jpeg) via [tool.uv.build-backend].data and reads it through sysconfig; opens it with Pillow. |
matplotlibdemo |
GUI | matplotlib plot with the TkAgg backend — uses the runtime's bundled tkinter/tcl-tk, no extra GUI deps. gui = true → pythonw.exe. |
pygamedemo |
GUI | A bouncing ball with pygame-ce (C extensions) collected as Windows wheels. gui = true. |
pyside6demo |
GUI | A Qt window with PySide6 — a large abi3 wheel (cp39-abi3) installed into the cp312 runtime, Qt plugins and all. gui = true. |
niceguidemo |
GUI (web) | "Weather Panel" built with NiceGUI + pywebview + requests; uses per-target extras (gtk/qt/gui) to pick the webview backend per platform. |
Status
Alpha — the pipeline works end-to-end, but expect breaking changes to the config schema, CLI, and output layout as it matures.
Project details
Release history Release notifications | RSS feed
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 pyappdist-0.5.0.tar.gz.
File metadata
- Download URL: pyappdist-0.5.0.tar.gz
- Upload date:
- Size: 52.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
93fbd6510a87ae0b9a4a840e6a12b4f96da46556b9aed43045671c60e99d362e
|
|
| MD5 |
fc94672d02dd69c4b3c6eb3e1ca18479
|
|
| BLAKE2b-256 |
0b53b7290b91c8d5da39f412f2a3d5bedf0b326a787f7254c853d9dd51123046
|
Provenance
The following attestation bundles were made for pyappdist-0.5.0.tar.gz:
Publisher:
publish.yml on atsuoishimoto/pyappdist
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyappdist-0.5.0.tar.gz -
Subject digest:
93fbd6510a87ae0b9a4a840e6a12b4f96da46556b9aed43045671c60e99d362e - Sigstore transparency entry: 1756311031
- Sigstore integration time:
-
Permalink:
atsuoishimoto/pyappdist@d4ff78527db113f2a4311d4f8d9819ee9d2b0579 -
Branch / Tag:
refs/tags/0.5.0 - Owner: https://github.com/atsuoishimoto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d4ff78527db113f2a4311d4f8d9819ee9d2b0579 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file pyappdist-0.5.0-py3-none-any.whl.
File metadata
- Download URL: pyappdist-0.5.0-py3-none-any.whl
- Upload date:
- Size: 70.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1857f09483592cc067fef33cdd8e69a5b58e628dc9a8b0e26588e1d678611549
|
|
| MD5 |
41581c9e77f40249b6234349b9e0ca8a
|
|
| BLAKE2b-256 |
b5df477d97d1daf78f6b8021c07f7f6bee0e7044efa6cd96bc6f18dbc81fd858
|
Provenance
The following attestation bundles were made for pyappdist-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on atsuoishimoto/pyappdist
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyappdist-0.5.0-py3-none-any.whl -
Subject digest:
1857f09483592cc067fef33cdd8e69a5b58e628dc9a8b0e26588e1d678611549 - Sigstore transparency entry: 1756311048
- Sigstore integration time:
-
Permalink:
atsuoishimoto/pyappdist@d4ff78527db113f2a4311d4f8d9819ee9d2b0579 -
Branch / Tag:
refs/tags/0.5.0 - Owner: https://github.com/atsuoishimoto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d4ff78527db113f2a4311d4f8d9819ee9d2b0579 -
Trigger Event:
workflow_dispatch
-
Statement type: