Skip to main content
Guides

Convert Python to EXE: A Real PyInstaller Walk-Through

Pyobfuscate Team··9 min read
Quick Answer

To convert a Python script to a .exe, install PyInstaller and run pyinstaller --onefile your_script.py — the standalone executable lands in dist/. It bundles the Python interpreter plus your code, so it runs on machines with no Python installed. Two catches most guides skip: you can only build a Windows .exe on Windows (PyInstaller can't cross-compile), and the .exe is not source protection — it can be unpacked back to bytecode and decompiled, so obfuscate first if the logic is valuable.

"How do I turn my Python script into an exe?" — I get why people ask. You wrote something useful, and you want to hand a client or a friend a single file they can double-click, without walking them through installing Python. That part genuinely works well.

But most "py to exe" tutorials stop at the happy path and skip the things that actually bite you. So I built a real .exe on this Windows machine with the current PyInstaller 6.21.0 (Python 3.14.2) and wrote down exactly what happened — the command, the file sizes, the limitation that breaks online converters, and the security reality.

The tool: PyInstaller

There are a few packagers (Nuitka, cx_Freeze, py2exe), but PyInstaller is the one most people mean by "py to exe." It works by bundling the CPython interpreter, your script's compiled bytecode, and every library it imports into one distributable. Install it with pip:

pip install pyinstaller
install

Here's the tiny script I packaged — a stand-in for a real CLI tool, with an import so the bundle has something to pull in:

import hashlib
import sys

def fingerprint(text):
    return hashlib.sha256(text.encode()).hexdigest()[:16]

def main():
    name = sys.argv[1] if len(sys.argv) > 1 else "world"
    print(f"Hello, {name}!")
    print("fingerprint:", fingerprint(name))

if __name__ == "__main__":
    main()
greeter.py

Building the .exe (the real run)

One command does it. --onefile collapses everything into a single executable:

$ pyinstaller --onefile greeter.py
INFO: PyInstaller: 6.21.0
INFO: Python: 3.14.2
INFO: Building PYZ (ZlibArchive) ... completed successfully.
INFO: Building EXE from EXE-00.toc completed successfully.

$ dist\greeter.exe Mithun
Hello, Mithun!
fingerprint: 7f6c499c46b87f83
build + run (real output, trimmed)

That's the whole thing: a dist/greeter.exe that runs on a Windows box with no Python installed, producing the exact same output as the script. For a lot of use cases — a utility for a non-technical client, an internal tool, a game script — that's all you need.

--onefile vs --onedir (measure before you pick)

PyInstaller has two output modes, and the difference is real. I built the same script both ways and measured:

ModeOutputSizeStartup
--onefileOne greeter.exe8.8 MBSlower — self-extracts to a temp dir on every launch
--onedirgreeter.exe + _internal/ folder20 MB total (1.97 MB launcher)Faster — nothing to extract

--onefile is tidier to hand over (literally one file), but it unpacks itself into a temporary folder each time it runs, so it starts slower and can trip antivirus. --onedir is bulkier and "messier" (a folder of files) but launches faster and is easier to debug. For a double-click-and-share tool I use onefile; for something launched often, onedir.

Neither mode makes the app smaller than Python itself — you're shipping the whole interpreter. An 8.8 MB "Hello world" is normal, not a mistake.

The catch that breaks online converters: no cross-compiling

This is the big one, and it's why you should be suspicious of any website that claims to "convert Python to exe" from your browser: PyInstaller cannot cross-compile. To build a Windows .exe you must run PyInstaller *on Windows*; a macOS .app must be built on macOS; a Linux binary on Linux. There is no flag that turns a Linux server into a Windows-exe factory.

So a real online "py to exe" service can't just run PyInstaller on one Linux box — it needs an actual Windows machine to build on. That's exactly how our Python to EXE tool works: instead of faking it, it generates a GitHub Actions workflow that builds your executable on real Windows, macOS, and Linux runners and hands you the artifacts. Same PyInstaller, but on genuine per-OS machines — the only honest way to do it in the cloud.

If a browser tool promises an instant Windows .exe with no build step, be careful — either it's building on a real Windows runner (fine, but not instant) or it isn't producing a real native exe.

Problems you will actually hit

  • Antivirus false positives. A self-extracting --onefile exe from an unknown publisher looks, to a heuristic scanner, a lot like malware. Unsigned exes get flagged. Code-signing or shipping --onedir reduces this.
  • Missing modules at runtime. PyInstaller finds imports by static analysis, so anything imported dynamically (importlib, plugins, some libraries) may be left out. The fix is --hidden-import modulename or a hook.
  • Data files not found. Bundled images/configs need --add-data, and your code must locate them via the sys._MEIPASS temp path at runtime, not a relative path.
  • Size. Every heavy dependency (pandas, numpy, PyQt) balloons the exe. --exclude-module and a clean virtualenv keep it down.

Reality check: an .exe is not source protection

This is the part that matters if your script has valuable logic or secrets. Packaging to .exe feels like it hides your code. It doesn't. I searched the real onefile exe I'd just built for tell-tale strings:

greeter      -> FOUND
PyInstaller  -> FOUND
_MEIPASS     -> FOUND
python314    -> FOUND
grepping the .exe (real)

Those markers (_MEIPASS is PyInstaller's runtime-extraction path) instantly identify it as a PyInstaller-packed Python app. And in --onedir, the _internal/ folder literally contains python314.dll and base_library.zip sitting in the open. A tool called pyinstxtractor unpacks that archive straight back into .pyc bytecode files — and as I showed when I decompiled a .pyc, that bytecode leaks your strings and decompiles to near-original source.

Order of operations matters: an exe is a *packager*, not a protector. If the logic is worth protecting, obfuscate the source first, then package it. Doing it the other way round protects nothing.

That's the workflow we'd recommend: run your script through the Python Obfuscator (AST renaming, string encryption, and up to a fully encrypted wrapper), *then* build the .exe. Now even if someone unpacks the exe and decompiles the bytecode, what they get back is the obfuscated version — not your real code.

Build a real .exe from your Python — free

Generate a GitHub Actions workflow that compiles your script into Windows, macOS & Linux executables on real runners.

Open the Python to EXE tool

Free tools mentioned here

Frequently asked questions

How do I convert a Python script to an .exe?

Install PyInstaller (pip install pyinstaller) and run 'pyinstaller --onefile your_script.py'. The standalone .exe appears in the dist/ folder and runs on Windows machines without Python installed. On this machine, that produced a working 8.8 MB executable from a small script.

Can I build a Windows .exe on Mac or Linux?

No. PyInstaller cannot cross-compile — you must build a Windows .exe on Windows, a macOS app on macOS, and a Linux binary on Linux. Real online converters get around this by building on actual per-OS machines (e.g. GitHub Actions runners), not by cross-compiling.

Why is my Python .exe so large?

Because it bundles the entire Python interpreter and every imported library. Even a tiny script produces an ~8-9 MB onefile exe; heavy dependencies like numpy or PyQt push it much higher. Use a clean virtualenv and --exclude-module to trim it.

Does turning Python into an .exe protect my source code?

No. A PyInstaller exe can be unpacked (e.g. with pyinstxtractor) back into .pyc bytecode, which decompiles to near-original source and leaks string literals. Packaging is distribution, not protection. Obfuscate the source first, then build the exe.

onefile or onedir — which should I use?

Use --onefile for a single shareable file (slower startup, more antivirus friction). Use --onedir for faster startup and easier debugging, at the cost of shipping a folder. In my test onefile was 8.8 MB and onedir was 20 MB total with a 1.97 MB launcher.

Keep reading