GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Import of submodule shadowing parent module attribute fails under lazy_imports mode · Issue #151208 · python/cpython

Import of submodule shadowing parent module attribute fails under lazy_imports mode #151208

New issue
Copy link
New issue
Copy link
Open
Open
Import of submodule shadowing parent module attribute fails under lazy_imports mode#151208
Copy link
Labels
interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-lazy-importstype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@jacobtylerwalls
jacobtylerwalls
opened on Jun 9, 2026
Issue body actions

Bug report

Bug description:

For this module structure:

└─ a/
   ├─ __init__.py    # Contains a function foo()
   └─ foo.py

This script passes on 3.14: (edited: removed unnecessary from ... line)

import a.foo
import types
assert isinstance(a.foo, types.ModuleType), type(a.foo)

And fails on 3.15 with lazy imports (python3.15 -X lazy_imports=all script.py):

Traceback (most recent call last):
  File "/Users/jwalls/script.py", line 3, in <module>
    assert isinstance(a.foo, types.ModuleType), type(a.foo)
           ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: <class 'function'>

CPython versions tested on:

3.15

Operating systems tested on:

macOS

Reacted by Avasam

Activity

  1. jacobtylerwalls
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Jun 9, 2026
  2. hugovk
    added
    topic-lazy-imports
    on Jun 9, 2026
  3. harjothkhara commented on Jun 10, 2026

    @harjothkhara
    harjothkhara
    on Jun 10, 2026
    Contributor
    More actions

    Confirmed still reproducing on main (ce916dc). Here's the root cause, in case it's useful.

    The two pieces that interact:

    1. import a.foo under -X lazy_imports=all defers the submodule import and only records "foo" as a pending submodule of "a" — Python/import.c#L4601 → register_lazy_on_parent(), which adds "foo" to lazy_pending_submodules["a"].

    2. The deferred binding is supposed to happen in _Py_module_getattro_impl when a.foo is accessed, but the lookup order shadows it. In Objects/moduleobject.c:

      • the generic dict lookup finds the real foo function that a/__init__.py defined, and a found non-lazy attribute is returned immediately at L1377 (return attr;);
      • try_load_lazy_submodule(m, name) at L1393 is only reached when the attribute is absent, so the pending submodule is never resolved.

    So a same-named attribute set by the package __init__ permanently shadows a pending submodule, and the deferred import a.foo never runs its binding.

    Why eager mode (3.14) doesn't have this: eager import a.foo goes through importlib._bootstrap._find_and_load, which does setattr(parent, child, child_module) unconditionally after importing the child, overwriting the function. Lazy mode moves that bind into getattro, but getattro resolves precedence the other way around.

    The fix looks like it belongs in _Py_module_getattro_impl: when a found attribute is not a lazy placeholder but name is a registered pending submodule, the pending submodule should take precedence (mirroring eager import a.foo, where the explicit import statement rebinds the parent attribute). Before committing to that, though, I wanted to check the intended semantics, since this overlaps the precedence work in gh-144957 and gh-150052:

    • Should an explicit import a.foo always win over an __init__-defined a.foo attribute under lazy mode (matching eager)?
    • Or is the intent to preserve the __init__ attribute, in which case this is arguably the documented divergence and the eager/lazy mismatch is acceptable?

    Happy to put up a PR with a regression test in test_lazy_import once the desired precedence is confirmed.

  4. Avasam
    added a commit that references this issue on Jun 12, 2026

    add ref python/cpython#151208

    1e22413
  5. picnixz
    added
    interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
    on Jun 13, 2026
  6. harjothkhara commented on Jun 16, 2026

    @harjothkhara
    harjothkhara
    on Jun 16, 2026
    Contributor
    More actions

    Following up on my root-cause note above: I prototyped a fix and it turned out to be subtler than the getattro shadowing alone suggests. Sharing the findings so whoever lands this (or I, with a steer on direction) accounts for them, since two of them are easy to miss.

    1. from pkg import name and import pkg.name need opposite precedence, but share state

    register_from_lazy_on_parent (Python/import.c:4422) feeds "pkg.name" into the same lazy_pending_submodules structure as an explicit import pkg.name (register_lazy_on_parent, Python/import.c:4364). But eager semantics differ:

    • import pkg.name rebinds pkg.name to the submodule (submodule wins).
    • from pkg import name only imports the submodule if pkg lacks the attribute — so when pkg/__init__.py defines name, the attribute wins and the submodule is never imported.

    So a naive "a pending submodule outranks an existing __init__ attribute on access" fix regresses the from case: under -X lazy_imports=all, from pkg import collision (with pkg.__init__ defining collision and a collision.py) starts resolving to <module pkg.collision> and force-loads it into sys.modules, where eager yields the __init__ function. Any fix has to distinguish the two import forms (they're indistinguishable in lazy_pending_submodules today).

    2. A getattro-level fix is bypassed by the LOAD_ATTR_MODULE specializer

    This is the structural one. _Py_module_getattro_impl is where the shadowing happens (Objects/moduleobject.c:1377), but LOAD_ATTR_MODULE (Python/bytecodes.c:2951) reads the module dict directly and never calls getattro. Registering an explicit lazy import pkg.sub mutates the pending tables, not pkg's dict, so the specialization is never invalidated:

    # -X lazy_imports=all
    import pkg as pkg
    def get(): return pkg.collision
    for _ in range(1000): get()   # warm LOAD_ATTR_MODULE -> __init__ function
    import pkg.collision
    print(type(get()))            # <class 'function'>  (stale; want module)

    Any fix that lives only in getattro returns the stale __init__ attribute from a warmed call site.

    3. Resolving on access (rather than at the import statement) clobbers later reassignment

    Eager import pkg.sub rebinds pkg.sub once, at the statement; later pkg.sub = x wins. A fix that overrides whenever the name is accessed stays "armed" until the submodule loads, so it also reverts user assignments — and this hits non-colliding submodules too, diverging from both eager and pre-PEP-810 lazy:

    # -X lazy_imports=all
    import pkg.plain               # plain.py exists; no `plain` attr in __init__
    import pkg as pkg
    pkg.plain = "sentinel"
    print(pkg.plain)              # <module ...pkg.plain>  (want 'sentinel')

    Where I think this points

    The rebind seems to need to happen by mutating pkg's dict (so it's visible to LOAD_ATTR_MODULE and overridable by later assignment), not by intercepting getattro reads — most naturally by replacing the colliding attribute with a lazy placeholder that the existing PyLazyImport_CheckExact path already resolves, with the dict-version bump invalidating the specializer. Because pkg may itself be imported lazily when import pkg.sub is registered, that insertion likely has to hook the reification path (_imp__set_lazy_attributes_impl, Python/import.c:5618) for pkg's pending explicit submodules.

    That's enough of a design choice inside PEP 810 that I'd rather not guess at it. Two questions for the feature owners:

    1. Should an explicit lazy import pkg.sub override an __init__-defined pkg.sub by installing a submodule placeholder at reification time (matching eager's statement-time rebind), while from pkg import sub keeps the existing missing-attribute behavior?
    2. Is the LOAD_ATTR_MODULE interaction something you'd want handled by the placeholder/dict-version path above, or is there a preferred invalidation hook?

    Happy to implement whichever direction you prefer — I have the reproducers and a branch with the from/import split already worked out; just want to build on the right foundation rather than a getattro-only patch that the specializer defeats.

  7. brittanyrey
    added 2 commits that reference this issue on Sep 18, 2026

    Import a lazy submodule that a package attribute shadows

    00da645

    Import a lazy submodule that a package attribute shadows

    2b0aa85
  8. jacobtylerwalls commented on Oct 1, 2026

    @jacobtylerwalls
    jacobtylerwalls
    on Oct 1, 2026
    ContributorAuthor
    More actions

    Working now, bisected the fix to dffac61. Have not checked if an additional regression test is needed.

  9. jacobtylerwalls commented on Oct 7, 2026

    @jacobtylerwalls
    jacobtylerwalls
    on Oct 7, 2026
    ContributorAuthor
    More actions

    The original reproducer was partially(*) fixed by dffac61 but is now failing again in 3.15.0rc3 since the backport of 00a1c3a.
    cc/ @hugovk is this interesting for 3.15 final in any way?


    (*) "Partially" means this variation of the original script using as ... still failed even on dffac61:

    import a as pkg
    import a.foo
    import types
    assert isinstance(pkg.foo, types.ModuleType), type(pkg.foo)
  10. hugovk commented on Oct 9, 2026

    @hugovk
    hugovk
    on Oct 9, 2026
    Member
    More actions

    (*) "Partially" means this variation of the original script using as ... still failed even on dffac61:

    A very quick test of this passes on latest main and 3.15, cc @pablogsal @brittanyrey.

    I think this can wait until 3.15.1.

    Reacted by Jacob Walls
  11. pablogsal commented on Oct 9, 2026

    @pablogsal
    pablogsal
    on Oct 9, 2026
    Member
    More actions

    @brittanyrey can you take a look?

    Reacted by Brittany Reynoso
  12. kevinprimeraquecreo-stack
    added this to Project_VKon Oct 18, 2026
  13. github-project-automation
    moved this to Todo in Project_VKon Oct 18, 2026
  14. kevinprimeraquecreo-stack
    added this to Project_VKon Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-lazy-importstype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.