Skip to content

Lazy from .sub import sub in a package is overwritten when the submodule is imported elsewhere #161265

Open

Description

@Lillecarl

Bug report

Bug description:

A package binds a name to an object from its own submodule of the same name:

shadow/
├─ __init__.py    lazy from shadow.thing import thing
└─ thing.py       thing = "the object"
import shadow
import shadow.thing
print(shadow.thing)

Without lazy this prints the object. With lazy it prints <module 'shadow.thing' ...>, and the package's binding is lost until the package is reloaded.

The same happens with the eager spelling under -X lazy_imports=all, whenever something other than the package's own placeholder imports the submodule first: importlib.import_module("shadow.thing"), an import inside a function, or another module's lazy from shadow.thing import thing once it is used.

Eagerly, the statement in __init__.py imports shadow.thing itself. The import system sets shadow.thing to the submodule, and then the statement binds the object over it. Lazily, the statement only binds a placeholder. The first real import of shadow.thing is the later outside one, and _find_and_load_unlocked (Lib/importlib/_bootstrap.py) then runs setattr(parent_module, child, module) over the placeholder.

lazy_import_replace_child (gh-142349) keeps this binding when the placeholder's own resolution loads the child. This is the other order: an outside import loads the child first, so no placeholder is left to resolve.

test_import.RelativeImportTests.test_rebinding encodes the eager behaviour (package3/__init__.py is from .submodule import submodule). Lazy mode breaks its import-first half.

A real-world case, reproduced on 3.15.0: cryptography's hazmat/backends/openssl/__init__.py does from cryptography.hazmat.backends.openssl.backend import backend, so backend names both a submodule and the object the package exports. cryptography.hazmat.backends.default_backend() imports that submodule inside a function. With cryptography's imports lazy, a call to default_backend() replaces the package's backend with the submodule. The SSH library asyncssh then does from cryptography.hazmat.backends.openssl import backend, gets the submodule, and backend.ed25519_supported() raises AttributeError.

gh-151208 is the converse. There the package binds foo to something unrelated to a.foo, so an explicit import a.foo must overwrite it, and lazy mode fails to. Here the binding comes from the submodule itself and must survive.

CPython versions tested on:

3.15, CPython main branch

Operating systems tested on:

Linux

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    testsTests in the Lib/test dir
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    and removed
    testsTests in the Lib/test dir
    on Nov 3, 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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions