Nuitka vs PyInstaller vs cx_Freeze: Which Python Packager Wins in 2026?
Distributing a Python application without requiring users to install Python—or without exposing your source code—requires a packaging tool. Three tools dominate this space: Nuitka, PyInstaller, and cx_Freeze. Each takes a fundamentally different approach, and choosing the wrong one costs you startup time, binary size, or runtime stability.
This guide cuts through the marketing and gives you the technical detail needed to make an informed decision.
How Each Tool Actually Works
Understanding the underlying mechanism explains most of the trade-offs.
PyInstaller: Bundling, Not Compiling
PyInstaller does not compile Python. It bundles the CPython interpreter, your .py source files (compiled to .pyc bytecode), and all dependencies into a single archive. At runtime, the bundle extracts itself to a temp directory and launches CPython as normal.
Implications:
- Startup time is slow: on cold start, the bundle must unpack before execution begins. A simple script can take 1–3 seconds to “start.”
- Source code is recoverable:
.pycfiles can be decompiled. PyInstaller provides no IP protection. - Broad compatibility: since CPython runs unchanged, nearly all libraries work out of the box, including C extensions.
- Single-file mode pain:
--onefilemode re-extracts on every run. For long-running servers this is fine; for short-lived CLIs it is punishing.
cx_Freeze: A More Configurable Bundler
cx_Freeze is similar to PyInstaller in mechanism—it bundles the CPython interpreter and your code—but takes a more explicit, configuration-file-driven approach. It does not produce a single file by default; instead it creates a distribution directory.
Key differences from PyInstaller:
- Better handling of some edge cases with packages that use
__file__paths - Requires more manual configuration (
setup.pyorcxfreeze.json) - Smaller community and slower update cadence than PyInstaller
- Same fundamental limitation: no real compilation, no IP protection
Nuitka: Genuine Compilation
Nuitka translates Python source into C, then compiles that C with a native compiler (GCC or MSVC). The resulting binary is a true native executable with an embedded libpython for any operations that still require the runtime.
Implications:
- Startup time is fast: no extraction step; the binary launches like any compiled C program.
- Source code protection: Python logic is compiled to machine code. Reversing it requires disassembling native code, not running
uncompile. - Performance gains: pure Python loops and function calls typically run 20–50% faster. I/O-bound applications see smaller gains since the bottleneck is elsewhere.
- Compatibility work required: some highly dynamic libraries (Django, SQLAlchemy’s mapper, heavy metaclass code) require explicit Nuitka plugins or flags.
- Build time is longer: compiling C takes time. A large project can take several minutes to build.
Direct Comparison
| Criterion | Nuitka | PyInstaller | cx_Freeze |
|---|---|---|---|
| Mechanism | Python → C → native binary | CPython + .pyc bundle | CPython + .pyc bundle |
| Cold startup | Fast (native) | Slow (extract + launch) | Slow (launch from dir) |
| Runtime performance | +20–50% for CPU-bound | Identical to Python | Identical to Python |
| Binary size (simple app) | ~10–30 MB | ~25–60 MB | ~30–70 MB |
| IP protection | Strong (native code) | Weak (decompilable .pyc) | Weak (decompilable .pyc) |
| Single-file output | Yes (--onefile) |
Yes (--onefile) |
No (directory only) |
| Build time | Minutes | Seconds | Seconds |
| Broad library compat | Good (with plugins) | Excellent | Good |
| Active maintenance | Yes (commercial + OSS) | Yes | Slower cadence |
| Windows support | Yes | Yes | Yes |
| macOS support | Yes | Yes | Yes |
| Linux support | Yes | Yes | Yes |
| Cross-compilation | No (compile on target) | No | No |
Performance Benchmarks
These numbers are representative for a CPU-bound workload. Results vary significantly by application type.
Fibonacci(35) — pure Python loop
CPython 3.12: 4.2 s
PyInstaller bundle: 4.1 s (-2%, noise)
cx_Freeze bundle: 4.1 s (-2%, noise)
Nuitka compiled: 1.8 s (-57%)
FastAPI app — requests/sec under load (I/O bound)
CPython 3.12: 12,400 req/s
PyInstaller bundle: 12,350 req/s (~same)
Nuitka compiled: 13,800 req/s (+11%)
For I/O-bound applications like web APIs, Nuitka’s gains are real but modest. The value proposition shifts toward startup time and binary cleanliness rather than throughput.
For CPU-bound workloads—data processing, image manipulation, algorithmic computation—Nuitka’s gains are substantial.
Startup Time Comparison
Startup time matters most for CLIs and serverless functions. For persistent servers it is less relevant.
# CLI tool: "hello world" + argparse + one library import
PyInstaller (--onefile): 1.8 s (extraction overhead)
PyInstaller (directory): 0.4 s
cx_Freeze (directory): 0.5 s
Nuitka (--onefile): 0.08 s
Nuitka (directory): 0.06 s
If you are building a CLI tool that runs frequently, PyInstaller’s --onefile mode is a poor experience for users. Nuitka is the clear winner here.
Use Case Recommendations
FastAPI / Uvicorn applications
Use Nuitka. The single-process Uvicorn model compiles cleanly. Startup time benefit is meaningful for containerized deployments that restart frequently. See the companion guide Nuitka Packaging for Web Frameworks for the specific compilation flags.
python -m nuitka \
--standalone \
--onefile \
--include-package=fastapi \
--include-package=uvicorn \
--include-package=pydantic \
main.py
Flask / WSGI applications
Use Nuitka. Flask compiles reliably. The main concern is including template and static directories:
python -m nuitka \
--standalone \
--onefile \
--include-data-dir=templates=templates \
--include-data-dir=static=static \
main.py
Django applications
Use PyInstaller reluctantly, or do not bundle at all. Django’s dynamic discovery model is hostile to both Nuitka and PyInstaller. If you must ship a standalone Django binary, PyInstaller handles the DJANGO_SETTINGS_MODULE magic more gracefully than Nuitka’s static analysis. Neither works reliably without significant manual intervention.
For Django deployments, standard container images with a pinned Python runtime are the more maintainable path.
CLI tools and scripts
Use Nuitka for tools with fast startup requirements (anything invoked repeatedly in a shell session or CI pipeline). Use PyInstaller (directory mode, not onefile) for simpler tools where build speed matters more than startup speed.
Proprietary software distribution
Use Nuitka. It is the only option that provides meaningful IP protection. PyInstaller and cx_Freeze ship decompilable bytecode.
Scientific / data applications (NumPy, pandas, scipy)
Use PyInstaller for the path of least resistance. NumPy, pandas, and scipy ship compiled C extensions that PyInstaller bundles without modification. Nuitka can handle them too but requires --include-package flags and sometimes Nuitka plugins. The performance gain on NumPy-heavy code is minimal since NumPy is already compiled C.
Nuitka Compilation Flags Cheat Sheet
# Minimum viable standalone binary
python -m nuitka --standalone --onefile app.py
# Web application with data files
python -m nuitka \
--standalone \
--onefile \
--include-data-dir=templates=templates \
--include-data-dir=static=static \
--include-package=your_package \
main.py
# Enable Nuitka plugin for common libraries
python -m nuitka \
--standalone \
--plugin-enable=pylint-warnings \
--include-package=pkg_resources \
main.py
# Build with optimization (slower build, faster binary)
python -m nuitka \
--standalone \
--onefile \
--lto=yes \
main.py
PyInstaller Spec File for a Flask Application
For completeness, the equivalent PyInstaller approach:
# flask_app.spec
block_cipher = None
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[
('templates', 'templates'),
('static', 'static'),
],
hiddenimports=['waitress'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='flask_app',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
)
The Verdict
Nuitka wins for production deployments where startup time, performance, or IP protection matter. The longer build time is a CI/CD cost, not a runtime cost.
PyInstaller wins for quick packaging where you need broad library compatibility and fast build iteration. It remains the better choice for Django applications and scientific tooling.
cx_Freeze has no compelling advantage over PyInstaller in 2026. Its primary use case is projects that already have it configured and have not found a reason to switch.
The decision tree is simple: if you are building a web application or a CLI tool with startup performance requirements, use Nuitka. If you are bundling a data science application with heavy NumPy/pandas dependencies and do not need IP protection, use PyInstaller.
Related: Nuitka Packaging for Web Frameworks: FastAPI, Flask, and Django