Shipping compiled Python — PyInstaller, Nuitka, and wheels
Single-file executables, Nuitka's compile-to-C path, and building manylinux wheels for extension modules.
Why this matters
Sometimes you need to ship Python to users who don't have Python — desktop tools, internal CLIs for non-developers, kiosks, embedded devices, plug-ins. PyInstaller / Briefcase / Nuitka pack a Python runtime + your code into a single executable (or .app / .exe / .dmg).
Learning objectives
- Build a standalone CLI exe with PyInstaller.
- Build a desktop app installer with Briefcase.
- Build a (possibly faster) standalone exe with Nuitka.
- Sign / notarise for macOS and Windows.
- Handle data files, plugins, and hidden imports.
1. When to compile / bundle
| Need | Tool |
|---|---|
| Single-file CLI for non-Python users | PyInstaller |
| Cross-platform desktop app installer | Briefcase |
| Maximum runtime speed (C compilation) | Nuitka |
| Lambda-ready zip | zipapp / shiv / pex |
| AWS Glue / Spark UDFs | pex |
| Python-in-the-browser | Pyodide / PyScript |
| Mobile (iOS / Android) | BeeWare/Briefcase + Python 3.13's mobile support |
2. PyInstaller — quick standalone exe
uv add --dev pyinstalleruv run pyinstaller --onefile --name mytool src/my_package/cli.py
# Output: dist/mytool.exe (Windows) or dist/mytool (Linux/macOS)That's it. The exe contains:
- The Python interpreter.
- All your modules + dependencies.
- A bootloader that extracts to a temp folder and runs.
Common flags
--onefile: single executable (slower startup; extracts on launch).--onedir: folder of files (faster startup; ship a zip).--name: output name.--icon=path/to/icon.ico.--windowed: hide console (for GUI apps).--add-data "data/templates;templates": bundle extra files.--hidden-import package.name: include modules PyInstaller can't auto-detect.--collect-all package: include every file from a package.
Spec files
For complex apps, generate a .spec once and edit it:
uv run pyi-makespec --onefile src/my_package/cli.py
# Edit mytool.spec, then:
uv run pyinstaller mytool.specCatch hidden imports
PyInstaller walks import statements but can't see dynamic imports. Common offenders: tiktoken, transformers, pydantic plugins, anything using importlib.
Use --collect-all or add to spec:
hiddenimports = ['tiktoken_ext.openai_public', 'tiktoken_ext']Test thoroughly. Distribute → user → "ModuleNotFoundError" is the classic PyInstaller bug.
Size
PyInstaller bundles the full Python + every dep, including ones you don't need. Typical sizes:
- Hello-world CLI: ~15 MB.
- With
numpy + pandas: ~150 MB. - With
torch: ~1.5 GB.
To shrink: exclude unused modules in the spec, use --exclude-module.
3. Briefcase — installers, not just exes
BeeWare's Briefcase builds native installers (MSI on Windows, DMG/PKG on macOS, AppImage/Flatpak on Linux, APK/IPA on mobile).
uv tool install briefcase
briefcase new # interactive scaffold
cd my-project
briefcase create # generate platform-specific project
briefcase build # build native artifacts
briefcase package # produce installer
briefcase run # test locallyOutput: MyApp.msi, MyApp.dmg, etc. Suitable for end-user distribution (double-click installer).
Native UI requires a UI framework — pick one:
- Toga (BeeWare's): cross-platform native widgets.
- PySide6 / PyQt6: full Qt; mature.
- Tkinter: stdlib; ugly but works.
- Flet: Flutter-powered.
- Web frontend + pywebview: HTML/JS frontend in an OS webview.
4. Nuitka — compile Python to C
Nuitka translates Python to C and compiles. Result: standalone binary that's often 2-5× faster than CPython.
uv add --dev nuitka
# Standalone binary (folder)
uv run python -m nuitka --standalone --onefile src/my_package/cli.pyPros:
- Faster runtime than PyInstaller (real compilation, not bytecode in a zip).
- Hides source code (binary, not zipped .pyc).
- Smaller than PyInstaller for some workloads.
Cons:
- Slower build (minutes for big apps).
- Build dependencies (C compiler).
- Trickier hidden-imports / dynamic-import handling.
- Some packages need explicit
--include-package.
Use Nuitka when you've already tried PyInstaller and need more performance / smaller binaries.
5. zipapp, shiv, pex — zipped Python apps
Build a single .pyz that any Python interpreter can run:
# stdlib zipapp
uv run python -m zipapp src/my_package -o tool.pyz -p "/usr/bin/env python3"
# shiv — bundles dependencies too
uv add --dev shiv
uv run shiv -c my-cli -o tool.pyz my-package
# pex — Twitter-grade reproducible builds
uv add --dev pex
uv run pex my-package -c my-cli -o tool.pex.pyz / .pex files include your code + (optionally) deps; require Python on the target machine.
Use case: shipping CLIs to a fleet of dev / build / data-eng machines that have Python. Smaller than PyInstaller; one file.
6. AWS Lambda packaging
Lambda has size limits and a specific Linux environment.
Container image (recommended in 2026)
FROM public.ecr.aws/lambda/python:3.12
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv export --frozen --no-dev -o requirements.txt && \
pip install -r requirements.txt -t ${LAMBDA_TASK_ROOT}
COPY src/my_handler/ ${LAMBDA_TASK_ROOT}/my_handler/
CMD ["my_handler.handler"]Push to ECR; reference from Lambda. Up to 10 GB. Modern default.
Zip package (legacy)
uv pip install -r requirements.txt --target ./package
Copy-Item src/my_handler ./package -Recurse
Compress-Archive ./package/* lambda.zip50 MB unzipped limit (250 MB with layers). Tighter constraints than containers.
7. Signing and notarisation (you can't skip this)
Windows
Unsigned exes show "Unknown publisher" warnings (or SmartScreen blocks). Sign with a code-signing certificate:
signtool sign /fd SHA256 /a /tr http://timestamp.digicert.com /td SHA256 mytool.exeEV certificates ($400/year) start with trust; regular certs need reputation built up over time.
macOS
Unsigned apps: "App is damaged" Gatekeeper message.
- Get an Apple Developer ID ($99/year).
- Sign:
codesign --force --options runtime --sign "Developer ID Application: ..." MyApp.app- Notarise:
xcrun notarytool submit MyApp.dmg --apple-id ... --team-id ... --password ... --wait
xcrun stapler staple MyApp.dmgBriefcase automates this.
Linux
No mandatory signing. AppImages can be verified by checksums; Flatpak / Snap have their own mechanisms.
8. Reducing bundle size
For PyInstaller / Nuitka:
# In .spec
excludes = ['tkinter', 'matplotlib.tests', 'numpy.tests', 'scipy.tests', 'IPython']Strip:
- Test directories of your dependencies.
- Tkinter (huge; only needed for GUI).
- Matplotlib data (
mpl-data/sample_data,mpl-data/fonts/ttfpartially). *.pycrebuilding (COLLECT(strip=True)).- Locale files for languages you don't ship.
Use UPX:
pyinstaller --upx-dir=path/to/upx --onefile cli.pyUPX compresses the binary 30-70% (slower startup; some antivirus flag it).
9. Cross-compilation
PyInstaller / Nuitka don't cross-compile. To build for Windows + macOS + Linux:
- Build on each platform (GitHub Actions matrix).
- Or use
Dockerfor Linux builds + native runners for Win/Mac.
GitHub Actions example:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install uv && uv sync --frozen
- run: uv run pyinstaller --onefile src/my_package/cli.py
- uses: actions/upload-artifact@v4
with:
name: mytool-${{ runner.os }}
path: dist/*For mobile: Briefcase + Apple silicon Mac for iOS; Briefcase + Linux/Mac for Android.
10. Worked example: a polished CLI installer
# 1. Build CLI
uv add typer rich
# write src/mycli/main.py with `typer` CLI
# 2. PyInstaller spec (one-time)
uv run pyi-makespec --onefile --name mycli \
--add-data "src/mycli/data:data" \
--icon assets/icon.ico \
src/mycli/main.py
# 3. Build
uv run pyinstaller mycli.spec
# 4. Test on a clean machine (or container)
docker run --rm -v ./dist:/dist debian:12-slim /dist/mycli --help
# 5. Sign (Windows / macOS); upload to GitHub releaseNow curl -L https://github.com/me/mycli/releases/.../mycli.exe -o mycli.exe && mycli --help is one step for users — no Python install needed.
11. Pyodide / PyScript — Python in the browser
For demos, dashboards, education: run Python in WebAssembly:
<py-script>
import numpy as np
print(np.arange(10))
</py-script>Pyodide ships CPython + NumPy + pandas + scikit-learn as WebAssembly. PyScript wraps it for HTML pages.
Not for everything (~10 MB initial download, no real threads, limited C extensions). Great for interactive demos.
12. Comparison summary
| Tool | Best for | Speed | Size | Complexity |
|---|---|---|---|---|
pip install | Other Python devs | n/a | tiny | minimal |
zipapp / shiv / pex | CLIs to machines with Python | normal | small | low |
| PyInstaller | Standalone exe for end users | normal | medium-large | medium |
| Briefcase | Native installers for end users | normal | large | medium-high |
| Nuitka | Maximum performance binaries | faster | medium | medium-high |
| Docker | Servers | normal | medium-large | low |
| Pyodide | Browser demos | slower | n/a | low |
Hands-on lab (2 hours)
- Build a 2-command CLI with
typer; run viauv run. - PyInstaller it (
--onefile); verify on a clean machine. - Add a data file (a JSON config); bundle with
--add-data. - Reduce the size by excluding unused modules; measure.
- (Optional) Nuitka the same; compare size + runtime.
- (Optional) Briefcase scaffold; build a tiny Toga app; package an installer.
- Bonus: GitHub Actions matrix that builds for 3 OSes and attaches to a release.
Common pitfalls
- PyInstaller working in dev but missing modules in
dist/— add tohiddenimports. - Massive binaries due to bundling unused libs.
- Forgetting code signing — users get scary warnings.
- Hardcoded paths that work in the source tree but break in the bundled exe (use
sys._MEIPASSfor PyInstaller bundle root). - Trying to cross-compile; you can't — use CI matrix builds.
- Shipping a debug build (Nuitka without
--lto=yes).
sys._MEIPASS for bundled data
import sys, os
def resource_path(rel):
base = getattr(sys, "_MEIPASS", os.path.dirname(__file__))
return os.path.join(base, rel)Use this for any file you --add-data'd.
Self-check
- When use PyInstaller vs Docker vs Briefcase?
- What does Nuitka do differently from PyInstaller?
- What's a hidden import?
- Why are macOS apps "damaged" without notarisation?
- When use Pyodide?
References
- PyInstaller docs: https://pyinstaller.org/.
- Nuitka docs: https://nuitka.net/.
- BeeWare / Briefcase: https://beeware.org/.
shiv: https://shiv.readthedocs.io/.pex: https://github.com/pex-tool/pex.- Pyodide: https://pyodide.org/.
- "Sign Code with EV Certificate" Microsoft guide.
- Apple notarisation docs.
Sign in to save your progress and earn badges.