This technical reference provides production-ready build scripts, advanced configuration options, and deployment strategies for compiling Python applications across Windows, macOS, and Linux platforms.
Tool | Python Support | Latest Version | Performance | Notes |
---|---|---|---|---|
PyInstaller | 3.8-3.12 | 6.15.0+ | Same as Python | Most popular, mature |
Nuitka | 3.4-3.13 | 2.5+ | 2-4x faster | True compilation |
cx_Freeze | 3.8-3.13 | 6.15.0+ | Same as Python | Cross-platform |
Important: PyInstaller officially supports Python 3.8-3.12 as of version 6.15.0. Nuitka supports Python 3.4-3.13 with version 2.5 adding Python 3.13 support. Always check the latest documentation for your specific Python version.
Production-ready Windows build script with error handling and cleanup:
@echo off
setlocal ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
REM ===============================================
REM Windows Python Build Script (PyInstaller)
REM Usage:
REM build.bat # build executable
REM build.bat clean # remove build artifacts
REM ===============================================
set "SCRIPT_DIR=%~dp0"
pushd "%SCRIPT_DIR%"
if /I "%1"=="clean" (
echo [CLEAN] Removing build/, dist/, .venv/, *.spec ...
if exist build rmdir /s /q build
if exist dist rmdir /s /q dist
if exist .venv rmdir /s /q .venv
if exist *.spec del /q *.spec
echo [CLEAN] Done.
popd
exit /b 0
)
echo [CHECK] Verifying Python installation...
where python3 >NUL 2>&1
if errorlevel 1 (
echo [ERROR] Python3 not found on PATH. Install Python 3.8-3.12 and try again.
popd
exit /b 1
)
echo [STEP] Creating isolated virtual environment...
python3 -m venv .venv
if errorlevel 1 (
echo [ERROR] Failed to create virtual environment.
popd
exit /b 1
)
echo [STEP] Activating virtual environment...
call .venv\Scripts\activate.bat
if errorlevel 1 (
echo [ERROR] Failed to activate virtual environment.
popd
exit /b 1
)
echo [STEP] Upgrading pip and installing build tools...
python3 -m pip install --upgrade pip setuptools wheel
python3 -m pip install pyinstaller
echo [STEP] Installing application dependencies...
REM Install your app's specific requirements here
python3 -m pip install "openai>=1.55.0" requests
echo [STEP] Building single-file executable...
REM Key PyInstaller options:
REM --onefile: Create a single executable file
REM --windowed: Hide console window (for GUI apps)
REM --add-data: Include additional files (Windows uses semicolon separator)
REM --hidden-import: Force include modules that PyInstaller might miss
REM --exclude-module: Exclude unnecessary modules to reduce size
REM --icon: Add custom icon
pyinstaller --onefile ^
--name myapp ^
--add-data "config.ini.example;." ^
--hidden-import=pkg_resources.py2_warn ^
--exclude-module=tkinter ^
--icon=app.ico ^
myapp.py
if errorlevel 1 (
echo [ERROR] PyInstaller build failed.
popd
exit /b 1
)
echo [SUCCESS] Build complete!
if exist "dist\myapp.exe" (
echo [INFO] Executable created: "%SCRIPT_DIR%dist\myapp.exe"
echo [INFO] File size:
dir "dist\myapp.exe" | findstr "myapp.exe"
) else (
echo [WARN] Expected executable not found in dist\
)
popd
exit /b 0
macOS builds with code signing and notarization support:
#!/bin/bash
# macOS Python Build Script
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Configuration
APP_NAME="myapp"
PYTHON_VERSION="3.12" # PyInstaller supports 3.8-3.12 as of 2025
CODESIGN_IDENTITY="${CODESIGN_IDENTITY:-}" # Set if code signing
NOTARIZE_PROFILE="${NOTARIZE_PROFILE:-}" # Set if notarizing
cleanup() {
echo "[CLEAN] Removing build artifacts..."
rm -rf build/ dist/ .venv/ *.spec
echo "[CLEAN] Done."
}
if [[ "${1:-}" == "clean" ]]; then
cleanup
exit 0
fi
echo "[CHECK] Verifying Python $PYTHON_VERSION..."
if ! command -v python$PYTHON_VERSION &> /dev/null; then
echo "[ERROR] Python $PYTHON_VERSION not found. Install via homebrew:"
echo " brew install python@$PYTHON_VERSION"
echo "[NOTE] PyInstaller supports Python 3.8-3.12. Python 3.13+ support may be experimental."
exit 1
fi
echo "[STEP] Creating virtual environment..."
python$PYTHON_VERSION -m venv .venv
source .venv/bin/activate
echo "[STEP] Installing build dependencies..."
pip install --upgrade pip setuptools wheel
pip install pyinstaller
echo "[STEP] Installing application dependencies..."
pip install "openai>=1.55.0" requests
echo "[STEP] Building macOS application bundle..."
pyinstaller --onefile \
--name "$APP_NAME" \
--add-data "config.ini.example:." \
--hidden-import=pkg_resources.py2_warn \
--exclude-module=tkinter \
--target-arch=universal2 \
myapp.py
if [[ -n "$CODESIGN_IDENTITY" ]]; then
echo "[STEP] Code signing executable..."
codesign --force --verify --verbose --sign "$CODESIGN_IDENTITY" \
"dist/$APP_NAME"
if [[ -n "$NOTARIZE_PROFILE" ]]; then
echo "[STEP] Notarizing executable..."
# Create a ZIP for notarization
cd dist && zip "${APP_NAME}.zip" "$APP_NAME" && cd ..
xcrun notarytool submit "dist/${APP_NAME}.zip" \
--keychain-profile "$NOTARIZE_PROFILE" \
--wait
# Staple the notarization
xcrun stapler staple "dist/$APP_NAME"
rm "dist/${APP_NAME}.zip"
fi
fi
echo "[SUCCESS] Build complete!"
ls -lh "dist/$APP_NAME"
Linux build script with glibc compatibility considerations:
#!/bin/bash
# Linux Python Build Script
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
APP_NAME="myapp"
TARGET_ARCH="${TARGET_ARCH:-x86_64}" # or aarch64 for ARM
cleanup() {
echo "[CLEAN] Removing build artifacts..."
rm -rf build/ dist/ .venv/ *.spec
echo "[CLEAN] Done."
}
if [[ "${1:-}" == "clean" ]]; then
cleanup
exit 0
fi
echo "[INFO] Building for Linux $TARGET_ARCH"
echo "[INFO] glibc version: $(ldd --version | head -1)"
echo "[CHECK] Verifying Python installation..."
if ! command -v python3 &> /dev/null; then
echo "[ERROR] Python3 not found. Install via package manager:"
echo " # Ubuntu/Debian:"
echo " sudo apt update && sudo apt install python3 python3-venv python3-pip"
echo " # RHEL/CentOS/Fedora:"
echo " sudo dnf install python3 python3-venv python3-pip"
echo "[NOTE] PyInstaller supports Python 3.8-3.12. Check compatibility before using 3.13+."
exit 1
fi
echo "[STEP] Creating virtual environment..."
python3 -m venv .venv
source .venv/bin/activate
echo "[STEP] Installing build dependencies..."
pip install --upgrade pip setuptools wheel
pip install pyinstaller
echo "[STEP] Installing application dependencies..."
pip install "openai>=1.55.0" requests
echo "[STEP] Building Linux executable..."
pyinstaller --onefile \
--name "$APP_NAME" \
--add-data "config.ini.example:." \
--hidden-import=pkg_resources.py2_warn \
--exclude-module=tkinter \
--strip \
myapp.py
echo "[STEP] Analyzing executable..."
file "dist/$APP_NAME"
echo "Size: $(du -h "dist/$APP_NAME" | cut -f1)"
echo "Dependencies:"
ldd "dist/$APP_NAME" 2>/dev/null || echo " (statically linked or no dependencies)"
echo "[SUCCESS] Build complete!"
echo "[INFO] Executable: $SCRIPT_DIR/dist/$APP_NAME"
For complex applications, PyInstaller's spec files provide fine-grained control:
# myapp.spec
# -*- mode: python ; coding: utf-8 -*-
import sys
from pathlib import Path
# Build configuration
APP_NAME = 'MyApp'
MAIN_SCRIPT = 'myapp.py'
# Platform-specific settings
if sys.platform == 'win32':
ICON = 'assets/icon.ico'
SEPARATOR = ';'
elif sys.platform == 'darwin':
ICON = 'assets/icon.icns'
SEPARATOR = ':'
else: # Linux
ICON = None
SEPARATOR = ':'
# Data files to include
data_files = [
('config.ini.example', '.'),
('templates/', 'templates/'),
('assets/', 'assets/'),
]
# Hidden imports (modules PyInstaller might miss)
hidden_imports = [
'pkg_resources.py2_warn',
'pkg_resources.extern',
'sqlite3',
'email.mime.text',
]
# Modules to exclude (reduce file size)
excluded_modules = [
'tkinter',
'matplotlib',
'PyQt5',
'PySide2',
'jupyter',
]
a = Analysis(
[MAIN_SCRIPT],
pathex=[],
binaries=[],
datas=[(src, dest) for src, dest in data_files],
hiddenimports=hidden_imports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=excluded_modules,
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name=APP_NAME,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True, # Compress with UPX if available
upx_exclude=[],
runtime_tmpdir=None,
console=True, # Set False for GUI apps
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=ICON,
)
# macOS app bundle (optional)
if sys.platform == 'darwin':
app = BUNDLE(
exe,
name=f'{APP_NAME}.app',
icon=ICON,
bundle_identifier='com.yourcompany.myapp',
info_plist={
'CFBundleShortVersionString': '1.0.0',
'CFBundleVersion': '1.0.0',
'NSHumanReadableCopyright': 'Copyright © 2025 Your Company',
'NSRequiresAquaSystemAppearance': False,
},
)
Build with the spec file:
pyinstaller myapp.spec
Nuitka transpiles Python to C++ and compiles to native executables, offering better performance. Nuitka is fully compatible with Python 3 (3.4 -- 3.13) and Python 2 (2.6, 2.7):
# Install Nuitka
pip install nuitka
# Basic compilation
python -m nuitka --onefile myapp.py
# Advanced compilation with optimizations
python -m nuitka \
--onefile \
--assume-yes-for-downloads \
--follow-imports \
--enable-plugin=anti-bloat \
--include-data-dir=templates=templates \
--include-data-file=config.ini.example=config.ini.example \
--output-dir=dist \
--output-filename=myapp \
myapp.py
Note: While Nuitka 2.5 added Python 3.13 support, always check the latest compatibility matrix for your specific Python version.
cx_Freeze offers good cross-platform support:
# setup.py for cx_Freeze
from cx_Freeze import setup, Executable
import sys
# Dependencies are automatically detected, but some might need explicit inclusion
build_options = {
'packages': ['openai', 'requests'],
'excludes': ['tkinter', 'unittest'],
'include_files': [
('config.ini.example', 'config.ini.example'),
('templates/', 'templates/'),
]
}
# Platform-specific options
if sys.platform == 'win32':
executables = [
Executable('myapp.py', target_name='myapp.exe', icon='icon.ico')
]
elif sys.platform == 'darwin':
executables = [
Executable('myapp.py', target_name='myapp', icon='icon.icns')
]
else:
executables = [
Executable('myapp.py', target_name='myapp')
]
setup(
name='MyApp',
version='1.0.0',
description='My Application',
options={'build_exe': build_options},
executables=executables
)
Build with:
python setup.py build
Large executables are a common complaint. Here are strategies to minimize size:
Exclude unnecessary modules:
pyinstaller --exclude-module=matplotlib --exclude-module=numpy myapp.py
Use UPX compression:
# Install UPX first, then:
pyinstaller --upx-dir=/path/to/upx myapp.py
Split into directory distribution:
# Instead of --onefile, use --onedir for smaller main executable
pyinstaller --onedir myapp.py
Profile imports to find bloat:
# Add to your main script to see what's being imported
import sys
print("Imported modules:", len(sys.modules))
for name in sorted(sys.modules):
if not name.startswith('_'):
print(f" {name}")
ImportError: No module named 'xyz'
# Solution: Add hidden import
pyinstaller --hidden-import=xyz myapp.py
FileNotFoundError for data files
# Use this pattern in your code for data files:
import sys
from pathlib import Path
def get_resource_path(relative_path):
"""Get absolute path to resource, works for dev and PyInstaller"""
if getattr(sys, 'frozen', False):
# Running as compiled executable
base_path = Path(sys.executable).parent
else:
# Running as script
base_path = Path(__file__).parent
return base_path / relative_path
# Usage
config_path = get_resource_path('config.ini.example')
Slow startup times
# Pre-compile bytecode and exclude tests
pyinstaller --optimize=2 --exclude-module=tests myapp.py
Automate builds across all platforms:
# .github/workflows/build.yml
name: Build Executables
on:
push:
tags: ['v*']
pull_request:
jobs:
build:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
python-version: ['3.12'] # PyInstaller supports 3.8-3.12
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install --upgrade pip
pip install pyinstaller
pip install -r requirements.txt
- name: Build executable
run: |
pyinstaller --onefile --name myapp-${{ runner.os }} myapp.py
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: executables
path: dist/
- name: Create Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
with:
files: dist/*
For consistent Linux builds across different environments:
# Dockerfile.build
FROM python:3.12-slim
# Install build dependencies
RUN apt-get update && apt-get install -y \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install pyinstaller
COPY . .
RUN pyinstaller --onefile myapp.py
FROM scratch AS export
COPY --from=0 /build/dist/ /
Build with:
docker build --target export --output dist .
For production applications, code signing builds trust:
Windows (with SignTool):
REM After PyInstaller build
signtool sign /f certificate.pfx /p password /t http://timestamp.digicert.com dist\myapp.exe
macOS (with Xcode tools):
# Sign
codesign --force --verify --verbose --sign "Developer ID Application: Your Name" dist/myapp
# Notarize
xcrun notarytool submit dist/myapp.zip --keychain-profile "AC_PASSWORD" --wait
xcrun stapler staple dist/myapp
Compiled Python applications can have slower startup times. Optimize with:
pyinstaller --optimize=2
Compiled executables typically use more memory due to:
Handle configuration files gracefully:
import os
import sys
from pathlib import Path
import configparser
def get_config_path():
"""Find config file in the right location"""
if getattr(sys, 'frozen', False):
# Running as executable
exe_dir = Path(sys.executable).parent
# Check next to executable first
if (exe_dir / 'config.ini').exists():
return exe_dir / 'config.ini'
# Check standard locations
locations = [
Path.cwd() / 'config.ini',
Path.home() / '.config' / 'myapp' / 'config.ini',
Path(os.getenv('APPDATA', '')) / 'MyApp' / 'config.ini' if sys.platform == 'win32' else None,
]
for location in filter(None, locations):
if location.exists():
return location
# Return default location for creation
return Path.home() / '.config' / 'myapp' / 'config.ini'
Organize your project for easy compilation:
myproject/
├── src/
│ ├── myapp/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ └── modules/
├── data/
│ ├── config.ini.example
│ └── templates/
├── scripts/
│ ├── build_windows.bat
│ ├── build_macos.sh
│ └── build_linux.sh
├── requirements.txt
├── myapp.spec
└── README.md
This guide was tested with Python 3.12 and the latest versions of PyInstaller (6.15.0), Nuitka (2.5), and cx_Freeze as of 2025. PyInstaller supports Python 3.8-3.12, while Nuitka supports 3.4-3.13. Always consult official documentation for the latest compatibility information.