Python 3.15 lazy imports: how to tell if a module has run without running it
Python 3.15's lazy imports defer a module until first use, which makes almost any probe of the module a use. Here is why hasattr() fails and the PEP 810 bookkeeping that answers the question safely.
Python 3.15 lets you write lazy import example and get a name bound without the module running. That raises a testing problem as soon as you try to verify it: how do you prove the module has not executed yet, when almost every way of looking at it makes it execute?
A Stack Overflow question set this up with two files. example.py prints Module executed and sets value = 42. main.py does lazy import example and then asks hasattr(example, 'value'). The probe answers the question by destroying it: the lookup forces the import, so the module's print fires inside the check meant to detect it.
The fix, confirmed in an answer quoting PEP 810, is to stop looking at the module object at all and look at the interpreter's bookkeeping instead: 'example' in sys.modules.
Key takeaways
- A lazily imported module is listed in
sys.lazy_modulesand is absent fromsys.modulesuntil it is used. - Using it, which PEP 810 calls reification, runs the module, removes its name from
sys.lazy_modulesand puts it insys.modules. hasattr(),getattr()and plain attribute access all reify, so none of them can tell you whether the module has run.'example' in sys.modulesis the check that answers the question without forcing the import.
What a lazy import leaves behind
When an import is marked lazy, PEP 810 says Python calls __lazy_import__ instead of __import__. That call adds the module's fully qualified name to sys.lazy_modules, a set of names. What it does not do is create an entry in sys.modules: the PEP is explicit that lazy module objects do not appear there and are only listed in the lazy set.
The lazy object itself, under normal operation, lives in a global variable. The common ways of reaching that variable, ordinary name access and module attribute access, resolve the import and swap the lazy object for the real module. The PEP calls this reification: resolving the import at that point in the program and replacing the placeholder with the concrete module. At that moment the name leaves sys.lazy_modules and the module, now fully executed, sits in sys.modules like any eagerly imported one.
Why hasattr() can never say no
PEP 810 states that accessing a lazy import through normal attribute access or getattr() triggers reification of the accessed attribute. Its own illustration uses the standard library's json:
lazy import json
# json not in sys.modules yet
dumps_func = json.dumps # reifies
dumps_func = getattr(json, 'dumps') # also reifies
# json is now in sys.moduleshasattr() does its work by performing the same kind of attribute lookup as getattr(), which is why the Stack Overflow answer rules it out. There is a second problem underneath: the PEP lists regular variable access among the paths that resolve a lazy import, so merely naming example in an expression counts as using it. A probe built on the module object reifies the module before it can report anything, so it will only ever answer that the module has run.
This is the design working as intended rather than a quirk. Lazy imports promise that the first real use pays the import cost, and a lookup is a real use. The question has to be asked somewhere the lazy object does not live.
The check that works: sys.modules
The Stack Overflow asker had already guessed at 'example' in sys.modules and was unsure whether it was reliable. The answer's verdict: yes, that is the correct way to check whether the module has executed. It never touches the lazy object, only the dictionary of loaded modules keyed by name, so there is nothing for Python to reify. The answer demonstrates the full transition with two assertions:
import sys
lazy import example
# still lazy
assert 'example' in sys.lazy_modules and 'example' not in sys.modules
example.value # attribute access forces reification
# fully reified
assert 'example' not in sys.lazy_modules and 'example' in sys.modulesRead together, the two sets give a three-way answer. Present in sys.lazy_modules means imported lazily and still waiting. Present in sys.modules means executed. In neither means the name was never imported this way at all. For the original question, has the module run yet, sys.modules alone is enough, and the Module executed line stays silent while you ask.
What counts as touching the module
The useful rule from PEP 810 is about where you look, not which function you call. Anything that goes through the variable bound by lazy import, whether a bare attribute, getattr() or hasattr(), is a use and pays the import. Anything that asks sys about the module's name is bookkeeping and leaves it alone.
There is one deliberate exception on the other side of the line. The PEP says calling dir() on a module is special-cased in mod.__dir__ to avoid reification, so listing a module's names does not by itself force the lazy imports it holds.
Limits and open questions
Both sets describe the module name, not each reference to it. PEP 810 says a module is removed from sys.lazy_modules when it is reified even if other unreified lazy references to it still exist. So 'example' in sys.modules tells you the module has executed; it does not tell you that every variable which lazily imported it has had its placeholder swapped for the real object yet.
The PEP also describes the normal behaviour with a hedge: lazy objects should only end up in global variables under normal operation. Code that digs placeholders out through unusual routes sits outside that description, so test any such pattern on your own 3.15 interpreter before building on it. And because the behaviour here is quoted from PEP 810 itself, check the final Python 3.15 documentation for sys.lazy_modules before relying on its exact semantics in production tests.
Does hasattr() trigger a lazy import in Python 3.15?
Yes. It performs an attribute lookup like getattr(), and PEP 810 says attribute access and getattr() reify the lazy module, so the module runs during the check.
Is checking sys.modules safe with lazy imports?
Yes. Lazy module objects are not placed in sys.modules; the module appears there only once it has been reified, and the membership test never touches the lazy object.
What is reification?
PEP 810's term for resolving a lazy import at the point of first use and replacing the lazy object with the real, executed module.
What is sys.lazy_modules?
A set of fully qualified names of modules imported lazily. A name is removed from it when the module is reified.
This article draws on How can I check whether a lazy import has executed in Python 3.15 without triggering it? and an answer to it, on the Stack Exchange network, licensed under CC BY-SA 4.0.
- #PEP 810
- #Python
- #lazy-loading
- #python-import
- #sys.lazy_modules
- #sys.modules



Comments