The "how do I turn my Python script into an .exe" question has a popular answer (PyInstaller) and four other answers that win in specific cases (Nuitka for speed, BeeWare for native apps, PyOxidizer for embedded Python, cx_Freeze for the legacy case). The hard part isn't the tool choice; it's everything that comes after: antivirus false positives, code signing, the cross-platform build trap, and the --onefile vs --onedir decision that affects startup time and AV detection. This guide compares all five tools, walks through the AV defense playbook, explains why you can't build a Windows .exe on macOS, and covers code signing for public distribution. It's one of 10 explainers in our Python Environment Setup pillar page.
Key Takeaways
PyInstaller is the default starting point with the largest community and best-documented workflow, but its bootloader is famous for antivirus false positives.
Nuitka compiles Python to C then to native machine code for 2-4x runtime speedup on compute-heavy code, at the cost of longer build times.
BeeWare Briefcase produces real platform-native bundles (MSI, .app, .deb) and is the right pick for GUI apps that need to look like native software.
You can't cross-compile a Windows .exe on macOS or Linux. Use CI runners on each target platform, GitHub Actions makes this free for public projects.
Code signing is mandatory for public distribution. Without it, Windows SmartScreen and macOS Gatekeeper scare users away from your unsigned binary.
--onefile trades startup speed and AV false positives for distribution convenience. For real apps, prefer --onedir packed into an installer.
Pick by what you're shipping. Then remember: a tool on macOS produces macOS binaries only, never Windows .exe.
The Five Tools in Brief
PyInstaller
The default. Released 2005, actively maintained, runs on Windows, macOS, and Linux. Analyzes your script, finds imports, bundles them plus the Python interpreter into a folder or single file.
pip install pyinstaller
# Folder bundle (recommended):
pyinstaller --onedir myapp.py
# Single file (convenient, with caveats):
pyinstaller --onefile myapp.py
# With an icon and no console window (GUI app):
pyinstaller --onefile --windowed --icon=app.ico myapp.py
# Result is in dist/myapp/ (or dist/myapp.exe for --onefile)
Nuitka compiles Python to C and then to a native binary. The result runs 2-4x faster than CPython on compute-heavy code, and the binary is harder to reverse-engineer than a PyInstaller bundle. Build times are longer (10-30 minutes for a real app), and some Python features (dynamic exec, certain metaclasses) need workarounds.
pip install nuitka
# Standalone build (folder with everything needed):
python -m nuitka --standalone myapp.py
# Single .exe with embedded dependencies:
python -m nuitka --onefile myapp.py
# GUI app on Windows (no console):
python -m nuitka --onefile --windows-disable-console myapp.py
BeeWare's Briefcase produces real platform-native installer formats (MSI on Windows, .app bundle and .pkg on macOS, .deb/.rpm/AppImage on Linux). Output looks like proper installed software, not a Python script in a coat. Best fit for GUI apps where the "this looks weird" feeling kills user trust.
pip install briefcase
briefcase new # scaffolds a new project
cd myapp
briefcase create # creates platform-specific bundle structure
briefcase build # compiles
briefcase package # produces installer (MSI, .app, .deb)
PyOxidizer (Rust-based) embeds a Python interpreter directly inside your binary, without unpacking files at runtime. The result is one self-contained executable that starts faster than PyInstaller --onefile and is harder for antivirus to false-flag because there's no bundle pattern to match.
cargo install pyoxidizer
pyoxidizer init-config-file myapp
cd myapp
# Edit pyoxidizer.bzl
pyoxidizer build
pyoxidizer run
The setup is heavier (requires Rust toolchain), but for long-lived projects where the binary is shipped for years, the cleanest result. Docs: pyoxidizer.readthedocs.io.
cx_Freeze
The original Python freezer (since 2002). Still maintained but rarely the right pick for new projects. Keep it if you inherit a cx_Freeze project that works; don't reach for it for new work in 2026.
Side-By-Side Comparison
Capability
PyInstaller
Nuitka
BeeWare
PyOxidizer
cx_Freeze
Output type
Bundle or .exe
Bundle, .exe, or native binary
Native installer
Self-contained binary
Bundle
Build time
Fast (~1-3 min)
Slow (10-30 min)
Medium
Medium
Fast
Runtime speed
Same as CPython
2-4x faster
Same as CPython
Same as CPython
Same as CPython
AV false positives
Common
Rare
Rare
Very rare
Sometimes
Cross-platform tool
Yes (build on each)
Yes (build on each)
Yes (build on each)
Yes (build on each)
Yes (build on each)
GUI-app native look
No
No
Yes
No
No
Community size
Largest
Medium
Medium
Small
Small
Setup difficulty
Easy
Easy (slow build)
Medium
Hard (Rust)
Easy
The Antivirus False Positive Problem
If you ship a PyInstaller .exe to a few users, expect at least one report of "Windows Defender deleted your file" or "Avast quarantined the download." It's not your code; it's how PyInstaller bundles work.
Why It Happens
The PyInstaller bootloader is the small executable at the front of every PyInstaller binary. Its job is to extract the bundled Python interpreter and libraries to a temp directory and then run your script. The same bootloader is used by every PyInstaller user, which means the same bytecode pattern shows up in malware that was also packed with PyInstaller. Antivirus heuristics flag the pattern; your clean app inherits the suspicion. This is documented on discuss.python.org and the PyInstaller issue tracker.
Five Defenses (In Order of Effort)
Switch from --onefile to --onedir. Lower trigger rate because there's no compressed-binary-with-runtime-extraction pattern. The cost is shipping a folder instead of a file (an installer wraps it).
Submit your binary to antivirus vendors as a false positive. Microsoft, Avast, McAfee, and the rest have submission portals. Whitelist usually arrives in 2-7 days.
Rebuild PyInstaller's bootloader from source. A fresh-compiled bootloader has a different signature from the cached one antivirus knows. Walkthrough in the PyInstaller docs.
Code sign with a real certificate. Antivirus often trusts signed binaries from CAs they recognize, especially EV certificates. This is the path public-distribution apps must take anyway.
Switch to Nuitka or PyOxidizer. Different bootloader patterns, much lower false-positive rate by default.
Don't pack with UPX if AV is already a concern. UPX (the executable packer that PyInstaller can use to shrink output) is heavily used by malware authors, so packed binaries trigger AV at much higher rates than unpacked ones. Save 30% file size, gain 5x AV reports. Add --noupx to your PyInstaller invocation.
--onefile vs --onedir: The Trade-Off
PyInstaller (and Nuitka) both offer two output modes:
Aspect
--onefile
--onedir
What you ship
1 .exe file
Folder with .exe + libs
Startup time
Slower (unpack on every run)
Fast (libs already on disk)
AV false positives
Higher
Lower
Behavior in restricted temp dirs
Can fail (no write access)
Works
Convenience for one-off distribution
High
Lower (zip it or installer)
Right for
Small utility, casual sharing
Real app, pro distribution
For something you're going to redistribute via an installer (Inno Setup, MSI, etc.), --onedir wins on every axis. For "I want a single file I can email someone," --onefile is more convenient.
Cross-Platform Builds: The Constraint
None of these tools cross-compile. The .exe you produce runs on the OS you built it from.
GitHub Actions provides free Windows, macOS, and Linux runners for public repos. The matrix strategy builds all three platforms from one workflow. For private repos, Linux runners are free; Windows and macOS time is metered.
The Tempting-But-Wrong Workarounds
Wine on Linux. Sometimes works for very simple PyInstaller builds but breaks on anything that uses native Windows APIs or extension modules. Don't ship from this.
Docker on Linux with a Windows image. Microsoft's mcr.microsoft.com/windows requires Windows host. Doesn't help on Linux/macOS hosts.
Building on Windows for macOS. Not possible because macOS frameworks aren't available.
The honest answer is to use the right OS for each build, which is what CI is for.
Code Signing for Public Distribution
For public distribution, an unsigned binary triggers Windows SmartScreen ("Windows protected your PC") and macOS Gatekeeper ("cannot be opened because the developer cannot be verified"). End users see these warnings and don't install.
Windows Code Signing
Buy a code signing certificate from a CA (DigiCert, Sectigo, GlobalSign). Standard certificates cost ~200 USD/year; EV (Extended Validation) certificates cost ~400 USD/year and clear SmartScreen warnings faster. Sign with signtool:
Join the Apple Developer Program (99 USD/year). Sign with codesign then submit to Apple's notary service:
codesign --deep --force --options=runtime --sign "Developer ID Application: Your Name (TEAMID)" myapp.app
xcrun notarytool submit myapp.app.zip --keychain-profile "AC_PASSWORD" --wait
xcrun stapler staple myapp.app
Linux Code Signing
Linux doesn't have a system-wide code-signing requirement. For distribution through Flatpak, Snap, or specific distros, follow their respective signing processes.
Size Optimization
A naive PyInstaller build of "hello world" is ~10 MB. A FastAPI app can be 80+ MB. Three ways to trim:
Exclude unused modules.--exclude-module tkinter drops the Tk GUI library; --exclude-module test drops the Python test suite; combine multiple --exclude-module flags to drop more.
Use a minimal base Python. Build in a fresh venv with only the dependencies your app needs; PyInstaller's analysis will then only bundle those.
Don't use UPX for the AV reasons above. The 30% size saving isn't worth the AV reports.
For really small binaries, Nuitka with --lto=yes often produces tighter output than PyInstaller because the compiler can dead-strip unused code paths.
Common Pitfalls
"My PyInstaller exe works on my machine but not on Windows 10 / Windows 7"
Either the target Windows is missing the Visual C++ Redistributable (PyInstaller bundles depend on it) or you built with a newer Python than the target supports. Build with the oldest Python you want to support; ship the VC++ redistributable in your installer.
"The exe is huge"
See size optimization above. Also: if you imported the entire SciPy or PyTorch by mistake, exclude what you don't use. Sometimes you imported matplotlib for a single helper and the whole library got bundled.
"The exe runs the script twice"
Multiprocessing on Windows starts a new Python process for each worker; in a frozen exe, that new process is another copy of your exe. Use multiprocessing.freeze_support() at the top of your if __name__ == "__main__": block to make multiprocessing aware of the frozen-exe case. See our explainer What does if __name__ == "__main__": do? for the underlying mechanism.
"Antivirus deleted my exe after my user downloaded it"
The five defenses above. Code signing is the only one that's a permanent fix for public distribution.
Always ask "does my user even have Python installed?" before committing to the .exe path. If yes, pip is a much smaller distribution mechanism.
Frequently Asked Questions
What is the best way to turn a Python script into an .exe in 2026?
For most users, PyInstaller. It has the largest community, the most StackOverflow answers, and works on Windows, macOS, and Linux. For users who need real performance gains, Nuitka compiles Python to C then to a native binary and runs 2-4x faster on compute-heavy code. For cross-platform GUI apps that need to look native, BeeWare's Briefcase produces proper platform-native bundles. For long-lived projects where you want one binary with embedded Python and zero runtime install, PyOxidizer is the most thorough option. The right choice depends on what you're distributing, not which tool is newest.
Why does my PyInstaller .exe get flagged by antivirus?
PyInstaller's bootloader (the small executable that unpacks and runs your code) has been used by malware authors to wrap actual malware. Antivirus heuristics flag the bootloader pattern, so your clean app inherits the suspicion. Five defenses help: rebuild PyInstaller's bootloader from source so your binary doesn't match the prebuilt signature antivirus knows; switch from --onefile to --onedir, which has fewer triggers; sign the executable with a code signing certificate from a trusted CA; submit the false positive to your target antivirus vendors for whitelisting; or switch to Nuitka or PyOxidizer, which don't have PyInstaller's bootloader reputation.
What is the difference between PyInstaller --onefile and --onedir?
--onefile bundles everything into a single .exe that unpacks to a temporary directory at runtime and runs from there. --onedir produces a folder with your .exe and a dist/ directory of every library file. The trade-offs: --onefile is one file you ship but has slower startup (unpacking happens every run), can fail on systems with restrictive temp directories, and triggers more antivirus false positives. --onedir starts faster, behaves more normally, but requires the user to keep the whole folder together. For tools you'll redistribute via an installer, --onedir is usually better; for one-file utilities, --onefile is more convenient.
Can I build a Windows .exe on macOS or Linux?
Not directly. PyInstaller, Nuitka, BeeWare, and PyOxidizer all build for the platform they run on; they don't cross-compile to a different OS. To build a Windows .exe, you need a Windows machine (or Windows in a VM, or Windows in a Docker container with mcr.microsoft.com/windows). To build a macOS app, you need macOS. To build a Linux binary, you need Linux. The practical answer for cross-platform distribution is to set up CI runners on all three platforms and build the artifacts there. GitHub Actions has free runners for Windows, macOS, and Linux, which makes this manageable for small projects.
Do I need a code signing certificate to distribute a Python exe?
It depends on who's installing it. For internal corporate distribution where users trust the source, no. For public distribution where Windows SmartScreen and macOS Gatekeeper will warn end users about an unsigned binary, yes, you need code signing. A Windows code signing certificate from a trusted CA costs around 200-400 USD per year (EV certificates more); macOS notarization is included with the Apple Developer Program at 99 USD per year. Without signing, your users see scary warnings and may refuse to run the binary at all, even though it's safe.
The Bottom Line: Pick by Use Case, Then Plan for AV and Signing
PyInstaller for the default case. Nuitka when speed matters. BeeWare Briefcase for native-looking GUI apps. PyOxidizer for long-lived projects. cx_Freeze only if you inherit it. Build on the OS you're targeting, use GitHub Actions for cross-platform CI, code sign for public distribution, and plan for antivirus false positives as a regular maintenance task, not a one-time fix. For the broader distribution story, see our Docker for Python guide, the venv decision guide, or browse the full Python Environment Setup pillar page.
Build Python Skills Before Building Binaries
CodeGym's Python track teaches the language through 800+ hands-on tasks across 62 levels, all running in a browser-based WebIDE. No installer, no PyInstaller bootloader, no AV reports. First level free; full plan on the pricing page.
GO TO FULL VERSION