importlib.reload in CPython 3.11: keeping existing object state consistent after a zero-downtime module swap
0 reputation · 17 Dec 2024, 02:24 UTC
0 reputation · 17 Dec 2024, 02:24 UTC
I want to use importlib.reload() as a zero-downtime update path for a small pure-Python application running on CPython 3.11, so a new version of a module can take effect without restarting the process.
The documentation confirms that reload() re-executes the module code and replaces the entry in sys.modules, but objects already instantiated from classes in that module keep referencing the old class definition. My application holds long-lived instances (service objects with internal caches and registered callbacks), so a naive reload would leave the process running two versions of the same class simultaneously. I also register signal handlers at module level, and I am unsure how to prevent duplicate registrations when the module body runs again.
All modules involved are pure Python; no C extensions are in scope.
__class__ or re-instantiating) without losing their internal state?reload() silently leaves stale references that a verification step should check?29775 reputation · 17 Dec 2024, 11:46 UTC
In CPython 3.11 importlib.reload() re‑executes the module body and swaps sys.modules[mod] for a brand‑new module object. Existing instances created from classes defined in that module keep referencing the old class objects, so a naive reload will leave two versions of the same class in memory. Below is a minimal, idiomatic pattern that:
import importlib, sys
# The module we want to hot‑swap
mod_name = 'myservice'
mod = sys.modules[mod_name]
# Keep a reference to the old class before the reload
OldCls = getattr(mod, 'Service')
# Perform the reload
new_mod = importlib.reload(mod)
NewCls = getattr(new_mod, 'Service')
# Migrate every live instance that was created from OldCls
for obj in list(globals().values()):
if isinstance(obj, OldCls):
# Preserve attributes that exist in the old instance
obj.__class__ = NewCls
# If the new class has a different layout (e.g. __slots__), copy missing attrs
for name, value in obj.__dict__.items():
if not hasattr(obj, name):
setattr(obj, name, value)
Key assumptions:
OldCls and NewCls are plain Python classes (no __slots__ or custom metaclass). If __slots__ is used, the migration must explicitly copy slot attributes or avoid __class__ reassignment.Side effects that should only run once (e.g. signal handlers) can be protected by a sentinel attribute in the module:
# In myservice.py
import signal
if not getattr(sys.modules[__name__], '_handlers_registered', False):
def handler(signum, frame):
print('Signal received')
signal.signal(signal.SIGINT, handler)
sys.modules[__name__]._handlers_registered = True
When the module body is re‑executed, the sentinel prevents re‑registration. If the sentinel is missing, reload() will run the body again and you’ll get duplicate handlers.
Reloading does not touch references that other modules or objects hold to the old module object. Common stale spots:
imported before the reload; they still point to the old definitions unless you reload them explicitly.mod.__dict__ snapshots).A conservative approach is to expose a cleanup() function in the module that removes or merges such stale state, and call it after the reload before assigning new classes to instances.
Service, call importlib.reload(), assign __class__ as shown, and assert that instance attributes (including those set in __init__) are still present.signal.getsignal(signal.SIGINT) before and after reload to confirm the handler is registered only once.sys.modules['myservice'] before and after reload to ensure the module object has changed.myservice to verify they still reference the old module; if so, reload those modules explicitly.Does myservice.Service use __slots__ or a custom metaclass? That detail determines whether the simple __class__ reassignment will work or whether you need a more elaborate migration routine.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.