pulumi/pulumi · error · ValueError
module object for {original_name!r} substituted in sys.modul
Error message
module object for {original_name!r} substituted in sys.modules during a lazy load What it means
Pulumi's `_LazyLoader` (used by `lazy_import`) defers module execution until first attribute access, then verifies the freshly executed module is still the object registered in `sys.modules`. This ValueError fires when, during that lazy load, a different module object was substituted under the same name in `sys.modules` — meaning some hook or custom loader replaced the module, which would leave references inconsistent.
Source
Thrown at sdk/python/lib/pulumi/_utils.py:323
original_name = self.__spec__.name
# Figure out exactly what attributes were mutated between the creation
# of the module and now.
attrs_then = self.__spec__.loader_state["__dict__"]
attrs_now = self.__dict__
attrs_updated = {}
for key, value in attrs_now.items():
# Code that set the attribute may have kept a reference to the
# assigned object, making identity more important than equality.
if key not in attrs_then:
attrs_updated[key] = value
elif id(attrs_now[key]) != id(attrs_then[key]):
attrs_updated[key] = value
self.__spec__.loader.exec_module(self)
# If exec_module() was used directly there is no guarantee the module
# object was put into sys.modules.
if original_name in sys.modules:
if id(self) != id(sys.modules[original_name]):
raise ValueError(
f"module object for {original_name!r} "
"substituted in sys.modules during a lazy "
"load"
)
# Update after loading since that's what would happen in an eager
# loading situation.
self.__dict__.update(attrs_updated)
return getattr(self, attr)
def __delattr__(self, attr):
"""Trigger the load and then perform the deletion."""
# To trigger the load and raise an exception if the attribute
# doesn't exist.
self.__getattribute__(attr)
delattr(self, attr)
class _LazyLoader(importlib.abc.Loader):View on GitHub (pinned to 793f7b2e16)
Solutions
- Remove any code that assigns a different object to `sys.modules[<name>]` during import.
- Access the module normally (`import module`) instead of through `lazy_import` when mocking is involved.
- In tests, mock attributes on the real module after import rather than replacing the module object in sys.modules.
- Ensure no custom meta_path finder/loader re-registers the module during exec.
Example fix
// before # in module under lazy import import sys sys.modules[__name__] = MyShim() // after # in module under lazy import # no sys.modules reassignment; expose shim as an attribute shim = MyShim()
Defensive patterns
Strategy: try-catch
Validate before calling
# before lazy load, ensure nothing will replace the module assert original_name not in sys.modules or id(sys.modules[original_name]) == id(expected_module)
Try / catch
try:
mod = lazy_import("mymodule")
mod.attr
except ValueError as e:
if "substituted in sys.modules" in str(e):
import importlib
mod = importlib.import_module("mymodule")
else:
raise Prevention
- Never assign to sys.modules[__name__] inside imported modules.
- Mock attributes, not module objects, in tests.
- Avoid combining custom meta-path importers with lazy_import.
When it happens
Trigger: During a lazy load, `exec_module()` (or module code itself) installs a new module object into `sys.modules[original_name]` — e.g. the module re-imports itself, a test double replaces it, or a custom importer hooks `sys.modules` writes.
Common situations: Test suites injecting mocks into sys.modules while lazy imports are pending; modules that call `sys.modules[__name__] = something` at import time; circular imports combined with lazy_import; custom meta-path importers fighting the lazy loader.
Related errors
- loader must define exec_module()
- failed to save policy project at %s: %w
- spec must be of the form name=URN
- expected a URN but got a Provider Reference, use '%s' instea
- validating stack config: %w
AI-assisted analysis of pulumi/pulumi@793f7b2e16 (2026-08-31).
Data as JSON: /api/errors/527cfd04e938e1e0.
Report an issue: GitHub.