Skip to content

Restore the deferred imports to module scope - #557

Open
jaraco wants to merge 1 commit into
mainfrom
feature/eager-imports
Open

jaraco wants to merge 1 commit into
mainfrom
feature/eager-imports

Conversation

@jaraco

@jaraco jaraco commented Oct 31, 2026

Copy link
Copy Markdown
Member

Closes #554.

Puts all six function-local imports back at module scope and keeps them deferred by declaring __lazy_modules__ (PEP 810).

     def _load_json(self, filename):
-        # Deferred for performance (python/importlib_metadata#503)
-        import json
-
         return pass_none(json.loads)(

Shape

__lazy_modules__ is only a module global, so it is inert before 3.15 and needs no guard — the declaration reads the same in both trees, and __name__ keeps the entries for this package's own modules identical too. The one entry that has to differ is zipfile, next to the import line that already differs for the zipp overlay; there is a comment on it so the sync step is visible.

Same behavior before 3.15 needs the backport, and it lives behind compat/py314 in the shape compat already uses, so the shared module carries only from .compat.py314 import lazy_importer and with lazy_importer():. Both come out at sync time like from .compat import py311 already does. The one real sync cost is that the imports inside the block are indented and have to be dedented.

_compat/localize.py gets the same treatment, not for its own sake but because it is imported eagerly — leaving _adapters at module scope there would have undone the deferral in __init__.py.

Two things worth knowing

from . import _adapters doesn't work. Per PEP 810 that form names the package as the module to defer, and the package is mid-import, so it defers nothing; on the backport it recurses until the stack is exhausted. Importing the member instead keys the declaration on _adapters and works on both, at the cost of _adapters.Message becoming Message at two call sites. Both declarations carry a note, since the next person to add an entry will hit this.

inspect can't be observed as deferred on 3.12 and 3.13, where importlib.abc — eagerly imported here — reaches it through importlib.resources._common. The declaration is still right; the module is just loaded for other reasons. test_declared_modules_stay_unloaded imports importlib.abc before taking its snapshot so that it measures the declaration and not the stdlib's own graph.

Cost

Per the project's own pytest-perf exercise:

import time
3.11 +0.6 ms
3.12 +1.6 to +2.6 ms
3.13 +0.9 to +1.7 ms
3.14 +0.8 to +1.1 ms
3.15 +0.06 ms (+1%)

Ranges because repeated runs on one machine disagree by that much — the honest figure is 1 to 2 ms before 3.15, and nothing measurable on 3.15. The entire cost is importing lazy_importer itself (its module body, plus contextlib, threading and importlib.util); the deferrals it buys back are the ones already in place.

lazy-importer becomes a dependency below 3.15. It is pure Python with no dependencies of its own, but it is at 0.1.0, which is worth weighing for a package this far down the stack — say the word and I'll make compat/py314 fall back to eager imports when it is absent, at the price of uniformity.

Verification

tox -e py,py311,py312,py313,py314 — 266/267 passed on each, mypy clean, coverage 99%; tox -e docs and tox -e diffcov clean, diff coverage 100%. Beyond the test above, test_declared_modules_resolve checks that reading a lazy name yields the real module and not one of the backport's proxies, which is what isinstance and type() comparisons would otherwise trip over.

🤖 Generated with Claude Code

Six imports sat inside the functions that used them, each with a comment
explaining why, and none where a reader looks for a module's
dependencies. PEP 810 lets the language express that instead: declared in
__lazy_modules__, they stay deferred while reading as ordinary imports.

Python 3.15 honors the declaration natively. Earlier versions reach the
same behavior through the lazy_importer backport, kept behind
compat/py314 so the shared module carries only one import and one with
statement, both removed when syncing to the stdlib.

Costs about 1-2 ms of import time before 3.15, the price of importing
lazy_importer, and nothing measurable on 3.15.

Closes #554

Co-Authored-By: Claude Opus 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restore the deferred imports to module scope with PEP 810 lazy imports

1 participant