By the time someone asks for the __dict__, we’ve already done most of the legwork of running the module’s top-level code and populating its global namespace with classes, top-level functions, eager imports, lazy imports that have been successfully reified, etc. If reification fails for a few lazy items, can we just exclude them from the __dict__?
Hm. This is a very compelling argument. I do think we need to reconsider implicit reification for anything that isn’t getting a specific name. So we’d keep reification for LOAD_GLOBAL and such, and also module.attr, but not module.__dict__ or globals() or other forms of inspecting modules/namespaces. That does make it slightly more likely for lazy objects to leak out and surprise code that wasn’t expecting them, but the fix isn’t particularly difficult: don’t use lazy imports, fixing the code doing the inspection, or reify at any point before that code is called. I think that gives us enough ways to sensibly work around dependencies that can’t deal with
lazy imports yet.
Thinking through how we got to the proposal we made in the PEP, I think we got here because we relied too much on our practical experience with lazy imports, all of which is of course with the old lazy imports proposal (as mentioned before, Meta uses it extensively internally, as do some of the other folks who worked on this PEP). I think perhaps we’re working too hard to hide the new feature, instead of accepting that some old code won’t work well with the new feature and it’s on the programmers to make that consideration – while still giving them enough tools to be able to make things work.
This is where the rejection of with and try/except with lazy imports comes from as well. I think on balance, purely looking at uses of the new syntax, try/except is still too much of a footgun to allow lazy imports in. For with, I’m personally still lightly opposed to it, but I think the other PEP authors think we should allow it. The theoretical utility does seem to outweigh the risk of people writing code like:
with MyGlobalLock:
lazy import extension_that_requires_global_lock
… although it would be nice if this theoretical utility actually works out in practice ![]()
The externally locked example does make me wonder if we should offer an eager import context manager in importlib.util that temporarily maps __lazy_import__ to __import__.
A module may contain a
__lazy_modules__attribute, which is a sequence of fully qualified module names (strings) to make potentially lazy (as if thelazykeyword was used).
Does __lazy_imports__ have to be a sequence?
The most straightforward way of reifying a module would be to “for real” import it, which could mean removing it.
The current proof of concept supports using a set, but I don’t know if that is part of the format specification.
from typing import Sequence
import sys
print(isinstance(set,Sequence)) # False
__lazy_modules__ = set(("requests","json"))
import json
print('json' in sys.modules) # False
__lazy_modules__.discard("json")
import json
print('json' in sys.modules) # True
Unless I misunderstand you, you don’t need a special context manager for that, you can just eagerly import it. The global flag situation is different but I think the plan with the global flag is that if you are going to change the behavior of all your libraries it is on you to make sure stuff doesn’t break (using filters or defensive coding or whatever).
Explicitly overriding the global flag is what I had in mind. Being able to say “this import absolutely must be complete after the statement has executed” allows for usage specific overrides that may reduce the need for name based filtering.
I think if such a thing is not provided and lazy modules start to be used, patterns for this will emerge. For example import foo; foo.
Not sure if this is a good or bad thing, but I am certain that this will emerge for some libraries that get reports that it would be nice to support the globally lazy option. (No matter what’s inside the PEP or documentation, such reports are going to come, and some authors will not want to shut them down)
Yeah, we should acknowledge that a vast majority of modules can be lazily imported, that for many code maintainers lazy imports are going to be the norm rather than the exception, and they will want to enable global lazy imports while being able to explicitly specify which select few imports are eager (usually those that install hooks as a side effect).
While a pattern like import foo; foo will work just fine, a dedicated syntax such as eager import foo or not lazy import foo would help express the intent much better.
I think this is going to be a matter of debate for a long time. As someone who has worked with Python for many years, I would expect imports to be eager by default. The idea of a statement that does nothing right now but triggers non-trivial work at some arbitrary later point in the program is unusual, and worthy of explicit note. I would personally insist that all lazy imports in programs that I maintain are explicitly marked with the lazy keyword, for maintainability.
I accept that not everyone would agree with my view, but I don’t think that “enabling global lazy imports” should be considered normal practice in the foreseeable future - after all, the rejection of PEP 690 explicitly confirmed the SC’s opposition to that practice.
The global flag is still considered an advanced feature and our experience shows it needs to remain that way. As we mentioned a couple of times in the discussion we don’t want to push the PEP toward a world where everything is lazy by default. The approach here is deliberately cautious and explicit. I know a lot of people think we should steer toward a future where lazy becomes the default and give even more emphasis to control that world, but at this time and for this PEP we strongly believe the main mechanism for laziness should remain the explicit lazy keyword (and related APIs). That makes intent clear, keeps semantics predictable, and avoids breaking code that relies on import-time side effects.
The flag absolutely has its users and it’s important for them (and that’s why we believe it’s crucial to have it) but we don’t want to expose everyone to its trade-offs automatically. We believe this is the right set of defaults: explicit laziness for predictability and safety, with a global control available for advanced users who know what they’re doing. We’ve spent a lot of time discussing this design, exploring alternatives, and weighing real deployment data across organizations using the feature.
So while the global switch will stay, it will remain an opt-in, advanced knob rather than a general setting where the final user that decides to opt-in into it it’s the one responsible for the consequences and the control, not the library developers or anyone else.
We’ve updated the PEP with a lot of clarifications, small renames, and several explicitly documented rejected ideas (all based on the feedback here and from private discussions). Many of the comments in this thread helped us refine the explanations and make the document clearer and more precise so thanks a lot ![]()
After discussing with the team extensively and reading through all the feedback, we’ve made a couple of updates based on what we’ve learned from the conversation:
• Lazy imports inside with blocks
We’ve decided to allow them. There are many legitimate use cases where with is used for managing lifetime or scoping behavior rather than just suppressing exceptions, and the syntax is explicit enough that users know what they’re doing. It fits Python’s “consenting adults” model as with carries broader semantics than just error handling. For the genuinely problematic cases (like contextlib.suppress(ImportError)), we think linters are the right abstraction to catch them rather than hard language restrictions.
• __dict__ and reification
We’ve also pivoted to not automatically reify on __dict__ access. This makes the model cleaner: __dict__ is already a low-level API, and introspection code generally shouldn’t trigger side effects implicitly. It’s a better mental model to ask users to reify explicitly when they need it, rather than forcing them into awkward patterns just to avoid automatic behavior. This decision also came out naturally when we had to make parts of the stdlib reason about this (for example, in the traceback module) as it quickly became clear that automatic reification was cumbersome and not the right thing to do. This also restores nice symmetry with globals(): both now expose the raw namespace view, and both are easy to explain and predict.
Thanks again to everyone who shared thoughts, questions, and concerns regarding this! All this feedback genuinely makes a difference and helps us refine the proposal into something better for everyone.
We will be updating the PEP shortly.
FYI, the updated PEP was sent to the SC today:
It’s probably too late now but I find the security implications section a little contradictory and unmotivated:
There are no known security vulnerabilities introduced by lazy imports. Security-sensitive tools that need to ensure all imports are evaluated eagerly can use sys.set_lazy_imports() with “none” to force eager evaluation, or use sys.set_lazy_imports_filter() for fine-grained control.
How can there be no know security vulnerabilities and something that security sensitive tools need to do? What is a security sensitive tool? A web server?
I would of preferred something like:
Python package installers, and other similarly security sensitive tools, that need to eagerly import to avoid installed packages overriding their import namespace during installation can use
sys.set_lazy_imports()with"none"to force eager evaluation, or usesys.set_lazy_imports_filter()for fine-grained control.
Apologies I hadn’t been keeping a close eye on if the text had been updated.
This was based on a very particular request by someone before in the discussion regarding pip depending on importing something for side-effects to ensure that certain guarantees where being met (which you could argue is not good practice anyway) and we generalized it a bit. In this context a ‘security sensitive tool’ is defined as ‘any tool that for some reason cares about this’. But its true that most tools (even if security sensitive) should not care about this in most cases so we welcome any improvements to the language.
I think it sounds better. Would you like to make a PR? We can discuss the specifics there ![]()
That was me, I’m a maintainer of pip, which is why I’m familiar with the issue.
Yeah, I’ll try and make time to open a PR later today (will be my first time in the PEP repo so exciting!).
Amazing!
Please CC me in the PR and I will ensusure to take a look asap ![]()
This feels a bit rushed. I was waiting for all the ~shouting~ discussion to subside and the design to stabilise before I did a proper analysis. Would you mind waiting a week or two?
I’m sorry you feel this way. After more than a week (which is what the SC recomends at least when submitting a new PEP) and almost 300 comments (which is more than what most PEPs get) and the discussion calming down as you mention, we certainly feel it has been discussed enough for us to feel confident on the design. You can surely disagree and that’s fine of course.
The SC is certainly not going to start reviewing this PEP this week or next week given the current queue, so don’t worry about that.
Edit/Update: I have requested in the submission post to not review this PEP before at least one or two weeks just to be sure Mark has time.
I didn’t mean that the design is rushed. Just the submission to the SC.
I’ll try to get my review done within a week or so.
I assumed you were referring to this but my previous comment still applies.