PyInstaller packaging
Run PyInstaller to build a standalone executable, bundle asset folders, and write a .spec file so the build is repeatable.
- 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.pyOn 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_pathReplace 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.specSmoke-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.
Packaging concepts
Distributing a .py file requires the player to have Python and the right packages installed. Learn the options for bundling your game into something anyone can run.
Cross-platform concerns
Windows, macOS, and Linux differ in path separators, filesystem case sensitivity, sound format support, and executable permissions — know these before shipping.