Python PyInstaller Packaging — Core Concepts

What PyInstaller does

PyInstaller reads your Python script, discovers every module and shared library it depends on, and bundles everything — including the Python interpreter itself — into a distributable package. The end user does not need Python or any libraries installed.

One-folder vs one-file mode

PyInstaller offers two output modes:

  • One-folder (default): Creates a dist/myapp/ directory containing the executable plus supporting files. Faster startup because files are already on disk.
  • One-file (--onefile): Packs everything into a single executable. At startup, it extracts files to a temporary directory, adding 2-10 seconds of delay. Easier to distribute but slower to launch.
# One-folder (default)
pyinstaller main.py

# One-file
pyinstaller --onefile main.py

How dependency analysis works

PyInstaller uses a hook system to discover dependencies:

  1. Static analysis — scans import and from X import Y statements in your code.
  2. Hooks — pre-written scripts that handle tricky packages. For example, the hook-numpy.py hook knows NumPy needs hidden shared libraries.
  3. Runtime hooks — scripts that run before your code starts, setting up paths or environment variables.

When analysis misses something (common with dynamic imports), you add it manually:

pyinstaller --hidden-import=sklearn.utils._cython_blas main.py

The spec file

The first run generates a .spec file — a Python script that configures the build:

# main.spec
a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[('assets/', 'assets/')],  # include data files
    hiddenimports=['requests'],
    hookspath=[],
)
pyz = PYZ(a.pure)
exe = EXE(pyz, a.scripts, [], name='MyApp', console=False, icon='icon.ico')
coll = COLLECT(exe, a.binaries, a.datas, name='MyApp')

After editing, build from the spec file directly:

pyinstaller main.spec

Including data files

PyInstaller only bundles Python modules by default. Images, config files, templates, and databases must be explicitly included:

pyinstaller --add-data "config.json:." --add-data "templates/:templates/" main.py

In your code, use the sys._MEIPASS attribute to find bundled files at runtime:

import sys
import os

def resource_path(relative):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative)
    return os.path.join(os.path.abspath('.'), relative)

config = resource_path('config.json')

This handles both development (files in the current directory) and frozen (files extracted to a temp directory) modes.

Console vs windowed

  • --console (default) — shows a terminal window. Good for CLI tools and debugging.
  • --windowed / --noconsole — hides the terminal. Required for GUI apps (Tkinter, PyQt, etc.) to look professional.

Common pitfalls

ProblemSolution
Missing module at runtimeAdd --hidden-import=module.name
Missing data filesUse --add-data or datas in spec
Antivirus false positivesSign the executable with a code-signing certificate
Large output sizeUse --exclude-module to strip unused packages
Slow one-file startupUse one-folder mode instead

Cross-platform reality

PyInstaller builds for the current platform only. You cannot build a Windows .exe on Linux. The common solutions:

  • Build on each target OS manually
  • Use CI/CD (GitHub Actions) with runners for Windows, macOS, and Linux
  • Use Docker for Linux builds from any host

Common misconception

“PyInstaller compiles Python to machine code.”

It does not. Your Python code is still interpreted by a bundled Python runtime. PyInstaller is a packager, not a compiler. Tools like Nuitka or Cython actually compile Python to C/machine code. PyInstaller’s advantage is simplicity — it works with almost any Python code without modification.

One thing to remember: PyInstaller bundles your script, its dependencies, and the Python interpreter into a standalone package — master the spec file, --hidden-import, and --add-data to handle the edge cases that trip up most first-time users.

pythonpyinstallerpackagingdistributiondesktop

See Also

  • Python Appimage Distribution An AppImage is like a portable app on a USB stick — download one file, double-click it, and your Python program runs on any Linux computer without installing anything.
  • Python Briefcase Native Apps Imagine a travel agent who repacks your suitcase for each country's customs — Briefcase converts your Python app into proper native packages for every platform.
  • Python Flatpak Packaging Flatpak wraps your Python app in a safe bubble that works on every Linux system — like a snow globe that keeps your program perfect inside.
  • Python Mypyc Compilation Your type hints are not just for documentation — mypyc turns them into speed boosts by compiling typed Python into fast C extensions.
  • Python Nuitka Compilation What if your Python code could run as fast as a race car instead of a bicycle? Nuitka translates Python into C to make that happen.