Skip to main content

lazy-importer

A backwards-compatibility shim allowing libraries to get lazy import semantics similar to PEP 810 automatically in Python 3.11+ without needing to set your minimum version to 3.15. In Python 3.15+, this falls back to the standard lazy imports mechanism.

Usage

Install from a checkout with python -m pip install .:

from lazy_importer import lazy_importer

__lazy_modules__ = {"json", "pathlib"}

with lazy_importer():
    import json
    from pathlib import Path


# Importing this module doesn't load json or pathlib for these statements.
def read_config(filename: str) -> object:
    return json.loads(Path(filename).read_text(encoding="utf-8"))

__lazy_modules__ determines which imports are lazy. On Python 3.11–3.14, the context enables the fallback, which checks the importing module's declaration at each import. On Python 3.15+, Python reads the declaration directly and the context is a no-op, leaving native controls and filtering in effect without loading the fallback implementation. Without a declaration, imports remain eager under the default interpreter settings.

__lazy_modules__ contains the names of the modules whose imports should be lazy. For a from import, the selected module is the one after from:

Import statement Entry in __lazy_modules__
import example.child "example.child"
from example import child "example"
from example.child import value "example.child"
from .child import value (inside example) "example.child"
from . import child (inside example) "example"

With a set of names, selecting "example" doesn't also select imports of "example.child". This is also how PEP 810's __lazy_modules__ works.

Both paths check membership in __lazy_modules__ at each import, so changes to the declaration take effect within an active context. A custom container can select names through __contains__ without supporting iteration. An empty or absent declaration selects no imports.

Each call to lazy_importer() creates a context. The fallback context can only be entered once, so separate with blocks need separate calls even if they use the same selection. This is why the example calls lazy_importer in the with statement rather than storing the context for reuse.

When imports run

In the example above, importing your module does not load json or pathlib on behalf of those statements. Calling read_config() loads them when it reaches the operations that need them. Any side effects of their initialization, such as registering plugins or configuring logging, are delayed along with the import. A module imported only for its side effects therefore needs an eager import, or an explicit call to resolve() to trigger initialization.

Import failures are delayed too. If one of the selected modules is missing or raises an exception during initialization, that exception reaches the code that first uses it. A try/except around the import block is therefore not a portable way to check whether an optional dependency is available. After a failed import, another use can retry it.

Once loading starts, the module's own imports run normally unless it opts into lazy imports itself. The package uses the import hooks and search path in effect at first use, so changes to the import environment between declaration and use can affect what gets loaded. With from module import first, second, loading first does not resolve the binding for second; each imported name is resolved when it is needed.

Differences from PEP 810

On Python 3.15+, Python handles lazy imports directly. On older runtimes, lazy-importer approximates that behavior with proxy objects, which can forward operations to the imported object but cannot intercept ordinary variable reads. This is the main source of differences between the two.

Native lazy imports resolve when you read the imported name. The fallback waits for an operation such as attribute access or a call, so assigning the name to another variable or putting it in a container can retain a proxy:

from lazy_importer import lazy_importer, resolve

__lazy_modules__ = {"decimal"}
with lazy_importer():
    from decimal import Decimal

classes = [Decimal]
print(type(classes[0]) is type)  # False on the fallback; True with native imports.
classes[0] = resolve(classes[0])
print(type(classes[0]) is type)  # True on both.

resolve(value) returns the real object, loading it if necessary, and returns ordinary objects unchanged. This makes it useful at boundaries where identity or type matters, including exception matching, serialization and extension code that requires a concrete object. Resolving a fallback proxy updates the module globals that still refer to it, but a container holding the proxy keeps that reference until you replace it, as in the example.

Reading another module's attributes has the same limitation: if a module imports Decimal lazily, other_module.Decimal can return a proxy on the fallback, whereas native attribute access resolves the import. Protocol checks can also see the proxy's methods rather than the real object's capabilities:

from lazy_importer import lazy_importer, resolve

__lazy_modules__ = {"math"}
with lazy_importer():
    from math import pi

print(callable(pi))  # True on the fallback; False with native imports.
print(callable(resolve(pi)))  # False on both.

There is also a difference in the scope of the selection. The fallback acts on imports made while the context is active on the current thread, including imports in functions called from the block. Native __lazy_modules__ does not make function-local imports lazy:

from lazy_importer import lazy_importer

__lazy_modules__ = {"decimal"}


def load_decimal():
    import decimal

    return decimal


with lazy_importer():
    module = load_decimal()  # A proxy on the fallback; a real module on 3.15+.

A module-level block containing only import statements avoids this difference. The fallback also does not enforce the native restrictions on lazy imports in class bodies or try blocks. If a context spans a yield or await, other code running on that thread can have its imports deferred according to its own module's declaration. Entering a context doesn't enable the fallback on other threads. Nested contexts keep it active until the outermost context exits.

Native controls and filtering, sys.lazy_modules, and exception chaining back to the import declaration are not provided on older runtimes.

While active, the fallback temporarily replaces builtins.__import__ and preserves any pre-existing hook. Code that replaces the same hook during a context can interfere with lazy selection; if that happens, exiting the context leaves the replacement in place. No proxy is inserted into sys.modules.

The package has no runtime dependencies. See CONTRIBUTING.md for testing and build instructions.

Metadata

Release files for lazy-importer 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lazy-importer 0.1.0
File Size Uploaded Provenance Actions
lazy_importer-0.1.0.tar.gz 84.0 kB With provenance Details for lazy_importer-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for lazy-importer 0.1.0
File Interpreter ABI Platform Actions
lazy_importer-0.1.0-py3-none-any.whl Python 3 none any Details for lazy_importer-0.1.0-py3-none-any.whl

Total release size: 96.6 kB

Release files / lazy_importer-0.1.0.tar.gz

Download URL lazy_importer-0.1.0.tar.gz
Size 84.0 kB
Tags Source
SHA-256 checksum
How to use checksums
b2e296716ce015c0cd80e57b940e52727c5b934b34b8656d5ce06bdd0ee5fbdd
BLAKE2b-256 checksum
How to use checksums
d923d2c8308466a6831043ac3518e480c2e1a06b21a13e0a8f6542ab29cf592a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / lazy_importer-0.1.0-py3-none-any.whl

Download URL lazy_importer-0.1.0-py3-none-any.whl
Size 12.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bd93fab7bef18e842eea02b839410954d37889140dc64ca4d9680aec6880c739
BLAKE2b-256 checksum
How to use checksums
0a287ded071d0975dcc6d5d106ffbf7b3ca420f7caa3b8a235fa604b6e37032a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for lazy-importer 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for lazy_importer-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page