article thumbnail
Compiling Python
Transforming your Python code into native executables across Windows, macOS, and Linux
#python, #development

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 Compatibility Matrix (2025)

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 Build Scripts

Windows Build Process

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 Build Process

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 Process

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"

Advanced PyInstaller Configuration

Spec Files for Complex Builds

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

Alternative Compilation Tools

Nuitka: True Python Compilation

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: Cross-Platform Alternative

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

Optimization and Troubleshooting

Reducing Executable Size

Large executables are a common complaint. Here are strategies to minimize size:

  1. Exclude unnecessary modules:

    pyinstaller --exclude-module=matplotlib --exclude-module=numpy myapp.py
  2. Use UPX compression:

    # Install UPX first, then:
    pyinstaller --upx-dir=/path/to/upx myapp.py
  3. Split into directory distribution:

    # Instead of --onefile, use --onedir for smaller main executable
    pyinstaller --onedir myapp.py
  4. 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}")

Common Issues and Solutions

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

Production Deployment Strategies

GitHub Actions CI/CD

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/*

Docker-based Builds

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 .

Code Signing and Distribution

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

Performance Considerations

Startup Time Optimization

Compiled Python applications can have slower startup times. Optimize with:

  1. Lazy imports: Only import modules when needed
  2. Bytecode optimization: Use pyinstaller --optimize=2
  3. Reduce import tree: Profile and eliminate unnecessary dependencies
  4. Consider Nuitka: Often has faster startup than PyInstaller

Runtime Performance

Memory Usage

Compiled executables typically use more memory due to:

Configuration Management

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'

Recommended Project Structure

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

Troubleshooting Common Issues

Development Workflow Best Practices

  1. Test early and often: Build executables during development, not just at release
  2. Use virtual environments: Avoid polluting builds with development dependencies
  3. Version pin dependencies: Ensure reproducible builds
  4. Test on target platforms: Always test on the actual deployment environment

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.