To package a Python script as an exe, you bundle the Python interpreter, your script and every third-party library it imports into one self-contained executable, usually with PyInstaller on Windows. You are not compiling your code to native machine code. A Windows exe has to be built on a Windows machine, and the whole process takes about ten minutes once your script already runs.
That last point is the whole reason people do this. A script that works on your laptop dies on your colleague’s because they do not have Python, pip, or the version of pandas your code expects. Handing over one double-clickable file removes every one of those problems.
I have built enough of these to know where people get stuck, and it is almost never the packaging command itself. It is the hidden import that fails on someone else’s PC, the data file that no longer resolves, or the moment Windows Defender eats the file in transit. This guide covers the build, the testing, and those failures, in that order.
The commands below are written for Windows 10 and 11 with PowerShell or Command Prompt. On macOS and Linux the same PyInstaller commands work, but they produce a native executable for that operating system, not a Windows exe.
Table of Contents
- What You Need
- Step-by-Step: How to Package a Python Script as an EXE
- 1. Test the Script in a Clean Environment
- 2. Install PyInstaller
- 3. Run the First PyInstaller Build
- 4. Choose One-File or One-Folder Distribution
- 5. Configure the Window, Icon, Name, and Entry Script
- 6. Test the Packaged EXE
- 7. Distribute the Finished EXE Safely
- Common Mistakes
- Frequently Asked Questions
- Should I use u002du002donefile or u002du002donedir when I package a Python script as an EXE?
- Can PyInstaller build a Windows EXE from Linux or macOS?
- Why does Windows Defender flag my packaged Python program?
- How do I include data files and other resources in a PyInstaller EXE?
- How do I package a GUI Python script without showing a console window?
- Conclusion
What You Need
Here is the full list before you start:
- A Windows 10 or 11 machine to build on, if your target audience uses Windows.
- Python 3 installed, with pip working. Check with
python --version. - A script that already runs from the command line with no errors.
- A virtual environment holding only the libraries your script needs.
- PyInstaller, which is the tool that does the bundling.
That clean environment matters more than people expect. If you build from your global Python install, PyInstaller will happily bundle every package you have ever installed, which produces a bloated exe and the occasional bizarre crash from a library you never meant to ship.
One rule to internalise early: PyInstaller packages for the operating system it runs on. Build on macOS and you get a macOS app. Build on Windows and you get an exe. There is no supported cross-compile path from Linux or macOS to Windows, and if you are not willing to use a Windows machine, a Windows virtual machine or a Windows CI runner is the practical answer.
Step-by-Step: How to Package a Python Script as an EXE
Seven steps, in the order that saves you the most rework. Skipping step one is the single biggest cause of a bad first build.
1. Test the Script in a Clean Environment
Create a virtual environment next to your project and install only what the script needs:
python -m venv .venv
.venvScriptsactivate
pip install -r requirements.txt
python myscript.py
PowerShell may refuse to run the activation line as typed. The fix is Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser, then activate again.
If the script runs here, you have a known-working baseline. That matters because once you package it, any failure you see is a packaging problem rather than a code problem.
Run it from the project root, not from an arbitrary folder. PyInstaller resolves your own local modules relative to the script path, and a script that only worked because of your working directory will produce a bundled exe that cannot find its own files.
2. Install PyInstaller
Install it through the environment’s Python so it lands in the same place as your other dependencies:
python -m pip install --upgrade pyinstaller
pyinstaller --version
The version check confirms two things at once: the command is on your path, and it came from the environment you just activated rather than a global install. The version number also matters for reproducibility, so pin it in a requirements file once you have a build you trust.
Using python -m pip instead of bare pip is a small habit with a real payoff. It guarantees the install targets the same interpreter that will run the build.
3. Run the First PyInstaller Build

From the folder containing your script, the shortest way to package a Python script as an exe is one command:
pyinstaller --onefile --name mytool myscript.py
It takes a while the first time, usually a minute or two, because it walks every import in your script and copies the matching library files into the bundle. When it finishes you get three new things in that folder.
build/ is the intermediate workspace. It contains the collected modules and two diagnostics worth knowing about: warn-myscript.txt lists modules PyInstaller could not find, and xref-myscript.html is a browsable map of every import it resolved.
myscript.spec is the build recipe. Once you need hidden imports, data files or hooks, you will edit this file instead of typing long commands, and committing it to your repository gives you reproducible builds.
dist/ holds the output you actually care about. The exe is in there, and in the --onefile case it is the only thing in there.
4. Choose One-File or One-Folder Distribution

Both modes produce the same program. The difference is packaging, and it is a bigger decision than it looks.
pyinstaller --onefile --name mytool myscript.py
pyinstaller --onedir --name mytool myscript.py
| Consideration | –onefile | –onedir |
|---|---|---|
| Output | One standalone exe | Folder of exe plus dependencies |
| Startup speed | Slower, extracts to a temp folder every launch | Faster, loads files in place |
| Antivirus flags | More common false positives | Fewer flags from scanners |
| Updating a release | Replace one file | Replace the whole folder |
| Debugging a failure | Harder, no loose files to inspect | Easy, contents are visible |
My recommendation is the same regardless of which sounds nicer to ship: build --onedir first, test it, then switch to --onefile for distribution if you want a single file. Use one-folder mode whenever startup speed, troubleshooting or antivirus behaviour matters, and one-file mode when a single portable file is worth more than those.
That startup penalty is not theoretical. A one-file build unpacks its whole payload to a temporary directory on every run, and users do notice the delay on anything over roughly 30 MB.
5. Configure the Window, Icon, Name, and Entry Script
Four options do most of the work:
pyinstaller --onedir --windowed --name BackupTool --icon tool.ico --noconfirm entry.py
--name sets the output filename, so users see something meaningful instead of entry.exe. --icon needs a Windows .ico file, not a PNG, and PyInstaller will reject anything else.
--windowed is the important one to think about. A console program gets a terminal window showing output and tracebacks. A GUI program, built with --windowed or --console=False, gets no console at all, which means when it fails you see nothing happen. No error, no dialog, no clue.
That silent failure is the most reported complaint about GUI builds. Fix it by keeping a development build without --windowed so tracebacks are visible, and by sending exceptions to a log file in production rather than relying on stdout.
Your entry script is the positional argument and always goes last. PyInstaller runs that one file and follows its imports, so a script that only runs when imported will produce a bundle with no entry point.
6. Test the Packaged EXE
Copy the whole dist/ folder to a clean location, ideally a different folder on the same machine, and run the exe from the command line so you can see the output and the exit code:
cd C:Test
mytool.exe
echo %ERRORLEVEL%
Check four things. Does it start? Does it still find the files it reads and writes? Does it behave the same when launched by double-click? And does it work on a machine with no Python installed, which you can approximate with a Windows Sandbox session or a spare PC.
In --onedir mode the exe needs the rest of its folder. Sending someone only the exe is the classic mistake, and it fails with an unhelpful error because PyInstaller’s loader is one of the files that stayed behind on your machine.
7. Distribute the Finished EXE Safely
Once it passes testing, a few habits make delivery painless. Compress it in a zip, because archive files trip fewer heuristic scans than bare executables. Generate a SHA256 checksum so the recipient can confirm nothing changed in transit, and publish both on a GitHub Release with a short README covering what the tool does and which Python version built it.
Include a version number in the filename. When a user reports a problem, knowing which build they have saves a long email thread.
If your tool is unsigned, say so in the README. A single sentence stating that the download is an unsigned Python bundle, plus the SHA256, defuses most of the confusion when Windows SmartScreen shows the blue warning screen.
Common Mistakes
Each of these has a symptom you will recognise, a cause worth checking, and a fix that actually works.
ModuleNotFoundError inside the exe, with no warning at build time. Your code imported something dynamically, through importlib.import_module(), __import__(), exec() or a plugin loader. PyInstaller’s analysis phase only sees static imports, so it never knows to look. Add --hidden-import modulename, or --collect-all package when a whole package loads its own plugins, or add a line to the hiddenimports list in the spec file.
A data file is missing at runtime. Files are not bundled unless you ask. Use --add-data "data/config.json;." and read it through sys._MEIPASS in the bundled build, because the current working directory is not what you expect. The separator on Windows is a semicolon; on macOS and Linux it is a colon. Getting that wrong is the most common --add-data bug.
The GUI exe does nothing when double-clicked. Almost always --windowed swallowing the traceback. Build once without it, see the real error, fix the cause, then rebuild with the console suppressed.
Windows Defender quarantines it. This is the big one, and it comes up repeatedly on r/learnpython and r/pythonhelp. A one-file PyInstaller bundle is a large unsigned executable containing a compressed Python interpreter, which is exactly the shape malware has. It is a false positive, not evidence of a problem with your code. Signing the executable with a code-signing certificate is the real fix, and it is what stops the SmartScreen warning on end-user machines. Short term, distribute a zip, publish the SHA256, and tell recipients how to allow the file.
It runs on your machine but not on someone else’s. Check three things in order. Did you build on the same architecture, 64-bit versus 32-bit? Did you send the whole --onedir folder? And was the target machine running a different Windows version? Missing Microsoft Visual C++ runtime libraries show up as a failure to start with no message at all.
The exe is far larger than you expected. Base Python plus a scientific stack lands in the 30 to 60 MB range, and Matplotlib or PyQt push it higher. Exclude test frameworks and unused plugins with --exclude-module, drop heavy optional imports from your code path, and consider --onedir zipped for download size.
You bundled a secret. Environment files, config files and local databases sitting next to your script can end up inside the exe. Check before you distribute by opening the one-file exe with a zip tool, or by inspecting the --onedir folder, and exclude anything you would not paste into a public repository.
For anything more exotic, PyInstaller’s own troubleshooting page is version-specific and worth reading, and a table of the flags you will actually use:
| Flag | What it does | When to use it |
|---|---|---|
| –onefile | Packages everything into a single exe | Distributing to non-technical users |
| –onedir | Keeps a folder of dependencies | Faster startup and easier debugging |
| –name | Sets the output filename | Always, for a readable filename |
| –windowed | Hides the console window | GUI apps only, after testing |
| –add-data | Bundles a data file | Config, templates, certificates |
| –hidden-import | Forces a dynamic import in | Plugin loaders and importlib calls |
| –collect-all | Bundles a whole package | numpy, pandas, SQLAlchemy style packages |
| –icon | Sets the app icon (.ico only) | Any build you hand to a user |
| –noconfirm | Overwrites output without asking | Repeat builds and CI |
| –paths | Adds a search path for imports | Local modules outside the script folder |
| –debug=all | Emits verbose import tracing | When a build fails and you are stuck |
If PyInstaller is not the tool you want, three alternatives come up in the same search results. Nuitka compiles Python to C and then to a binary, giving noticeably faster startup and fewer antivirus false positives at the cost of a slower build and a more limited plugin ecosystem. cx_Freeze behaves like PyInstaller and is worth trying if a specific build fails. py2exe is Windows-only and largely unmaintained now. A fourth option that some sysadmins prefer is skipping the bundler entirely and distributing a portable Python install launched by a batch file, which is more files but far easier to audit.
Frequently Asked Questions
Should I use u002du002donefile or u002du002donedir when I package a Python script as an EXE?
Start with u002du002donedir while you are building and testing, then switch to u002du002donefile for distribution. One-folder mode starts faster because it does not extract anything on launch, is easier to inspect when something fails, and draws fewer antivirus false positives. One-file mode gives you a single portable file that is far easier to hand to a colleague. If startup speed or scan warnings matter, keep one-folder mode and compress the folder for distribution.
Can PyInstaller build a Windows EXE from Linux or macOS?
No, not for a Windows target. PyInstaller is not a cross-compiler; it bundles the interpreter and libraries for the operating system the build runs on. A build on macOS produces a macOS application and a build on Linux produces a Linux binary. To get a Windows exe you need a Windows machine, a Windows virtual machine, or a Windows CI runner such as a GitHub Actions windows-latest job that downloads your finished exe as an artifact.
Why does Windows Defender flag my packaged Python program?
Because a one-file PyInstaller build is a large unsigned executable that contains a compressed Python interpreter, which closely matches the profile of malware. It is a heuristic false positive and does not mean your code is unsafe. The durable fix is code signing with a code-signing certificate, which also clears the SmartScreen warning for end users. Meanwhile distribute a zip rather than a bare exe, publish a SHA256 checksum, and tell recipients the file is an unsigned Python bundle.
How do I include data files and other resources in a PyInstaller EXE?
Use the u002du002dadd-data option, with the source path and the destination path separated by a semicolon on Windows and a colon on macOS and Linux. For example, u002du002dadd-data u0022config/settings.json;configu0022 copies the file into that folder inside the bundle. Read it at runtime through the path exposed by sys._MEIPASS rather than the current working directory, which points somewhere unexpected inside a one-file build.
How do I package a GUI Python script without showing a console window?
Build with the u002du002dwindowed option, or u002du002dconsole=False if you use the spec file, and PyInstaller produces a windowed executable with no terminal attached. The tradeoff is that tracebacks become invisible, so a crash looks like the program doing nothing. Keep a development build without u002du002dwindowed while you test, and write caught exceptions to a log file in the shipped version. Name the entry script with a .pyw extension if you want the same effect at source level.
Conclusion
If you only remember one workflow, make it this one: clean virtual environment, --onedir build, test the exe on a machine without Python, then rebuild as --onefile for delivery. That order catches dependency problems and silent GUI failures while you can still see the error output.
To package a Python script as an exe well, the build command is the easy half. Hidden imports, bundled data files and code signing are where the real work sits, so pin your PyInstaller version, keep the spec file in version control, and read the version-specific documentation on hooks and signing when you reach that stage. It is dense, but it answers the questions the forum threads cannot.


