How do I ship a Python application to end users?
Python usually ships an environment rather than a native executable. It ships as code plus an interpreter plus dependencies, which is why distribution gets harder the moment the users are not Python developers themselves. The real question is not whether Python can be shipped well. It can. The question is which delivery shape fits the audience: an install command, a bundled app, a container image, or something that stops being Python at all.
This page maps the 2026 options and recommends a default for each case. It assumes the project already has a working Python package and is deciding how users will get it.
Why Python has no native binary output
Python is an interpreted language. Running a program means running the CPython interpreter over source code (or bytecode) at startup, with a standard library on disk and any third-party dependencies already installed into a reachable location. Nothing in that model produces a single file.
Other language ecosystems answer this question at the compiler level. Rust’s cargo build --release emits a native executable with everything statically linked; Go does the same by default. Python’s model requires the interpreter to be present wherever the code runs, and the interpreter is not small. The Go developer’s Python guide covers this architectural gap in more detail.
The practical result is that Python distribution always involves shipping one of three things: a pointer to the code (an install command), a bundle that contains an interpreter, or a deliverable that replaces Python with something else (a compiled C binary, a web service, or an ONNX graph). Every option on this page is a variation on one of those themes.
Start with the distribution channel, not the tool
Before reaching for PyInstaller, decide what the user is actually going to do:
- Technical user, willing to run an install command. Developers, data scientists, sysadmins. Any mainstream CLI works.
- Non-technical desktop user, expects an installer or a
.exe. Someone who double-clicks things. The bar is much higher: the bundle has to include an interpreter and be code-signed to avoid antivirus warnings. - Mobile user. iOS or Android. A different deployment target; most Python tooling doesn’t cover it.
- Server or container operator. Another engineer on the team running
docker pull, or a Kubernetes cluster. No desktop story needed at all. - ML model consumer. Someone who wants the model’s predictions, not the training stack. Often best served without shipping Python at all.
Ship via a package index
The lowest-overhead distribution channel is PyPI plus an install command. Publish a wheel, expose a [project.scripts] entry point, and tell users to run:
uvx yourtool
# or
uv tool install yourtool
# or
pipx install yourtoolAstral’s own uv, Ruff, Marimo, Posting, and Harlequin all use this pattern. The author’s effort is a single uv publish command; the consumer’s effort is a single uvx call. There’s no bundle size to manage and no code-signing certificate to buy.
The catch is that the user needs uv (or pipx) already installed. uv itself is distributed as a standalone binary and can bootstrap on any supported platform in one command, so the prerequisite is less punishing than it used to be. For audiences that are comfortable running a shell command this is almost always the right default. See How to install Python CLI tools without Python for the consumer-side story and How to write install instructions for a Python library for what to put in a README.
Astral’s documentation is explicit that uv tool install targets development utilities, not end-user applications, but in practice the pattern has absorbed most of the Python CLI distribution space.
Ship a lightweight launcher
When the user can’t be expected to install uv first but the team wants to avoid a full bundled executable, PyApp splits the difference. PyApp is a Rust launcher by Ofek Lev (author of Hatch). The build produces a small native launcher, a few megabytes per platform. By default, on first run it fetches a python-build-standalone interpreter and installs the target project with uv or pip into a user-local cache directory; later runs reuse that installation with no download. PyApp can also embed the Python distribution and the project into the launcher itself, which eliminates the first-run fetch at the cost of a much larger binary.
Tradeoffs:
- Tiny artifact in the default configuration, with dependencies installed on first launch.
- Needs network on first run unless the distribution and project are embedded.
- The first launch takes seconds to download the interpreter and resolve dependencies.
- The
self updatesubcommand refreshes the installed project; it does not replace the launcher binary.
PyApp integrates with Hatch via hatch build --target app, which makes it a low-effort option for projects already using Hatch. It’s the best middle ground for distributing a CLI to users who won’t install uv themselves but for whom a one-time network download is acceptable.
Ship a bundled executable
For GUI apps and situations where the deliverable has to be a .exe, .app, or native Linux installer, the mainstream tools are PyInstaller, Nuitka, and cx_Freeze. Each ships the interpreter, the standard library, and all imported dependencies in a package that does not require Python on the target machine.
- PyInstaller has the largest hook ecosystem. Community-maintained configurations for most popular third-party packages ship out of the box, so projects with tricky dependencies (NumPy, PyQt, matplotlib) rarely need custom tuning.
- Nuitka is a Python-to-C compiler rather than a bundler. It translates bytecode to C and links the result against libpython. The output is faster on compute-bound code (official benchmarks claim 2x to 3x over CPython on pystone; real applications typically see 10% to 40%). Nuitka Commercial, a paid tier, adds constant obfuscation and traceback encryption for projects that need to ship closed-source binaries.
- cx_Freeze is the quietest of the three but has the strongest installer story. It builds MSI files on Windows, DMG files on macOS, and AppImage and Debian packages on Linux through a unified
bdist_*interface. Recent releases added Python 3.14 and free-threaded 3.13t support on Windows.
All three share the same set of problems:
- Bundle size. A bundled Python application starts at around 15 MB for a hello-world and grows with every dependency. A PyTorch install is typically hundreds of megabytes on its own, and full desktop bundles that include it can push past 1 GB.
- Antivirus false positives. The stock bootloader binaries have signatures that Windows Defender and other AV products flag, because malware authors have used the same bundlers. Mitigating this means building the bootloader from source and signing the result.
- No automatic code signing. None of the three will produce a signed, notarized artifact by themselves. Distributing a bundle that users will trust means an Apple Developer ID plus
notarytoolon macOS, or an EV code-signing certificate plussigntoolon Windows.
The bundled-executable path is the right answer for desktop GUI apps but carries real per-release operational overhead. Budget for it.
Ship a mobile app
Briefcase is the only mainstream Python tool that targets mobile. It’s part of the BeeWare project (the Toga GUI toolkit is its sibling). Briefcase builds for macOS, Windows, Linux, iOS, and Android from a single pyproject.toml, using a prebuilt wheel set that BeeWare maintains for the mobile platforms.
Caveats:
- Still pre-1.0.
- Mobile support depends on the BeeWare wheel set, so any dependency that needs a C extension not already built for iOS or Android requires upstream or project-side work.
- The dominant users are BeeWare’s own Toga apps and hobby projects. No marquee commercial mobile shipping case has emerged.
For a desktop-only app, PyInstaller, Nuitka, or cx_Freeze is usually a better fit than Briefcase. For mobile, Briefcase is the only serious Python option.
Ship an ML model without shipping the framework
For a project whose payload is a machine-learning model, the question “how do I ship this to users?” often has a better answer than “bundle all of PyTorch.” ONNX Runtime runs models exported from PyTorch (torch.onnx.export), TensorFlow and Keras, and scikit-learn (via skl2onnx), with XGBoost and LightGBM covered too.
The size win is large. The ONNX Runtime CPU wheel is around 15 MB. A minimal PyTorch install is around 1 GB. For a project whose only real use of PyTorch is model.forward(x), exporting to ONNX and shipping onnxruntime drops two orders of magnitude off the final bundle.
This works when:
- The model uses operators supported by ONNX Runtime (most common architectures are covered).
- Inference is the only thing the end user needs. Training-time dependencies stay on the training machine.
- The performance profile is acceptable. ONNX Runtime’s CPU inference is competitive with PyTorch’s for most workloads, and the GPU runtime uses CUDA directly.
For ML-heavy desktop applications, the combination “ONNX model plus onnxruntime plus a thin UI layer in PyInstaller or PyApp” produces a much smaller, more reliable artifact than bundling the full training framework.
Ship within an organization
When every target machine already has a compatible Python interpreter installed, a full bundled executable is overkill. Two tools target this case:
- PEX (used heavily in Pantsbuild) produces a
.pexfile that’s a zipapp plus dependencies. Given a compatible interpreter, it runs as one file. PEX is actively maintained and ships universal lockfiles. - Shiv is LinkedIn’s simpler zipapp builder. Like PEX, Shiv assumes a compatible interpreter is already present on the target machine. Quieter than PEX externally but battle-tested inside LinkedIn for smaller projects that don’t need PEX’s lockfile story.
Docker is the other organization-scale answer. Building an image with a Python base layer, installing dependencies with uv sync --frozen, and copying the application source produces a reproducible artifact that works anywhere a container runs. For server deployments that’s the default path regardless of language.
Both approaches assume the target environment is one the team controls. Neither is a substitute for the consumer-grade options covered in Ship via a package index and Ship a bundled executable.
Consider one exotic option
Cosmopolitan Python is real. Cosmopolitan libc produces Actually Portable Executables that run on Linux, macOS, Windows, and the BSDs from a single file, and the Cosmopolitan project publishes a prebuilt portable CPython binary.
The catch: C extension wheels from PyPI don’t load unless they’re also built against cosmocc. That means NumPy, pandas, PyTorch, PyArrow, and most of the scientific stack are off the table. Cosmopolitan Python is usable for pure-Python command-line tools and not much else as of 2026. Treat it as a curiosity.
Sign the binary, or users won’t run it
Every bundled-executable path (PyInstaller, Nuitka, cx_Freeze, Briefcase) eventually hits the same problem: unsigned binaries trigger antivirus warnings on Windows and Gatekeeper blocks on macOS. The mitigation is a per-release signing step that the bundling tools do not perform for you.
- macOS. Apple Developer ID (paid, $99 per year),
codesign, andnotarytoolfor notarization. Without notarization, Gatekeeper blocks the app on first launch on recent macOS versions. - Windows. Code-signing certificate (EV for SmartScreen reputation, around $200 to $500 per year),
signtool.exeto sign the binary. An EV certificate builds SmartScreen reputation faster; a standard certificate works but users see a warning until enough installs accumulate. - Bootloader rebuild. PyInstaller specifically recommends rebuilding the bootloader from source so the resulting binary doesn’t share a signature with known-bad samples. Same applies to cx_Freeze.
The full story is beyond this page, but anyone shipping a desktop bundle should budget time and money for the signing pipeline from day one.
Choose a strategy
A compressed decision tree:
- Can the user install uv first? Publish to PyPI; tell them
uvx yourtool. Done. - CLI for moderately technical users, no uv prereq? PyApp. Ships a few-megabyte launcher that bootstraps an interpreter on first run.
- Desktop GUI for non-technical users? PyInstaller (hook ecosystem), Nuitka (obfuscation and marginal speed), or cx_Freeze (native installer formats). Budget for code signing.
- Mobile? Briefcase. Pre-1.0; expect rough edges.
- ML model? Export to ONNX, ship
onnxruntime. Only fall back to bundling PyTorch or TensorFlow if ONNX doesn’t cover your operators. - Internal distribution on a Python-on-every-machine fleet? PEX or Shiv.
- Server deployment? Docker. Different problem, different shape; the bundled-executable question doesn’t apply.
- One binary for every OS? Cosmopolitan Python is real for pure-Python tools only.
The most useful upgrade most projects can make is treating “tell users to uvx it” as a legitimate first-class distribution channel rather than a developer-only shortcut. uv is distributed as a standalone binary on every platform pydevtools.com covers, so the prerequisite is a single install command for the user. For many projects, that is the entire distribution story.
Learn more
- The uv tools documentation covers
uv tool installanduvxin depth. - PyApp is the lightweight launcher option.
- PyInstaller, Nuitka, and cx_Freeze cover the bundled-executable space.
- Briefcase is the mobile option.
- ONNX Runtime lets an ML application skip the framework entirely.
- PEX and Shiv are zipapp-based options for intra-organization distribution.
- Why doesn’t Python just fix packaging? covers the organizational reason there’s no single blessed distribution story.