Jason Miller / Nuitka vs PyInstaller vs cx_Freeze: Python Packaging Compared (2026)

Created Tue, 18 Aug 2026 00:00:00 -0700 Modified Tue, 18 Aug 2026 22:01:35 +0000

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: .pyc files 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: --onefile mode 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.py or cxfreeze.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