Code of the Day
IntermediatePolish and Persistence

Saving game state

Decide what state is worth persisting, understand the boundary between session state and persistent state, and make your data JSON-safe before writing it to disk.

Game DevIntermediate5 min read
By the end of this lesson you will be able to:
  • Identify which state is worth saving (score, level, position) versus discarding on quit
  • Distinguish session state from persistent state
  • Understand which Python types are JSON-safe and how to serialise non-safe types

Most games need at least one form of persistence: a high score that survives restarts, an unlocked level that the player should not have to re-earn, or a saved position so the player can continue where they left off. Choosing what to save and how to represent it is a design decision with technical consequences.

Session state versus persistent state

Session state is everything that belongs to a single play session. It is valid from "start game" to "quit" and should be discarded when the player quits or dies. Score during a run, current enemy positions, the player's momentum — none of this needs to survive a restart.

Persistent state is information that should survive between sessions. High scores, unlocked levels, player preferences (volume, difficulty), accumulated currency or upgrades in a roguelike. This is what you write to disk.

Confusing the two causes bugs in both directions: saving too much clutters the save file with transient data that will be wrong on reload; saving too little means the player loses progress unexpectedly.

What is worth saving?

As a rule, save the facts that determine what the player has earned or discovered, not the in-progress simulation:

SaveDo not save
High scoreCurrent session score
Unlocked levels (list of IDs)Current level's enemy positions
Player preferencesThe current animation frame
Checkpoint positionVelocity / momentum

For a "continue" feature, save the player's world position and level ID at checkpoint moments — not every frame.

JSON-safe types

The simplest cross-platform save format is JSON. The Python json module handles these types natively:

  • int, float, bool, None
  • str
  • list (of the above)
  • dict with str keys

These types are not JSON-safe and must be converted:

pygame.Rect — serialise as {"x": rect.x, "y": rect.y, "w": rect.width, "h": rect.height}.

pygame.Surface — do not save surfaces. Save the path string of the source image instead and reload it on load.

Tuplesjson.dumps() serialises tuples as JSON arrays, which load back as list. Either accept that and use list, or use a wrapper.

Sets — not JSON-serialisable. Convert to a sorted list before saving.

# Example: converting non-safe types before saving
save_data = {
    "high_score":     150,
    "unlocked_levels": sorted(list(unlocked_set)),   # set → sorted list
    "last_position":  {"x": player.rect.x, "y": player.rect.y},
    "volume":         0.7,
}

If your save data grows to include complex nested objects, consider a lightweight schema — a helper function that converts your game objects to dicts and another that reconstructs them. Keeping serialisation logic in one place prevents the save format from silently diverging from the game state.

Save file location

Do not write save files next to the game executable. On most operating systems the game directory may be read-only (installed games in /usr/share on Linux, or an app bundle on macOS). The correct location is the user's home directory or the platform-specific app data folder.

A portable choice in Python is pathlib.Path.home() / ".mygame" / "save.json". The next lesson shows how to create the directory if it does not exist and handle the file-not-found case on first run.

Where to go next

Next: save and load config — write game state to JSON, load it back, and read a config.json for tunable values.

Finished reading? Mark it complete to track your progress.

On this page