I don’t think the “competing PEPs” model is a good idea. What I want is for there to be one best PEP which has carefully considered the alternatives and has the greatest chance of getting this incredibly useful feature into the language in the best way possible.
I hope you understood that the main purpose of this was to avoid the ugly __lazy_modules__ work-around for older Python. Although syntactically I also prefer one with statement and indentation to repeated lazy on each line as it encourages grouping the lazy imports and not scattering them between other imports.
The problems I have with writing an existing context manager version are that:
Reification doesn’t exist and is necessary to convert the proxy objects cleanly to the real objects
I’m not aware of a safe way to temporarily change the meaning of the import statement.
I wanted to experiment with the __lazy_import__ function to see if it could be used to provide an (unsafe) context manager, but it doesn’t seem to work like I would expect. sys.lazy_modules, mentioned as part of the mechanism also doesn’t appear to exist (yet?).
>>> unittest = __lazy_import__("unittest")
>>> unittest
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
unittest = __lazy_import__("unittest")
ImportError: deferred import of 'unittest' raised an exception during resolution
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<python-input-1>", line 1, in <module>
unittest
TypeError: 'module' object is not callable
Just stopping-by to show some love. Thank you so much for putting this together. As Stéfan said above, we have been wanting to have something like this for years and had our solution working well for a bit for Scientific Python. It is really an invaluable mechanism to have. Seeing a standard solution emerge is huge
@thomas As far as rejecting the “lazy imports find the module without executing it” idea goes, after posting the suggestion, I realised that while the import machinery as a whole separates “finding the module” from “executing the module”, the __import__ interface does NOT expose that separation.
Since PEP 810 is careful to ensure that __import__ still gets executed (just later than it otherwise would), that on its own would be enough to rule out the LazyLoader style semantics. So yeah, while the list of reasons you added for not doing it that way is already compelling, this is yet another reason to reject that approach.
It does highlight a semantic detail though: do lazy imports capture the value of __import__ at the time the statement is executed (presumably on the proxy object), or do they delay looking up __import__ until reification occurs?
(Given the level of mutable state in the import system, I think we have to do the latter, but anyone playing those kinds of games at runtime is pretty squarely in “that’s probably going to break things and you get to keep all the shiny pieces” territory)
Thank you for the PEP, this looks very useful. I’m personally looking forward to using lazy imports with PEP 749.
But to let the bike-shedding begin: You mentioned that you used lazy instead of defer as keyword. I’d ask you to reconsider. defer has several important advantages in my opinion:
The PEP starts with “Lazy imports defer the loading and execution of a module …”. This already hints at the fact that “defer” is the more easily understood term. While “lazy” is often used for deferred execution in programming, it’s a technical term. Why not instead use the technical term that’s also has the same meaning in English?
defer import x and defer from x import y read quite natural, while I personally stumble over lazy import x and especially lazy from x import y.
The PEP looks very well researched and well reasoned (heaps better than anything I’d be able to come up with, that’s for sure), but as someone who’s written sizeable CLIs in Python and has used LazyLoader to guard all of their CLI package’s local imports, I can’t really think of when I’d want to import a module and not want it to be lazy by default (if I should need to reference a symbol at the top level, an eagerly-evaluated lazy import would serve me just as well). Having to consider whether each individual import should be eager or lazy is just an unnecessary cognitive burden in the vast majority of cases, and my CLIs could very easily turn into lazy import ‘soup’.
The spec mentions that sys.set_lazy_imports is primarily intended for testing, but it looks like exactly what I’d want to use in my __main__ block if a more granular switch isn’t made available. Would you consider providing a per-module opt-in with lower precedence than sys.set_lazy_imports which would turn all import statements in a module and its sub-modules into lazy imports?
I realized that I’m not clear on how to inspect a module without triggering reification.
I read the section on globals() and __dict__ access, which I think is really nice and gives me some basic idea. My intuition from that is that globals() is the only way to access module attributes without triggering this mechanism. Is that right?
I’m not sure how practical this question is. I don’t have a use case which needs it. I started from idly wondering how hasattr should behave and thinking a little about special use cases like sphinx autodoc.
The vast majority of imports in Python are used to import functions or types, and not to trigger side-effects. If such usage would always, or almost always, result either in same or better performance, then lazy imports will be the better default.
That might lead to linters, code reviews, and PRs that will just try to switch every import to be a lazy import, and also to people preemptively using lazy imports everywhere, even if it’s not really needed.
Have you thought about approaching this in the other direction? Add a new keyword for imports that are eager, so that people can express their intent - only a very small number of imports have to be eager, so it would IMO be better to let this specific case be explicit, rather than force users to mark all the other cases and hoping that it will improve perf.
Then CPython could:
Add a flag to let people opt-in into making all non-eager-marked imports be lazy.
Trigger warnings for imports not marked with eager that are inside try blocks, and other similar situations, for a few releases, and then in the future flip the switch and make lazy imports the default.
To expand on this point, the -X lazy_imports="disabled" option is in direct contradiction with the motivation of using lazy import to avoid using if TYPE_CHECKING for circular type checking references.
If the response is simply to say “don’t use lazy imports for circular type references,” I foresee that will lead to frustration on both sides. Library authors will (perhaps unintentionally) create circular references, only to later get bug reports from those (presumably) rare users that disable lazy imports.
This is manageable so long as Python 3.14 is supported (since a non-lazy code path is still required), but will cause problems once libraries stop supporting 3.14 and expect lazy imports to work reliably. More generally, lazy circular imports (typing or otherwise) are likely to become relatively common in a few years, at which time the -X lazy_imports="disabled" option will be effectively unusable (since even a single such use in any library will break).
The PEP discusses how lazy imports become potentially lazy during evaluation. Have the PEP authors contemplated an analogous potentially eager state that eagerly reifies lazy imports except for those which reference partially imported modules?
Thanks for your comment. I see where you’re coming from and thanks for raising this. This is a deliberate design choice in the PEP, and I want to be clear about why we made it this way.
The -X lazy_imports flag both for enable and disable is an advanced feature for users who explicitly choose to disable lazy imports and accept the consequences (see also my previous comment regarding the flag). If a library relies on lazy imports for correctness (including circular type references), users who enable the disable flag own that compatibility issue: this is the expected and documented behavior.
I understand your point about circular references. I recognize that options may vary but our stance is still that circular imports remain a code smell and poor design practice. The PEP doesn’t encourage them: we still recommend refactoring to avoid them. However, if library authors choose to use lazy imports to work around circular references (particularly for typing), we don’t prevent it. The trade-off is that this makes life slightly harder for users who want to globally disable lazy imports. But that’s acceptable: disabling is an advanced feature that requires thoughtful use of the filter mechanism anyway. Users operating at that level can handle the additional complexity of excluding specific modules.
We’ve extensively researched what minimal set of controls serves real community needs. Many users need both the enabled and disabled modes for their specific use cases. These are advanced features designed to work with the filter mechanism, giving different users the flexibility they need. We believe this is a strength of the design and we want to keep it.
We appreciate you all thinking through these implications carefully. Please trust us: we’ve explored the design space extensively with input from multiple user bases and organizations, and we’re confident the current approach (explicit syntax, optional global modes, and filtering) strikes the right balance for real-world needs. The trade-offs are acceptable, and we’re moving forward with this design. Thanks for your understanding.
Thank you all so much for the incredibly thoughtful engagement with PEP 810 We’re truly humbled by the positive reception and the quality of feedback we’ve received. We are all super excited as you are and we look forward to have the best PEP possible.
We want you to know that we’re working hard to review every comment and consideration raised in this discussion. Please understand that we may not be able to respond directly to every comment, but we promise we are reading and considering all feedback carefully. While many of you feel strongly about specific aspects (and rightfully so!), we’re in the challenging position of balancing the entire feature set, CPython’s long-term maintainability, implementation constraints, and the need to build safely and incrementally. This is a very complicated and delicate task.
This is an exciting feature, but we’re committed to getting it right. Our strategy is to target the smallest stable core that we can confidently ship and build upon in future releases. This means some potentially interesting suggestions will need to wait for subsequent enhancements. Not because they lack merit, but because we need to establish a solid foundation first.
We appreciate your understanding when our responses seem firm on certain design choices. There are seven authors on this PEP, each bringing different deep technical expertise and years of experience with Python internals, large-scale deployments, and language design. We’ve been working incredibly hard together to explore every angle of this feature. If we hold a position strongly, it’s because we’ve collectively explored the alternatives extensively and reached a considered conclusion. We’re making difficult trade-offs based on deep investigation of the design space.
Please read through the existing comments and our responses before posting. We’ve already addressed several topics in detail (keyword placement, circular imports, the disable flag, etc.). The more comments we receive covering the same ground, the harder it becomes for our team to identify and respond to genuinely new perspectives. The volume can quickly become overwhelming, making it difficult to give proper attention to new concerns. If you have a new angle on a previously discussed topic, please reference the earlier discussion and explain what’s different about your perspective. Please also respect when we’ve indicated that we’ve decided to close a particular avenue of discussion after thorough consideration.
Thank you for helping us shape this feature. Your input is invaluable, and we’re grateful for this collaborative process.
Any proposals that assume that lazy imports will ever become the default fall under the general umbrella of “Rejected when PEP 690 was rejected”:
Since defaulting to eager imports isn’t going to change at this late stage in Python’s history, it’s the non-default lazy imports that get the more verbose syntax.
I’m very excited to see a new proposal for lazy imports! I have a 10-year old codebase at work (200,000+ lines of code) that is starting to see noticeable problems in startup time due to import overhead.
I’m glad you’ve found a solution that avoids modifying the dict class itself.
I was confused at first about why globals() and module.__dict__ had different reification behaviors, but I think you explained why well at the end of the PEP
The strong focus on local behavioral changes, avoiding cascading & global effects, I think is great and probably a must for backward compatibility.
The only aspect of the PEP that gave me pause was the placement of “lazy” in this syntax: “lazy from X import Y”. (Sorry, bikeshedding incoming:) Both (1) “from X lazy import Y” and (2) “from X import lazy Y” would be a more intuitive choice for me. I realize that (1) is ambiguous from the Rejected Ideas section, which is unfortunate.
Other ideas:
# PEP status quo
lazy import X
lazy from X import Y # 🤨
# Squished keywords
lazyimport X
from X lazyimport Y
# Squished keywords, reversed
importlazy X
from X importlazy Y
# Reversed keywords
import lazy X
from X import lazy Y
I personally like all of the “reversed” variations above because then my visual parsing of “import …” and “from …” continues to correctly identify imports, lazy or not.
The “squished” variations above avoid all grammar ambiguities, which is a plus IMHO.
I was only able to read the first 20 replies to this thread so my apologies if I’ve rehashed any points brought up later in the thread.
(Edited) Thanks for your comment. We have listened to people raising this point and we have still decided to keep it as rejected as it’s covered in the PEP:
While we appreciate the comment and we know is hard and is a lot of work to go through the whole thing, please try to read the full document and the discussion before commenting as you may be reopening an already closed topic or just proposing the same thing again, and this will make the discussion much more chaotic and harder to follow for us. Thanks for your understanding!
This looks like a great addition to python language. I am wondering if there is maybe a more natural way to implement and integrate it because, as it is described, it feels a bit hacky.
In the “syntax restriction” section there are many situations to check. I feel like there is some burden to implement all these. On top of this, how the 0x01 flag in IMPORT will be processed? Are the same checks going to be present at the bytecode level? Looking at _PyImport_LoadLazyImportTstate in the branch, I cannot really say whether it is the case.
The performance implications of implementing this magic in LOAD_GLOBAL and LOAD_NAME are indeed concerning (as also mentioned in the PEP). How would the worst case of performance degradation look like for the code that does not use the lazy import feature?
I think there is a misunderstanding here as there is no negative performance impact either if you use the feature of if you don’t. The whole thing is designed with that in mind (not only we are 3 core devs in the proposal but we have discussed this approach in the 2025 Python core dev sprint extensively with a lot of other core devs). It’s covered in the PEP: PEP 810 – Explicit lazy imports | peps.python.org
He did acknowledge that later in the paragraph…if you’re gonna ask him to read the whole thread it’s only fair that you have to read his whole post.
My impression (and personal opinion) is that from X lazy import Y is widely preferred and I’ll echo the comment from @Nineteendo that this doesn’t seem like such a breaking change. I think it’s worth considering if there’s a way to make it work.
Perhaps a transition plan could be: 3.15 allows from X lazy import Y only if X is not .. Simultaneously, it introduces a SyntaxWarning for the extra whitespace in from . module import. Eventually the whitespace is no longer valid and from . lazy import is allowed.
I would be surprised if from . module (with the extra space) is very common, it looks like a typo.
Thanks for pointing this out. I indeed read the comment in full but in my previous comment (PEP 810: Explicit lazy imports - #93 by pablogsal) I asked to please respect when we’ve indicated that we’ve decided to close a particular avenue of discussion and I wanted to point out that we already have indicated our position in the PEP. I can see how the response may have been a bit direct and is not conveying this nuance and for that I apologize. Thanks for calling me out on that!
We understand the middle placement feels more natural and that was indeed our first option before we realized it was backwards incompatible. We genuinely appreciate you taking the time to propose a transition path. Our position is that If the Steering Council wants to grant an exception for this rule or wants this version so much that wants a deprecation period when reviewing the PEP, we’ll certainly go with that. But for now, our proposal maintains 100% backwards compatibility with the syntax as designed (we are 3 core devs in the author list and we really care about backwards compatibility!).
Thanks for your understanding and for helping us make this PEP better through your engagement!
This is a very welcome proposal! It looks quite useful in a lot of cases.
General questions:
Is there a preferred way to inspect whether a name is reified (at runtime, not in a debugger)? I think the PEP kind of implies something like checking whether the module is in sys.modules or the type of globals()[“something”]. Would there be value in a specialized way (a builtin, or a language feature) to check this right now or is it better deferred until later?
Was this designed with more laziness in mind beyond just lazy imports? How does this interact with possible future plans for e.g. lazy assignments? I’m thinking something like top-level assignments that pre-compute something or load data from somewhere, although I guess with this PEP these could be moved out to a lazily-imported module. Additionally, anything that is not top-level.
I can imagine situations where it would be nice to prevent or delay reification. One example:
lazy from my_module import impl1, impl2
if some condition:
impl = impl1
else:
impl = impl2
Is this possible right now? My guess is it would be, although not sure if desirable. What is probably not feasible right now is to prevent reification in bindings that are not top-level, e.g.:
lazy from my_module import lazy_little_thing
def foo(value=lazy_little_thing): ...
# or
class Foo:
x = lazy_little_thing
+1, this is fantastic! I’m genuinely excited about this PEP.
I’ve struggled with startup performance for years in CLIs and data processing tools that import heavy libraries conditionally. The current options are all bad
Also I was not aware of PEP 690. I have reviewed the document and the thread and I have to say that this solution is so much more elegant! Making laziness explicit rather than implicit is exactly the right call. Although not a fan of lazy (I prefer defer or something else as someone pointed out), the lazy import keyword makes intent immediately clear to both humans and tools what’s going on.
Thanks for sharing this PEP. Cannot wait to use this!