Save Game Incompatibility in modified Python class structures
0 reputation · 31 Jan 2025, 01:27 UTC
0 reputation · 31 Jan 2025, 01:27 UTC
Ren'Py employs a modified pickle-like mechanism to serialize Python objects for save games and persistent data. This system is designed to maintain the state of complex variables across different game sessions by storing the object's attributes.
A conflict occurs when a developer modifies the structure of a custom class—such as adding, removing, or renaming attributes—after a user has already created a save file. Because the engine attempts to reconstruct the object from the serialized data, the discrepancy between the saved state and the current class definition can trigger incompatibility issues during the load process.
The goal is to determine the engine's native behavior for reconciling these differences without manually purging user save data.
32775 reputation · 31 Jan 2025, 12:58 UTC
Ren'Py does not provide a native declarative mechanism to define default values for attributes that are missing from a loaded save. When a save is loaded, the engine reconstructs the object by restoring its instance dictionary (__dict__) from the serialized state; it does not call __init__. Therefore, if you added a new attribute after the save was created, the loaded object will lack it, and accessing it will raise AttributeError unless you patch it manually.
Conversely, if you removed an attribute from the class definition, the loaded object will still carry the old attribute in memory. Ren'Py does not purge stale keys during deserialization. That stale data remains accessible and will be written back on the next save.
The underlying cause is that Ren'Py's save system extends Python's pickle protocol, which serializes instance state rather than class structure. Because __init__ is bypassed, any defaults or initialization logic you placed there are not applied to unpickled objects. This is well-established Python behavior that Ren'Py inherits.
What is confirmed by the engine's design: missing attributes are not backfilled, and removed attributes are not stripped. There is no built-in schema-migration layer that reconciles class changes automatically.
after_load hook or a wrapper around your load logic, before gameplay resumes.__dict__.def migrate_player(p):
if not hasattr(p, "new_attribute"):
p.new_attribute = 0
if hasattr(p, "removed_attribute"):
del p.removed_attribute
label after_load:
python:
migrate_player(player)
return
This behavior has been consistent across modern Ren'Py 7.x and 8.x releases, but exact hook names and console commands should be checked against your project's version. Because serializer internals can evolve, treat the migration pattern above as a conservative baseline and test with a save created before your class change.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.