Skip to content

gh-157614: Fix lazy import a.b as c incorrectly binding a.b.b - #157626

Open
grgalex wants to merge 2 commits into
python:mainfrom
grgalex:fix-issue-157614
Open

grgalex wants to merge 2 commits into
python:mainfrom
grgalex:fix-issue-157614

Conversation

@grgalex

@grgalex grgalex commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

lazy import a.b as c binds c to a.b.b instead of the module a.b when a.b is already in sys.modules and has an attribute named b.

Root cause

import a.b as c compiles to IMPORT_NAME a.b followed by IMPORT_FROM b. For a lazy import, IMPORT_NAME returns a placeholder with lz_from = "a.b".

_PyEval_LazyImportFrom() has a fast path for modules already in sys.modules. It looks up lz_from directly and gets name from that module. In this case that means looking up b on a.b, producing a.b.b, instead of looking up b on the base module a.

Fix

Compute the base module before the sys.modules fast path and perform the lookup against it.

The fast path must also be limited to the last IMPORT_FROM identifiable from the placeholder. Longer dotted names compile to multiple IMPORT_FROM instructions, and resolving an intermediate step would make the rest of the import eager. Eight existing test_lazy_import tests cover this.

Two-component imports and from imports remain eligible for the fast path.

Test

Adds MixedLazyEagerImportTests.test_eager_dotted_import_before_lazy_resolves_to_same_module() alongside the existing single-name test.

It creates a package a with a/b.py defining an attribute b, then checks:

import a.b as c
lazy import a.b as lazy_c

assert lazy_c is c

The test fails on main because lazy_c resolves to a.b.b and passes with the fix. test_lazy_import, test_import, and test_importlib also pass.

Commits

  1. Move the lazy import subprocess helpers to LazyImportTestCase so the regression test can reuse them. No behavior change.
  2. Fix _PyEval_LazyImportFrom(), with the regression test and NEWS entry.

Future Work / Not Addressed

If package a itself has an attribute b shadowing submodule a.b, the lazy import can still reify to that attribute rather than the submodule. This is a separate mechanism, already tracked in gh-151208. The same mechanism makes lazy import math.pi as x bind the float where the eager statement raises; gh-155194 fixed the bare lazy import math.pi form only.

For names with three or more components, an already-loaded module still resolves on first use rather than at the import statement because later IMPORT_FROM steps cannot tell whether they are the last one. Fixing that would require additional compiler or placeholder information and is only an optimization.

Closes gh-157614.

Move _run_subprocess_with_modules() and _assert_subprocess_ok() from
FilterFunctionSignatureTests to LazyImportTestCase, so that any lazy import
test can build a package tree in a temporary directory and run code
against it in a subprocess.

Signed-off-by: Georgios Alexopoulos <grgalex42@gmail.com>
…e module

_PyEval_LazyImportFrom() ran its sys.modules fast path on lz_from, which
for lazy import a.b as c is the whole dotted name a.b rather than the
module a that b has to be taken from.

Changes to _PyEval_LazyImportFrom():

1. Compute the base module before the fast path instead of after it, and
   look the attribute up on the base.  The base is lz_from up to the
   first dot when lz_attr is NULL, lz_from + "." + lz_attr when lz_attr
   is a str, and lz_from itself for a from-import.

2. Add last_step and skip the fast path when it is false.  It is true
   for a from-import and for a placeholder without lz_attr whose name has
   a single dot, and false when lz_attr is a str, that is on the later
   steps of a name with three or more components.  Resolving one of those
   eagerly would hand a real module to the next IMPORT_FROM, which then
   imports the rest of the name right away instead of deferring it.

3. Hold the base in one owned reference, from, released on every exit,
   so the three _PyLazyImport_New() call sites collapse into one.

Add a test that imports a.b, where a/b.py defines an attribute b, and
checks that lazy import a.b as x binds the module.

Signed-off-by: Georgios Alexopoulos <grgalex42@gmail.com>
@python-cla-bot

python-cla-bot Bot commented Sep 16, 2026

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@grgalex grgalex changed the title gh-157614: Fix lazy import a.b as c incorrectly binding a.b.b instead of the module gh-157614: Fix lazy import a.b as c incorrectly binding a.b.b Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lazy import a.b as c incorrectly binds to a.b.b

1 participant