Code of the Day
AdvancedPublishing

Lab: Publish your game

Package the complete game with PyInstaller, verify the build runs without Python installed, and write a one-page itch.io description.

Lab · optionalGame DevAdvanced30 min
Recommended first
By the end of this lesson you will be able to:
  • Audit and fix all hardcoded asset paths to use pathlib.Path
  • Bundle the game and its assets with PyInstaller
  • Verify the standalone executable launches without a Python installation
  • Write a concise itch.io page description

This lab walks through the full publishing pipeline from source to itch.io page. Work through each step on your completed game. If you do not have a finished game yet, the code snippets apply to any pygame project.

These are shell/terminal commands and Python edits. Run shell commands in your terminal in the root directory of your game project.

Step 1 — Audit paths

Search your codebase for hardcoded string paths:

grep -rn "open(" src/
grep -rn "pygame.image.load(" src/
grep -rn "pygame.mixer" src/
grep -rn "pygame.font.Font(" src/

Any call that uses a string literal like "assets/player.png" is a candidate for a bug. Replace all of them with pathlib.Path as shown in the previous lesson.

Add the asset_path helper at the top of your entry-point file:

import sys
from pathlib import Path

def asset_path(relative):
    """Works in development and after PyInstaller bundling."""
    base = Path(getattr(sys, '_MEIPASS', Path(__file__).parent))
    return base / relative

Then replace every asset load:

# Before
img = pygame.image.load("assets/sprites/hero.png")

# After
img = pygame.image.load(asset_path("assets/sprites/hero.png"))

Run the game from the project root and confirm it still works before proceeding.

Step 2 — Rename assets to lowercase

Check for uppercase letters in your assets directory:

find assets/ -name "*[A-Z]*"

Rename any offending files. Update the corresponding load calls. Run the game again on Linux if possible (a Docker container is enough):

docker run --rm -v "$PWD:/game" python:3.12-slim \
  bash -c "pip install pygame && cd /game && python game.py"

A FileNotFoundError here reveals a case-sensitivity bug caught before it hits a player.

Step 3 — Convert audio to .ogg

List audio files that are not .ogg:

find assets/ -name "*.mp3" -o -name "*.wav" | head -20

Convert with ffmpeg (install once: sudo apt install ffmpeg or brew install ffmpeg):

for f in assets/sfx/*.mp3; do
  ffmpeg -i "$f" "${f%.mp3}.ogg" && rm "$f"
done

Update load calls to reference the new filenames.

Step 4 — Run PyInstaller

Install PyInstaller if needed:

pip install pyinstaller

First build — let PyInstaller auto-detect everything:

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

On Windows replace : with ; in --add-data. Check dist/MyGame (or dist/MyGame.exe) was created.

Step 5 — Edit the .spec for repeatability

Open the generated MyGame.spec and verify the datas list includes all asset folders. Add any that are missing:

datas=[
    ('assets',  'assets'),
    ('fonts',   'fonts'),
    ('data',    'data'),    # add if you have a save/config folder
],

Rebuild from the spec:

pyinstaller MyGame.spec

Commit MyGame.spec to version control. This is now your canonical build recipe — anyone who clones the repo can run pyinstaller MyGame.spec and get an identical output.

Step 6 — Smoke test on a clean machine

Copy dist/MyGame (and nothing else) to a machine without Python installed, or test inside a clean VM / Docker container:

# Linux test in a minimal container
docker run --rm -v "$PWD/dist:/dist" ubuntu:22.04 /dist/MyGame

Confirm:

  • The window opens.
  • All images, sounds, and fonts load.
  • The game plays through at least one full session.
  • There are no console errors.

Step 7 — Package for distribution

Create a distribution archive:

# macOS / Linux
cd dist && zip -r MyGame-linux.zip MyGame

# Windows (PowerShell)
Compress-Archive -Path dist\MyGame.exe -DestinationPath MyGame-windows.zip

Include a README.txt in the zip:

My Game  v1.0
=============
Controls:
  Arrow keys  — move
  Space       — jump
  H           — attack

Linux: chmod +x MyGame then ./MyGame
macOS: right-click → Open (first time only)
Windows: double-click MyGame.exe

Report bugs at: https://yourusername.itch.io/mygame

Step 8 — Publish on itch.io

  1. Create an account at itch.io.
  2. Click Create new project.
  3. Set kind to Downloadable.
  4. Upload your zip file(s). Mark each with the correct OS.
  5. Write a short description. A good template:
A 2D platformer featuring physics-based movement, AI enemies with
pathfinding, and particle effects.

Controls: Arrow keys to move, Space to jump, H to attack.

Built with Python and pygame.
  1. Add at least one screenshot (take one with pygame's pygame.image.save(screen, "screenshot.png")).
  2. Set the price (free or pay-what-you-want for your first release).
  3. Click Save and view page to confirm everything looks correct.
  4. Publish.

itch.io provides a free "butler" CLI tool for automated uploads: butler push dist/MyGame-linux.zip yourusername/mygame:linux This is useful if you want to integrate uploads into a CI pipeline.

Congratulations — you have shipped a game.

Finished reading? Mark it complete to track your progress.

On this page