Code of the Day
AdvancedPublishing

PyInstaller packaging

Run PyInstaller to build a standalone executable, bundle asset folders, and write a .spec file so the build is repeatable.

Game DevAdvanced10 min read
Recommended first
By the end of this lesson you will be able to:
  • Run PyInstaller with --onefile and --add-data to bundle a game and its assets
  • Locate the output in the dist/ directory
  • Write a minimal .spec file for a repeatable build

PyInstaller is the standard tool for packaging a Python game into a standalone executable. The commands below are run in your terminal, not in a Python script.

Install PyInstaller with: pip install pyinstaller These are shell commands, not Python code — run them in your terminal from the root of your project.

The minimal build command

pyinstaller --onefile --windowed game.py

--onefile bundles everything into a single executable.
--windowed (Windows/macOS) suppresses the terminal window so only the game window appears.

PyInstaller creates three items in your project directory:

your-project/
├── build/          ← temporary work files (safe to delete)
├── dist/
│   └── game        ← the standalone executable (game.exe on Windows)
└── game.spec       ← the spec file (keep this)

The executable in dist/ can be copied to any machine of the same OS and run without Python installed.

Bundling assets

By default PyInstaller only includes Python modules. Image, sound, and font files must be declared explicitly with --add-data. The syntax is source:destination where the destination is relative inside the bundle:

pyinstaller --onefile --windowed \
  --add-data "assets:assets" \
  --add-data "fonts:fonts" \
  game.py

On Windows use a semicolon as the separator:

--add-data "assets;assets"

Inside your game code, asset paths must resolve correctly in both development and the bundled executable. PyInstaller sets sys._MEIPASS to the extraction directory at runtime. Use this helper at the top of game.py:

import sys
import os
from pathlib import Path

def asset_path(relative_path):
    """Return absolute path, works in dev and after PyInstaller packaging."""
    base = getattr(sys, '_MEIPASS', Path(__file__).parent)
    return Path(base) / relative_path

Replace all hardcoded asset paths with calls to asset_path:

# Before:
image = pygame.image.load("assets/player.png")

# After:
image = pygame.image.load(asset_path("assets/player.png"))

The .spec file

The first pyinstaller run generates game.spec. Edit it instead of re-running the CLI — this is what you commit to version control so the build is reproducible:

# game.spec  (abbreviated)
a = Analysis(
    ['game.py'],
    pathex=[],
    binaries=[],
    datas=[
        ('assets', 'assets'),
        ('fonts',  'fonts'),
    ],
    hiddenimports=[],
    ...
)
pyz = PYZ(a.pure)
exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    name='MyGame',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=False,   # windowed mode
    icon='assets/icon.ico',  # optional — Windows taskbar icon
)

Rebuild from the spec with:

pyinstaller game.spec

Smoke-testing the build

Before distributing, always test the executable on a clean machine (or VM) that does not have Python installed. Verify:

  • The window opens without a console error.
  • All images and sounds load (missing assets cause FileNotFoundError).
  • The dist/ folder is the only thing needed — no other files required.

Antivirus software on Windows sometimes flags PyInstaller executables as suspicious because the self-extraction mechanism resembles malware behaviour. Code signing with a purchased certificate resolves this for public releases. For friends and itch.io, most players can dismiss the warning.

Where to go next

Next: cross-platform concerns — path separators, case-sensitive file systems, sound format compatibility, and executable permissions on Linux.

Finished reading? Mark it complete to track your progress.

On this page