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.
- 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:
| Save | Do not save |
|---|---|
| High score | Current session score |
| Unlocked levels (list of IDs) | Current level's enemy positions |
| Player preferences | The current animation frame |
| Checkpoint position | Velocity / 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,Nonestrlist(of the above)dictwithstrkeys
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.
Tuples — json.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.