TL;DR (Edited): I think that lazy imports should probably be allowed in with blocks*, and it’s important that that be in this PEP*, because the main use case is ease of backporting, which is defeated by forbidding it in any release.
First off, this is a great PEP. It’s clearly super well thought-out and it’s going to solve a lot of real problems that people are currently working around. dateutil not only has this thing (which solved the perennial problem of “how come import dateutil; dateutil.tz.tzfile doesn’t work?” without making all the modules eager), but also has had stuff like this (lazy imports between modules) for over 20 years now. It will be very nice to update that stuff to use a native mechanism.
That said, the biggest stumbling block I’m hitting with the specifics of the PEP here is that, as a library maintainer, I don’t see a great way to have a well-scoped backwards-compatible implementation that is lazy both pre-Python 3.15 and post-Python 3.15. The __lazy_modules__ mechanism allows you to opt in to lazy semantics for 3.15, but I already have lazy semantics, and switching to __lazy_modules__ for Python <3.15 would be a regression for my users. I believe @stefanv expressed something similar here.
I can think of two ways to ergonomically accomplish this, but in both cases I would want to use a context manager like this one, so that you can do something like this:
__lazy_modules__ = ["foo", "bar"]
from ._compat import lazy_importer
with lazy_importer(__lazy_modules__):
import foo
from bar import blah
I’m guessing that in Python 3.14 you would implement this with some kind of dirty hack or custom loader (epyutils does it like this, which is less dirty than I expected but also doesn’t look especially thread-safe). With the PEP as it is now, because lazy imports are forbidden in with blocks, you would also have to use a hack in Python 3.15, but I think you could at least base that one on __lazy_import__ instead of __import__, so you would get some gain, but you won’t be able to avoid hacks until you fully drop anything lower than 3.14.
If lazy imports were available in with blocks, however, in Python 3.15+, lazy_importer could just fall back to being contextlib.nullcontext. So my question is this: do we actually need to forbid lazy imports in with blocks?
I think that while it is possible for context managers to do exception handling, the kind of exception handling done there is pretty different from the kind done in try/except blocks: usually you don’t use them to try to handle exceptions so much as to prevent exceptions from interfering with your management of the lifetime of some resource (though contextlib.suppress and presumably warning-suppression context managers are significant counter-examples here).
The most salient points to me:
-
The benefits of forbidding lazy imports in with statements are modest but real, and much lower than the benefits of forbidding them in try/except blocks.
-
I can think of at least one potential advantage to allowing this pattern — backwards compatibility context — but I can’t think of any others.
-
The backwards compatibility use case only works if with statements are allowed from the beginning — if they are forbidden in 3.15 and then allowed in 3.16+, we are still stuck with a hack in 3.15.
-
One advantage of forbidding lazy imports in with blocks is that for people who want to run with -X lazy_imports=enable, you can do something like this to force eager imports:
with eager(): # alias for contextlib.nullcontext
import whatever
Whereas if they are allowed, I think the best you could do is:
try:
import whatever
except Exception:
raise
Though, again, perhapse there is a better way to do this, and also maybe global mode is not something that needs to be driving the semantics here.
At the moment I’m leaning towards recommending that lazy imports be allowed in with blocks, but it’s very much an empirical question, so I’d ask the PEP authors, people who have rolled their own lazy import mechanisms and library maintainers to weigh in on these questions:
- Is this context manager idea even the best way to do a backwards-compatible polyglot lazy import?
- Are there other circumstances you’ve seen where on balance people are going to be bitten by doing lazy imports in
with blocks? (Bonus points if these are real scenarios and not things that just could go wrong)
- Are there other uses for
with blocks that I’m missing that would weigh in favor of allowing lazy imports to occur within them.