Save, load, and config
Write game state to a JSON save file with pathlib, load it back on startup, and read a config.json for tunable game values.
- Write game state to a JSON save file using pathlib and json
- Load a save file on startup with a sensible default when the file does not exist
- Read a config.json for tunable values like volume and difficulty
The previous lesson identified what to save. This lesson shows the complete read/write cycle: writing state on quit, loading it on start, and separating it from configuration that designers might want to tweak without changing code.
This lesson does not use pygame at all — it is pure Python file I/O. You can
run these examples directly with python save_demo.py without any additional
libraries. pathlib and json are part of the standard library.
Save and load
import json
from pathlib import Path
# ── Paths ─────────────────────────────────────────────────────────────────────
SAVE_DIR = Path.home() / ".mygame"
SAVE_FILE = SAVE_DIR / "save.json"
SAVE_DIR.mkdir(parents=True, exist_ok=True) # create on first run
# ── Defaults ──────────────────────────────────────────────────────────────────
DEFAULT_SAVE = {
"high_score": 0,
"unlocked_levels": [1],
"last_position": {"x": 40, "y": 400},
}
# ── Save ──────────────────────────────────────────────────────────────────────
def save_game(score, unlocked, player_x, player_y):
data = {
"high_score": score,
"unlocked_levels": sorted(unlocked),
"last_position": {"x": player_x, "y": player_y},
}
SAVE_FILE.write_text(json.dumps(data, indent=2))
print(f"Saved to {SAVE_FILE}")
# ── Load ──────────────────────────────────────────────────────────────────────
def load_game():
if not SAVE_FILE.exists():
print("No save file found — using defaults.")
return dict(DEFAULT_SAVE) # return a copy so defaults stay clean
try:
data = json.loads(SAVE_FILE.read_text())
# Fill in any keys added in a newer game version
for key, value in DEFAULT_SAVE.items():
data.setdefault(key, value)
return data
except (json.JSONDecodeError, KeyError) as exc:
print(f"Corrupt save file ({exc}) — using defaults.")
return dict(DEFAULT_SAVE)
# ── Demo ──────────────────────────────────────────────────────────────────────
save_game(score=150, unlocked={1, 2, 3}, player_x=200, player_y=400)
state = load_game()
print("Loaded:", state)Running this creates ~/.mygame/save.json with content like:
{
"high_score": 150,
"unlocked_levels": [1, 2, 3],
"last_position": {"x": 200, "y": 400}
}A second load_game() call reads it back. If the file does not exist (first
run) or is corrupt, the defaults are returned without crashing.
Config file
Configuration values — volume, difficulty multiplier, key bindings, window
size — do not belong in code. Putting them in a separate config.json means
a level designer or player can tweak them without touching Python.
CONFIG_FILE = Path("config.json")
DEFAULT_CONFIG = {
"volume": 0.7,
"difficulty": "normal", # "easy", "normal", "hard"
"fullscreen": False,
}
def load_config():
if not CONFIG_FILE.exists():
return dict(DEFAULT_CONFIG)
try:
cfg = json.loads(CONFIG_FILE.read_text())
for key, value in DEFAULT_CONFIG.items():
cfg.setdefault(key, value)
return cfg
except (json.JSONDecodeError, KeyError):
return dict(DEFAULT_CONFIG)
def save_config(cfg):
CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
# At game startup:
config = load_config()
VOLUME = config["volume"]
DIFFICULTY = config["difficulty"]
# When the player changes volume in the settings screen:
config["volume"] = 0.5
save_config(config)Using the values in a pygame game
Call load_game() and load_config() before your main loop starts. Call
save_game() when the player quits (in the pygame.QUIT handler) and
optionally on reaching each checkpoint.
# At startup
config = load_config()
state = load_game()
# In the QUIT handler (after the main loop):
save_game(
score = current_score,
unlocked = unlocked_levels,
player_x = player.rect.x,
player_y = player.rect.y,
)The setdefault pattern when loading ensures forward compatibility: if you
add a new key to DEFAULT_SAVE in a future version, existing save files
get the default for the new key rather than crashing with a KeyError.
Where to go next
Next: the Polish and Persistence lab — extend the tile platformer with jump and coin sound effects, looping background music, save-on-quit, and a settings scene.
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.
Lab: Polish and persistence
Extend the tile platformer with jump and coin sound effects, looping background music, save-on-quit / load-on-start, and a settings scene for volume control.