From PyInstaller to Nuitka: Convert Python to EXE Without False Positives

As a Python developer, I ran into an annoying problem: my PyInstaller-compiled executables kept getting flagged as viruses by Windows Defender. After hours of research and frustrating whitelist attempts, I discovered Nuitka – and it changed everything.

In this article, I’ll show you how to convert Python projects into clean, performant EXE files using Nuitka that won’t get blocked by antivirus software.

The Problem with PyInstaller

PyInstaller has been the go-to tool for creating Python executables for years. But there’s a massive problem: false positives from antivirus software.

Why does this happen?

  • PyInstaller uses a bootstrapping mechanism that looks similar to malware behavior
  • The EXE unpacks itself at runtime into temporary folders
  • This behavior triggers heuristic virus scanners
  • Microsoft Defender is particularly aggressive about flagging these files

The result? Your legitimate business software gets reported as a trojan. Not exactly what you want when trying to impress clients.

The Solution: Nuitka

Nuitka is a true Python compiler (not a packer like PyInstaller). The key difference:

Nuitka compiles Python → C/C++ → Machine Code

This means:

  • No self-extraction mechanism
  • No suspicious runtime behavior
  • Real native binaries
  • Bonus: Better performance (10-20% faster than regular Python)
  • Superior source code protection

The false positive rate is drastically lower than PyInstaller.

Step-by-Step Tutorial

Prerequisites and Setup

Before we start compiling, let’s talk about Python environment management. Your choice here affects how smoothly the Nuitka compilation will go.

If you’re using vanilla Python with pip, the installation is straightforward. However, if you’re working with modern tools like Poetry or uv, there are some considerations to keep in mind. Poetry creates virtual environments and manages dependencies through pyproject.toml files, which is excellent for development but requires a different approach when compiling. The tool uv, on the other hand, is Rust-based and incredibly fast for package installation, making it ideal for CI/CD pipelines where compilation speed matters.

Installation

The basic installation depends on your package manager. With pip, you simply install Nuitka globally or in your virtual environment. With Poetry, you add it as a dev dependency to keep your project dependencies clean. And with uv, you get the speed benefits during both installation and subsequent package operations.

Using pip (traditional approach):

# Create a virtual environment first (recommended)
python -m venv venv
source venv/bin/activate  # On Windows: venvScriptsactivate

# Install Nuitka and optimization tools
pip install nuitka
pip install ordered-set  # Speeds up compilation significantly
pip install zstandard    # Reduces final EXE size by 30-50%

Using Poetry (modern Python projects):

# Add Nuitka as a development dependency
poetry add --group dev nuitka ordered-set zstandard

# Activate Poetry's virtual environment
poetry shell

# Now you can use Nuitka within the Poetry environment
python -m nuitka --version

The Poetry approach is particularly valuable because it keeps Nuitka separate from your production dependencies. This means your pyproject.toml file clearly distinguishes between what your application needs to run versus what you need to build it. This separation becomes crucial when you’re managing multiple projects or working in a team where not everyone needs the compilation toolchain.

Using uv (fastest option, great for CI/CD):

# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create a virtual environment with uv (much faster than venv)
uv venv

# Activate the environment
source .venv/bin/activate  # On Windows: .venvScriptsactivate

# Install dependencies blazingly fast
uv pip install nuitka ordered-set zstandard

# Install your project dependencies
uv pip install -r requirements.txt
# Or if you have pyproject.toml: uv pip install -e .

The speed difference with uv is remarkable. Where pip might take 30 seconds to install Nuitka and its dependencies, uv can do it in under 5 seconds. This matters a lot in CI/CD pipelines where you’re compiling regularly.

Creating Your First EXE

Let’s start with the simplest case and build up to more complex scenarios. The key is understanding what each flag does and why you need it.

Basic standalone executable:

python -m nuitka --standalone your_script.py

This creates a folder with your executable and all necessary dependencies. The standalone mode means Nuitka bundles Python itself along with your script, so the target machine doesn’t need Python installed. However, this creates a folder structure rather than a single file, which can be inconvenient for distribution.

Single-file executable (most common for distribution):

python -m nuitka --standalone --onefile your_script.py

The onefile mode takes that folder of dependencies and packages everything into a single executable. When you run this EXE, it temporarily unpacks itself to a cache directory, runs your program, and cleans up afterward. This is perfect for sharing with clients or deploying to end users who just want to double-click and run.

GUI application without console window:

python -m nuitka --standalone --onefile --windows-disable-console your_app.py

If you’re building a GUI application with Tkinter, PyQt, or similar frameworks, you don’t want a console window popping up behind your GUI. The windows-disable-console flag prevents that. This only works on Windows and should not be used for CLI tools or scripts that need to print output.

Working with Dependencies and Poetry Projects

Here’s where things get interesting. If you have a Poetry project with a pyproject.toml file, you need to ensure Nuitka can find all your dependencies. Poetry installs packages in its own virtual environment, which Nuitka needs to access.

Compiling a Poetry project:

# First, make sure you're in the Poetry environment
poetry shell

# Compile with plugin support for common packages
python -m nuitka 
  --standalone 
  --onefile 
  --enable-plugin=numpy 
  --enable-plugin=tk-inter 
  --follow-imports 
  your_script.py

The follow-imports flag is crucial here. It tells Nuitka to recursively include all imported modules, even if they’re dynamically imported. Poetry projects often have complex dependency trees, and this flag ensures nothing gets missed.

If you’re using specific packages that need special handling, Nuitka provides plugins. For example, numpy requires the numpy plugin because it has compiled C extensions. Similarly, matplotlib needs the matplotlib plugin. You can check which plugins are available with:

python -m nuitka --plugin-list

Embedding Resources and Assets

Many applications need to include images, configuration files, or data files. This is where people often struggle because the file paths change when your code is compiled into an executable.

Including data directories:

python -m nuitka 
  --standalone 
  --onefile 
  --include-data-dir=./assets=assets 
  --include-data-dir=./config=config 
  --windows-icon-from-ico=icon.ico 
  your_script.py

The syntax for include-data-dir is important to understand. The format is source=destination, where source is the folder on your development machine and destination is where it will be placed in the compiled executable. So ./assets=assets means “take my assets folder and make it available as ‘assets’ in the compiled version.”

However, there’s a catch. When running as a onefile executable, your script doesn’t run from the same directory as the EXE file. Nuitka unpacks everything to a temporary directory. You need to adjust your code to handle this:

import os
import sys

def get_resource_path(relative_path):
    """Get the absolute path to a resource, works for dev and compiled."""
    # If running as a Nuitka onefile, __file__ points to the temp directory
    if getattr(sys, 'frozen', False):
        # Running as compiled executable
        base_path = os.path.dirname(sys.argv[0])
    else:
        # Running as script
        base_path = os.path.dirname(__file__)

    return os.path.join(base_path, relative_path)

# Usage
config_path = get_resource_path('config/settings.json')
image_path = get_resource_path('assets/logo.png')

This helper function ensures your resource paths work correctly both during development and when running as a compiled executable. The sys.frozen attribute is set by Nuitka when your code is compiled, allowing you to detect the runtime environment.

Advanced Optimization with UPX

After Nuitka creates your executable, you can compress it further using UPX. This is particularly valuable if you’re distributing your application over the internet or need to fit it on limited storage.

# Install UPX first
# Windows with Scoop: scoop install upx
# macOS with Homebrew: brew install upx
# Linux: apt install upx-ucl

# Compress your executable (can take several minutes)
upx --ultra-brute your_script.exe

# For faster compression with slightly less compression ratio:
upx --best your_script.exe

The ultra-brute option uses maximum compression, which can reduce file size by 50-70% but takes significantly longer. The best option gives you about 40-50% reduction in just a fraction of the time. For CI/CD pipelines, I recommend using –best to keep build times reasonable.

One important note: some antivirus software doesn’t like UPX-compressed executables because malware authors also use UPX. So if you’re using Nuitka specifically to avoid false positives, you might want to skip UPX compression or test thoroughly with your target antivirus software.

Using uv in CI/CD Pipelines

If you’re building executables in a CI/CD environment like GitHub Actions or GitLab CI, uv can dramatically speed up your build times. Here’s a complete example workflow:

# .github/workflows/build.yml
name: Build EXE with Nuitka

on: [push]

jobs:
  build:
    runs-on: windows-latest

    steps:
    - uses: actions/checkout@v3

    - name: Install uv
      run: curl -LsSf https://astral.sh/uv/install.sh | sh

    - name: Create venv and install dependencies
      run: |
        uv venv
        .venvScriptsactivate
        uv pip install nuitka ordered-set zstandard
        uv pip install -r requirements.txt

    - name: Compile with Nuitka
      run: |
        .venvScriptsactivate
        python -m nuitka --standalone --onefile --windows-disable-console app.py

    - name: Upload artifact
      uses: actions/upload-artifact@v3
      with:
        name: windows-executable
        path: app.exe

The beauty of uv in CI/CD is its speed. Traditional pip-based workflows might take 2-3 minutes just for dependency installation. With uv, that drops to 15-30 seconds, which adds up significantly if you’re building frequently.

Troubleshooting Common Issues

When compiling complex projects, you’ll inevitably run into issues. Here are the most common problems and their solutions.

If Nuitka can’t find a module even though it’s installed, the issue is usually that the module is imported dynamically or conditionally. Use the –include-package flag to force inclusion:

python -m nuitka --standalone --onefile --include-package=my_module your_script.py

If your compiled executable crashes with import errors at runtime, check if you’re using packages that need plugins. Run python -m nuitka –plugin-list and look for your package. Then enable it with –enable-plugin=package-name.

For Poetry projects, if Nuitka complains about missing dependencies, make sure you’re running the compilation from within the Poetry shell. You can verify this by checking which python shows you the Poetry environment’s Python interpreter.

If the compilation takes forever or uses too much memory, try reducing the optimization level with –python-flag=no_asserts or compiling without optimizations first using –python-flag=-O0. Once it works, you can gradually increase optimization.

PyInstaller vs Nuitka

Feature PyInstaller Nuitka
Method Packer (bundles files) Compiler (C/C++ → Binary)
False Positives ⚠️ Very common ✅ Rare
Performance = Python Interpreter 🚀 10-20% faster
EXE Size 15-30 MB (smaller) 25-50 MB (larger)
Compilation Time ⚡ Fast (seconds) 🐌 Slower (minutes)
Setup Easy Requires C compiler
Source Protection Medium Excellent
Best For Quick & Dirty Production Deployments

My Recommendation

  • Development/Prototyping: PyInstaller (if false positives don’t matter)
  • Production/Client Projects: Nuitka (more reliable, safer)
  • Performance-Critical: Nuitka (noticeably faster)

Conclusion

Switching from PyInstaller to Nuitka was a game changer for me. Yes, compilation takes longer. Yes, the setup is more complex. But:

✅ No more false positives
✅ Better performance
✅ More professional solution for production environments

For internal tools or prototypes, PyInstaller is perfectly fine. But as soon as you’re distributing software to clients or deploying at scale, I’d always recommend Nuitka.

Have you had similar experiences? Which solution do you prefer? Drop a comment below! 👇

Like this article? Follow me for more Python tips and automation content! 🐍

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

🧠Maybe I Just Do Not Get It!

Next Post

รัน Typhoon 2.5 บน Colab ฟรี: จาก 30B (ไม่ไหว) สู่ 4B “Sweet Spot”

Related Posts