Skip to content

gh-155695: Remove resolved names from sys.lazy_modules consistently - #157714

Open
brittanyrey wants to merge 5 commits into
python:mainfrom
brittanyrey:b-lazy-modules-stale-entries
Open

brittanyrey wants to merge 5 commits into
python:mainfrom
brittanyrey:b-lazy-modules-stale-entries

Conversation

@brittanyrey

@brittanyrey brittanyrey commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

summary

Improve sys.lazy_modules to address the following issues:

  • A lazy import of a module that has already been loaded does not get added to sys.lazy_modules .
  • The "pkg.attr" entry for lazy from pkg import attr is cleaned up after reification.

perf

End-to-end (hyperfine, --warmup 50, 1000 runs; 400 for reify-only)

scenario base patched control patched/base control/base
bare startup (~0 lazy stmts) 12.1 ± 0.6 ms 12.0 ± 1.0 ms 12.1 ± 0.7 ms 0.99× 1.00×
synthetic: 243k lazy stmts 43.9 ± 3.0 ms 46.4 ± 2.9 ms 43.7 ± 2.8 ms 1.06× 1.00×
synthetic: 162k reifications 223.7 ± 11.8 ms 228.1 ± 10.7 ms 225.1 ± 11.9 ms 1.02× 1.01×
realistic -X lazy_imports=all app 68.0 ± 3.2 ms 68.3 ± 3.1 ms 67.4 ± 2.4 ms 1.00× 0.99×

@pablogsal pablogsal left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one tracking regression when a module fails during initialization, see the inline comment.

Comment thread Python/import.c Outdated
@bedevere-app

bedevere-app Bot commented Sep 18, 2026

Copy link
Copy Markdown

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@brittanyrey

Copy link
Copy Markdown
Contributor Author

@pablogsal Addressed comments.

@brettcannon
brettcannon removed their request for review September 21, 2026 21:03
@brittanyrey

Copy link
Copy Markdown
Contributor Author

cc @encukou

Comment thread Python/import.c Outdated
@pablogsal

Copy link
Copy Markdown
Member

Thanks! The initializing-module case is covered now. I left one follow-up comment about the new __spec__ lookup.

Names only left sys.lazy_modules through _imp._set_lazy_attributes(), which
the import machinery calls from _find_and_load_unlocked(). Two cases never
reached it, so their names were recorded and then kept forever:

- A lazy import of a module already in sys.modules. _find_and_load() returns
  early, so nothing ever discards the name. Do not record it in the first
  place.

- The "pkg.attr" entry for `lazy from pkg import attr`. The import machinery
  only discards module names, and attr is often not a module. Discard it when
  the lazy object is reified, where the name is already known and has been
  resolved either way.

Submodules that are not yet loaded are still tracked: loading a package does
not load its submodules, so those imports can still fire. Names whose
reification failed also stay tracked, since the import can still happen.
A module is in sys.modules while its body runs, so lazy_modules_add()
counted it as loaded and skipped recording the name. If the body then
raises, the module is removed from sys.modules again and the lazy
import is left pending under no name at all.

Check __spec__._initializing so such a module does not count as loaded.
A name added while a module initializes is still discarded once the
import completes, since _set_lazy_attributes() runs after the body.
@pablogsal pablogsal added the needs backport to 3.15 pre-release feature fixes, bugs and security fixes label Sep 26, 2026
@pablogsal
pablogsal force-pushed the b-lazy-modules-stale-entries branch from 374d85f to 44ebbaa Compare September 26, 2026 21:39
@pablogsal
pablogsal enabled auto-merge (squash) September 26, 2026 21:39
@pablogsal

Copy link
Copy Markdown
Member

I found a couple of issues and pushed fixes. Checking the spec could call an _initializing property or a custom __bool__ during a lazy import declaration. We now read the stored flag directly without calling user code.

Importing an already loaded attribute could also leave a stale entry in sys.lazy_modules, since Python returns the cached value without going through the lazy resolution code. We now skip adding the entry when the attribute is already stored on the module.

@pablogsal
pablogsal disabled auto-merge September 26, 2026 22:15
@pablogsal
pablogsal enabled auto-merge (squash) September 26, 2026 22:21
@pablogsal
pablogsal disabled auto-merge September 26, 2026 22:21
Read stored initialization flags without invoking spec callbacks or materializing dictionaries. Keep unsupported spec state conservatively tracked. Skip cached exports without changing their import context or clearing independently pending modules.
@encukou

encukou commented Sep 29, 2026

Copy link
Copy Markdown
Member

I spent a bunch of time reading the code, trying to understand the internal invariants.

@pablogsal, your edit suggests that lazy import shouldn't run user code. Is that right, or is constraint different?
Today, lazy import can run user code; in fact I found that this causes another (minor) bug: #158402


The Unsupported spec representations stay conservatively tracked part worries me a bit. This PR should be titled “Remove resolved names from sys.lazy_modules more consistently”. We can't really remove the caveats from the docs; in fact the “consumers [of lazy_modules] are expected to verify each entry’s status” advice continues to apply. I worry a bit that if we fix the usual cases in 3.15, consumers won't learn do that, and the bugs won't surface until they're combined with quirky custom importers.

The magic needed in lazy_modules_add worries me, too. If _initializing needs this kind of access, would it be better as a field in the PyModuleObject struct, rather than a Python attribute on the (possibly duck-typed) spec?

@pablogsal

Copy link
Copy Markdown
Member

lazy import shouldn't run user code.

The point of the direct lookup was to keep the tracking change from adding calls to user code. Reading a spec normally can invoke a property or __bool__, so adding that check can make a previously successful declaration fail. I wanted to improve the diagnostic set without changing when those callbacks run. The filter and import hooks can still run Python.

There’s a cleaner way to handle this, though. I’d suggest building on #158282 and removing the cached lazy from shortcut. That shortcut means an attribute can be captured at declaration time if its module happens to be loaded already, whereas otherwise it’s looked up on first use. Always creating the placeholder would give us consistent timing and match the behavior described in PEP 810. It also removes the declaration-time spec lookup behind #158402.

consumers [of lazy_modules] are expected to verify each entry’s status

Agreed, that caveat should stay. I’d make the bookkeeping follow the operations we actually perform: add names when declaring the lazy import and remove them when resolution succeeds. For a from-import, successful module import and successful attribute lookup are separate steps. If the module imports but the attribute lookup fails, we can remove the module entry while keeping the attribute entry.

This still isn’t a list of every unresolved binding. Multiple declarations share one name, and resolving one can remove that name while other placeholders remain. A previously loaded module can also be listed after a new, unused lazy declaration. The docs should explain those cases.

The magic needed in lazy_modules_add worries me, too.

The reason for checking initialization was that a module can be in sys.modules and still fail during import. Skipping its entry merely because it exists would lose a pending lazy import.

With cleanup at resolution, we don’t need to predict that outcome when recording the declaration. #158282 already brings the resolution code together, so I’d use that structure and drop the declaration-time filtering. Moving initialization state onto the module may still be useful elsewhere, but we wouldn’t need that change to maintain this set.

@encukou

encukou commented Sep 29, 2026

Copy link
Copy Markdown
Member

So, do we leave this as is for 3.15, and go for a refactor in 3.16?

@pablogsal

pablogsal commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

No, the refactor should land in 3.15 otherwise it's going to be hell to keep both versions and backport any fixes.

@pablogsal

Copy link
Copy Markdown
Member

@hugovk I think this should mark as a release blocked and block 3.15 on landing the other commit as we are still on time to avoid quite a painful set of backports

@bedevere-bot

Copy link
Copy Markdown

🤖 New build scheduled with the buildbot fleet by @hugovk for commit ad36b56 🤖

Results will be shown at:

https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F157714%2Fmerge

If you want to schedule another build, you need to add the 🔨 test-with-buildbots label again.

@bedevere-bot bedevere-bot removed the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting merge needs backport to 3.15 pre-release feature fixes, bugs and security fixes release-blocker

Projects

Development

Successfully merging this pull request may close these issues.

5 participants