Why can importlib.reload leave parts of a running program still executing the old code?
answer
- Same module object, re-executed
- Only that namespace is rebound
- Copies taken earlier are not updated
- Old instances, new class object
- isinstance quietly starts returning False
basics
~20 simportlib.reload re-executes the source into the same module object, so only attribute lookups through that module see new code. Names copied out by from-imports, existing instances and registered callbacks still reference the old function and class objects, which stay alive.
solid answer
~40 s`importlib.reload(mod)` re-runs the module's source and rebinds names **in that module's namespace**. Anything that took a copy earlier keeps the old object: a `from mod import handler` binding in another module still points at the old function, an object created before the reload still has `__class__` pointing at the old class, and any callback already registered in a dispatch table is the old callable. Two more traps: `isinstance(old_obj, mod.Klass)` is now `False` because there are two distinct class objects, and reload does **not** recurse into submodules or clear names deleted from the source, since the module `__dict__` is updated rather than replaced. Reload is a REPL and dev-server convenience. In production, replace the process — or run the plugin in a subprocess — rather than trying to swap code under live objects.
code
python · 17 linesimport importlib, pathlib, sys, tempfile
d = tempfile.mkdtemp()
sys.path.insert(0, d)
src = pathlib.Path(d, "labparser.py")
src.write_text("VERSION = 1\nclass Parser:\n pass\n")
import labparser
from labparser import VERSION
old = labparser.Parser()
src.write_text("VERSION = 222\nclass Parser:\n pass\n")
importlib.invalidate_caches()
importlib.reload(labparser)
print(VERSION, labparser.VERSION)
print(isinstance(old, labparser.Parser))go deeper
Know that importlib.reload re-runs a module's code and that it is a development convenience, not something you rely on in a running service. Restarting the process is the normal way to pick up changed code.
Explain the mechanism: reload executes the source into the same module dict, so only attributes looked up through the module change. Names copied by from-imports and objects already constructed keep the old function and class objects.
Show that you have debugged this — inconsistent behaviour across paths after a reload, isinstance failing on pre-existing objects, dispatch tables holding old callables, and the bytecode-cache and invalidate_caches traps. Argue for process replacement.
Own the code-replacement strategy: whether the system needs in-process hot reload at all, and if so what isolation boundary makes it safe — a subprocess or separate interpreter with a message interface rather than shared live object references.
### What reload actually does `importlib.reload(module)` requires a module object that is already in `sys.modules`, re-finds its source, and executes it **into the existing module's `__dict__`**. The module object's identity is deliberately preserved, so every other module that holds a reference to the module still sees the updated attributes. That single design decision explains every trap that follows. Executing into the existing dict means the dict is *updated*, not replaced. A name that the old source defined and the new source deleted survives the reload. A module-level cache, registry or counter is re-initialised only if the new source assigns it; if it is built with `CACHE = {}` it is replaced by a fresh empty dict, and if it is guarded with `try: CACHE except NameError:` it is not. ### The stale-reference problem Reloading rebinds `mod.thing`, and nothing else. Consider a clinical-lab result loader that hot-reloads a vendor parser after a config push: - Another module did `from labparser import parse_row`. That statement copied the function object into the importing module's globals. Reload creates a *new* function object and binds it as `labparser.parse_row`; the copy is untouched and keeps running the old code. - A dispatch table built at startup — `HANDLERS["acme"] = labparser.AcmeHandler` — holds the old class. Every row for that vendor still uses it. - Objects constructed before the reload keep `__class__` pointing at the old class object. Their methods are the old methods, and `isinstance(old_row, labparser.AcmeHandler)` is now `False`, because the name refers to a *different* class with the same qualified name. Type-dispatch code and `except SomeError` clauses silently stop matching. - Decorators that registered functions with a framework at import time run again, so the registry can end up holding both the old and the new callable, and which one wins depends on the framework's overwrite semantics. The symptom in a service is exactly what you would expect from that mix: most work uses the new code, some paths use the old code, and a subset of requests behave inconsistently. In a 17-service dependency graph, where several services import the same shared module and each holds its own copies, the result reads like an intermittent timeout or a heisenbug rather than a code-loading fault, and it does not reproduce after a restart — which is the tell. ### Things reload does not do **It does not recurse.** Reloading a package's `__init__` does not reload its submodules; each already sits in `sys.modules` under its own name and is untouched. **It does not fix identity.** Even a full recursive reload would create new class objects, so instances outlive their classes. **It does not always see your edit.** The compiled-bytecode cache keys on source mtime and size. Rewriting a file within the same second to the same length can leave the stale cached bytecode in play; and a *newly created* file may be invisible until `importlib.invalidate_caches()` drops the import machinery's cached directory listings. **It does not undo module-level side effects.** Threads started, sockets opened and signal handlers installed at import time are done again on reload, usually without cleaning up the first set. **It is unreliable for extension modules.** A compiled extension that does not implement multi-phase initialisation cannot meaningfully re-execute; reload may be a no-op or worse. ### The `del sys.modules[name]` variant Deleting the `sys.modules` entry and importing again produces a genuinely fresh module object rather than an updated one. That is *more* dangerous, not less: now two module objects with the same `__name__` coexist. Module-level state such as a connection pool or a registry is duplicated, `isinstance` checks across the boundary fail, and the old module is kept alive by every reference to it. Singleton invariants quietly stop holding. ### What to do instead Reload's honest use is interactive: a REPL session where you are editing a helper, or a development auto-reloader that in practice **restarts the whole process** rather than reloading a module in place — that is why dev servers re-exec. In production, the reliable unit of code replacement is the process. Roll a new process and drain the old one; that is what a deployment already does. If a specific plugin genuinely must be swapped without a restart, isolate it behind a boundary that has no shared object references to keep stale — a subprocess with a message interface, or a separate interpreter — so replacing it means tearing down that boundary rather than reaching into live objects. And if you must reload, design for it: keep no `from` imports of reloadable modules, look attributes up through the module object at call time, and rebuild dispatch tables after every reload.
- After a reload, why does isinstance on an object created beforehand return False?Re-executing the source runs the `class` statement again, producing a brand-new class object bound to the module attribute. The old instance's `__class__` still points at the previous class object, and the two are unrelated despite sharing a qualified name. Type dispatch, `except` clauses on reloaded exception classes and registry lookups all stop matching.
- Is deleting the sys.modules entry and re-importing safer than reload?No — it is riskier. Reload keeps one module object and updates it; deleting the cache entry creates a second module object with the same name while the first stays alive through existing references. Module-level state such as pools, caches and registries is then duplicated, and cross-boundary isinstance checks fail. It is only reasonable in a throwaway test process.
- You edited a file and reloaded, but the old values are still there. What would you check?First that you reloaded the module you think you did, and that the file on `sys.path` is the one you edited rather than a shadowing copy. Then the bytecode cache: it keys on source mtime and size, so a same-second rewrite of identical length can reuse stale bytecode. For files created during the run, call `importlib.invalidate_caches()` before importing.
saying these in an interview costs you the question
- Believes reload updates every existing reference
- Thinks reloading a package reloads its submodules
- Proposes reload as a production hot-deploy mechanism
- Expects isinstance to keep working across a reload
- Suggests deleting the sys.modules entry as the safe fix
- Assumes names deleted from the source disappear on reload