PyInstaller is a cross-platform tool that converts Python programs into standalone executables. It analyzes your Python code, bundles all dependencies (including the Python interpreter), and creates self-contained binaries for Windows, macOS, and Linux. This eliminates the need for end-users to have Python installed.
- Cross-Platform: Build executables for Windows, macOS, and Linux
- One-File Bundles: Create single .exe files with all dependencies included
- Hidden Imports Detection: Automatically detects most Python imports
- Console and GUI Support: Works with CLI apps and graphical interfaces
- Code Obfuscation: Optional encryption of Python bytecode
- Bootloader Customization: Modify startup behavior and splash screens
- DLL/SO Support: Bundles compiled extensions and native libraries
- No Source Exposure: Compiled to bytecode, not plain text
# Standard installation
pip install pyinstaller
# With optional dependencies for enhanced functionality
pip install pyinstaller[all]
# For development (from source)
git clone https://github.com/pyinstaller/pyinstaller
cd pyinstaller
pip install -e .
pyinstaller --version
# Create executable from script
pyinstaller script.py
# Generate single executable (one file)
pyinstaller --onefile script.py
# Build for specific OS (from current platform)
pyinstaller --onedir --windowed myapp.py
dist/
├── script/
│ ├── script.exe (or script on Linux/macOS)
│ ├── python39.dll
│ ├── library.zip
│ └── [dependencies]
└── script.exe (--onefile mode)
build/
└── script/
└── [build artifacts]
script.spec
[PyInstaller spec file - configuration]
| Option | Description | Example |
|---|
--onefile | Create single executable file | pyinstaller --onefile app.py |
--onedir | Create directory with executable | pyinstaller --onedir app.py |
--windowed | No console window (GUI apps) | pyinstaller --windowed app.py |
--console | Show console window (default) | pyinstaller --console app.py |
-n NAME | Set output name | pyinstaller -n MyApp app.py |
-i ICON | Add icon to executable | pyinstaller -i app.ico app.py |
--add-data | Include data files | pyinstaller --add-data 'src:src' app.py |
--add-binary | Include binary files | pyinstaller --add-binary 'lib.so:.' app.py |
-p PATH | Add import search path | pyinstaller -p ./libs app.py |
--hidden-import | Force include module | pyinstaller --hidden-import=module app.py |
--collect-all | Collect all submodules | pyinstaller --collect-all numpy app.py |
--strip | Strip binaries (Linux/macOS) | pyinstaller --strip app.py |
--upx-dir | UPX compression directory | pyinstaller --upx-dir=/usr/bin app.py |
--key | Encryption key for bytecode | pyinstaller --key=mykey123 app.py |
--splash | Show splash screen | pyinstaller --splash splash.png app.py |
Create a spec file for advanced configuration:
# Generate spec file
pyi-makespec --onefile --windowed app.py
# Edit spec file for fine-tuning
nano app.spec
# Build using spec file
pyinstaller app.spec
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_submodules, collect_data_files
block_cipher = None
a = Analysis(
['app.py'],
pathex=['.'],
binaries=[],
datas=[
('assets/', 'assets'),
('config.json', '.'),
],
hiddenimports=['package.module'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludedimports=['matplotlib', 'tensorflow'],
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='MyApp',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='app.ico',
)
# Create Python script
cat > hello.py << 'EOF'
import argparse
def main():
parser = argparse.ArgumentParser(description='Simple hello tool')
parser.add_argument('name', help='Name to greet')
args = parser.parse_args()
print(f"Hello, {args.name}!")
if __name__ == '__main__':
main()
EOF
# Build executable
pyinstaller --onefile --console hello.py
# Test
./dist/hello World
# Create GUI app
cat > gui_app.py << 'EOF'
import tkinter as tk
from tkinter import messagebox
class App:
def __init__(self, root):
self.root = root
self.root.title("My Application")
self.root.geometry("400x300")
tk.Label(root, text="Hello World", font=("Arial", 20)).pack(pady=20)
tk.Button(root, text="Click Me", command=self.on_click).pack()
def on_click(self):
messagebox.showinfo("Info", "Button clicked!")
if __name__ == '__main__':
root = tk.Tk()
app = App(root)
root.mainloop()
EOF
# Build with icon
pyinstaller --onefile --windowed --icon=app.ico gui_app.py
# Project structure
# myproject/
# ├── main.py
# ├── assets/
# │ ├── logo.png
# │ └── config.ini
# └── data/
# └── default.json
# Build with data files
pyinstaller --onefile \
--windowed \
--add-data 'assets:assets' \
--add-data 'data:data' \
--name MyApp \
main.py
# Flask app with static files
cat > app.py << 'EOF'
from flask import Flask, render_template
import os
app = Flask(__name__)
@app.route('/')
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=False, port=5000)
EOF
# Build with templates and static files
pyinstaller --onefile \
--add-data 'templates:templates' \
--add-data 'static:static' \
--hidden-import=flask \
app.py
# Build with native libraries
pyinstaller --onefile \
--add-binary '/usr/lib/libcustom.so:.' \
--hidden-import=numpy \
--hidden-import=scipy \
scientific_app.py
# Dynamic imports may not be detected
import importlib
module = importlib.import_module(user_input)
# Explicit hidden imports required
pyinstaller --hidden-import=requests \
--hidden-import=pandas \
--hidden-import=sklearn \
app.py
| Package | Hidden Import | Command |
|---|
| PIL/Pillow | PIL | --hidden-import=PIL |
| NumPy | numpy | --hidden-import=numpy |
| Pandas | pandas | --hidden-import=pandas |
| SQLAlchemy | sqlalchemy | --hidden-import=sqlalchemy |
| Django | django | --hidden-import=django |
| Flask | flask | --hidden-import=flask |
| Requests | requests | --hidden-import=requests |
| BeautifulSoup | bs4 | --hidden-import=bs4 |
# Collect all submodules of a package
pyinstaller --collect-all=django \
--collect-all=numpy \
app.py
# Multiple packages
pyinstaller --collect-all=package1 \
--collect-all=package2 \
--collect-all=package3 \
app.py
# Create Windows console application
pyinstaller --onefile --console app.py
# Create Windows GUI (no console)
pyinstaller --onefile --windowed app.py
# Add version information
pyinstaller --onefile \
--version-file=version.txt \
app.py
# Manifest file for Windows
pyinstaller --onefile \
--manifest=manifest.xml \
app.py
# Code signing (Windows)
pyinstaller --onefile \
--distpath ./signed \
app.py
# Then sign with signtool
signtool sign /f certificate.pfx /p password /t http://timestamp.server app.exe
# Create macOS app bundle
pyinstaller --onefile \
--osx-bundle-identifier com.example.app \
app.py
# Set macOS deployment target
MACOSX_DEPLOYMENT_TARGET=10.13 pyinstaller app.py
# Code signing for macOS
pyinstaller --onefile \
--codesign-identity "Developer ID Application: Company" \
app.py
# Notarization (required for distribution)
xcrun altool --notarize-app -f app.dmg -t osx -u email@example.com -p @keychain:altool-password
# Exclude unnecessary modules
pyinstaller --onefile \
--exclude-module=matplotlib \
--exclude-module=tensorflow \
--exclude-module=pandas \
app.py
# Use UPX compression
pyinstaller --onefile --upx-dir=/usr/bin app.py
# Strip binaries (Linux/macOS)
pyinstaller --onefile --strip app.py
| Configuration | Size |
|---|
| Default —onedir | 50-100 MB |
| —onefile | 60-120 MB |
| With UPX compression | 30-50 MB |
| Exclude dependencies | 20-40 MB |
# Generate encryption key
python -c "from PyInstaller.utils.otp import generate_key; print(generate_key())"
# Build with encryption
pyinstaller --onefile \
--key=<encryption-key> \
app.py
# Note: Minimal security, not true obfuscation
# Explicitly add hidden imports
pyinstaller --onefile \
--hidden-import=missing_module \
app.py
# Check imports
python -c "import missing_module"
# Add search path
pyinstaller --onefile \
-p /path/to/modules \
app.py
# Include binary files
pyinstaller --onefile \
--add-binary 'C:\path\to\lib.dll:.' \
app.py
# On Linux
pyinstaller --onefile \
--add-binary '/usr/lib/lib.so:.' \
app.py
# Build with console output for debugging
pyinstaller --onefile --console app.py
# Run and check error messages
./dist/app.exe
# Verbose logging
pyinstaller --onefile --debug=all app.py
# Profile execution
python -m cProfile -o stats.prof app.py
# Analyze
python -m pstats stats.prof
# Build without debug info
pyinstaller --onefile --strip app.py
# Create custom hook
cat > hook_mymodule.py << 'EOF'
from PyInstaller.utils.hooks import get_module_file_attribute
def get_module_bin_files(name):
return [('/path/to/binary', '.')]
def get_module_data_files(name):
return [('/path/to/data', 'data')]
binaries = get_module_bin_files('mymodule')
datas = get_module_data_files('mymodule')
EOF
# Use custom hook
pyinstaller --hookspath=. --onefile app.py
# runtime_hook.py - runs before app starts
import os
import sys
# Add environment variables
os.environ['MY_VAR'] = 'value'
# Modify system paths
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib'))
# Create splash screen
pyinstaller --onefile \
--splash splash.png \
app.py
# Update splash during execution
from pyi_splash import update_text
update_text("Loading modules...")
#!/bin/bash
APP_NAME="MyApp"
VERSION="1.0.0"
# Windows
pyinstaller --onefile --windowed \
-n "$APP_NAME" \
--distpath "./dist/windows" \
app.py
# macOS
pyinstaller --onefile --windowed \
-n "$APP_NAME" \
--distpath "./dist/macos" \
app.py
# Linux
pyinstaller --onefile --console \
-n "$APP_NAME" \
--distpath "./dist/linux" \
app.py
echo "Build complete!"
- Bytecode Not Encrypted: Executables can be reverse-engineered
- Use Additional Obfuscation: For sensitive code, use PyArmor or similar tools
- Sign Your Executables: Use code signing to ensure authenticity
- Verify Dependencies: Check all included libraries for vulnerabilities
- No Root Access Required: Don’t request unnecessary privileges
- Network Security: Validate all network operations and inputs
# Windows MSI installer (requires WiX Toolset)
heat dir dist -o files.wxs
candle files.wxs -o obj/
light obj/files.wixobj -out MyApp.msi
# macOS DMG (requires create-dmg)
create-dmg --volname "MyApp" \
--icon MyApp.app 100 100 \
MyApp.dmg dist/MyApp.app
# GitHub Actions example
name: Build Executables
on: [push]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.11'
- run: pip install pyinstaller -r requirements.txt
- run: pyinstaller --onefile app.py
- uses: actions/upload-artifact@v2
with:
name: executable-${{ matrix.os }}
path: dist/
| Tool | Single File | Size | Speed | Learning Curve |
|---|
| PyInstaller | Yes | Medium | Fast startup | Low |
| cx_Freeze | Yes | Large | Slower | Medium |
| py2exe | Windows only | Large | Fast | Low |
| py2app | macOS only | Large | Fast | Low |
| Nuitka | Yes | Small | Fast | Medium |
| Cython | Yes | Small | Very fast | High |
| Task | Command |
|---|
| Simple CLI app | pyinstaller --onefile --console app.py |
| GUI application | pyinstaller --onefile --windowed --icon=icon.ico app.py |
| Web app with assets | pyinstaller --onefile --add-data 'templates:.' app.py |
| With data files | pyinstaller --onefile --add-data 'data:data' app.py |
| Multiple dependencies | pyinstaller --onefile --collect-all package app.py |
| Optimized build | pyinstaller --onefile --strip --upx-dir=/usr/bin app.py |