Skip to content

gh-157757: Fix to make lazy import a.b as c import the module a.b - #158092

Open
brittanyrey wants to merge 5 commits into
python:mainfrom
brittanyrey:b-gh157757-lazy-dotted-as
Open

brittanyrey wants to merge 5 commits into
python:mainfrom
brittanyrey:b-gh157757-lazy-dotted-as

Conversation

@brittanyrey

@brittanyrey brittanyrey commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Bug: import a.b as c compiles to IMPORT_NAME a.b followed by IMPORT_FROM b. Lazily, IMPORT_NAME leaves a placeholder holding "a.b", and IMPORT_FROM rewrote it into the placeholder lazy from a import b produces. Reification then imported a alone and read b off it, so the module a.b was never imported under its own name: an attribute of the package shadowing it answered instead, and math.pi, which no module backs, bound the float where the eager statement raises ModuleNotFoundError.

Fix: Follow @pablogsal's suggestion to have each deferred IMPORT_FROM keep the previous placeholder and the attribute name. Reification runs the original import and then applies the recorded lookups in order with _PyEval_ImportFrom, as the eager statement does.

For lazy from a import b, c, the import gets only the name being resolved as the fromlist, so accessing b doesn't import a.c.

`import a.b as c` compiles to `IMPORT_NAME a.b` followed by `IMPORT_FROM b`.
Lazily, IMPORT_NAME leaves a placeholder holding "a.b", and IMPORT_FROM
rewrote it into the placeholder `lazy from a import b` produces.  Reification
then imported `a` alone and read `b` off it, so the module `a.b` was never
imported under its own name: an attribute of the package shadowing it answered
instead, and `math.pi`, which no module backs, bound the float where the eager
statement raises ModuleNotFoundError.

Mark the dotted import on the placeholder and keep the whole name on it.
Reification imports that name and then walks its components with IMPORT_FROM,
which is what the eager statement does.

The test pinning `lazy import math.pi as pi` as working is inverted, since the
eager statement raises.
It passes now that a lazy `import a.b as c` imports the module: the
KeyError on 'test.tracedmodules.testmod' came from the submodule never
being imported under its own name.
The flag means "bind the whole dotted name, not the root", which the old
name did not say, and import.c already has unrelated lazy_pending_submodules
machinery to be confused with.
@brittanyrey brittanyrey changed the title gh-157757: [RFC] Import the module a lazy import a.b as c names gh-157757: Import the module a lazy import a.b as c names Sep 24, 2026
@brittanyrey brittanyrey changed the title gh-157757: Import the module a lazy import a.b as c names gh-157757: Fix to make lazy import a.b as c import the module a.b Sep 24, 2026
@pablogsal

Copy link
Copy Markdown
Member

Hmm, maybe we can have each deferred IMPORT_FROM hold onto the previous placeholder and the attribute name?

For import a.b.c as d, we’d keep the original import of "a.b.c", then record the lookup of b, then c. When resolving it, we could walk back to the original import and apply those lookups in order with _PyEval_ImportFrom. Each step would just add one node, without copying the previous names.

Perhaps we can use the existing fields for this: lz_from could hold either the module name or the previous placeholder, and lz_attr could hold the fromlist or the attribute name. That would also handle a custom __lazy_import__ returning a placeholder for a different module name, since we’d keep the actual lookups instead of reconstructing them from that name.

I prototyped this and it seems to work.

We would keep the intermediate placeholders around until resolution, so it uses more memory. I think it’s worth considering, though. It follows what the bytecode does and avoids needing the extra flag.

Each deferred IMPORT_FROM off a placeholder without a fromlist now keeps
the previous placeholder in lz_from and the attribute name in lz_attr.
Reification walks back to the placeholder IMPORT_NAME left, runs that
import, and replays the lookups in order with _PyEval_ImportFrom, which
is what the eager bytecode does.  This drops the lz_dotted_as flag and
also follows a custom __lazy_import__ that returns a placeholder for a
different module name.
@brittanyrey

Copy link
Copy Markdown
Contributor Author

Thanks @pablogsal! That was very useful context + direction.
I had initially held off from publishing this one since the new lz_dotted_as felt like overkill for a single corner case

`lazy from a import b` now records its lookup the same way as `import
a.b as c`, so a placeholder holds either the module name and fromlist or
the previous placeholder and an attribute name, and reification has a
single path.  The import passes only the name being resolved as the
fromlist, so accessing b still does not import the other names'
submodules.
@brettcannon
brettcannon removed their request for review September 24, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Development

Successfully merging this pull request may close these issues.

3 participants