Lazy import utilities for Python — defer module loading until an imported object is actually used.
lateimport provides two complementary tools:
lateimport— an explicit proxy for deferring imports in application codecreate_late_getattr— a factory for the__getattr__hook in package__init__.pyfiles, for use with dispatch-table-based lazy loading (e.g. as generated by Exportify)
Both are stdlib-only. No dependencies.
Python 3.12+
pip install lateimportUse lateimport when you want to defer a heavy import in application code. The module is not imported until the returned proxy is actually used — called, subscripted, passed to isinstance, etc.
from lateimport import lateimport
# No import happens here
numpy = lateimport("numpy")
pandas = lateimport("pandas", "DataFrame") # defers pandas.DataFrame
# Import triggered on first use
result = numpy.array([1, 2, 3])
df = pandas({"a": [1, 2]})Proxies are thread-safe and cache the resolved object after first access.
proxy = lateimport("heavy.module", "ExpensiveClass")
proxy.is_resolved() # False
instance = proxy() # heavy.module imported, ExpensiveClass resolved
proxy.is_resolved() # TrueLateImport[T] is a generic class. Annotating a variable with the resolved type gives you full type-checking and autocompletion downstream — the type checker knows that calling or accessing the proxy produces a T.
from __future__ import annotations
from typing import TYPE_CHECKING
from lateimport import LateImport, lateimport
if TYPE_CHECKING:
import numpy as np
from pandas import DataFrame
# Type checker knows ndarray() returns np.ndarray
ndarray: LateImport[np.ndarray] = lateimport("numpy", "ndarray")
arr = ndarray([1, 2, 3]) # arr: np.ndarray
# type[DataFrame] means calling the proxy produces a DataFrame instance
DataFrame: LateImport[type[DataFrame]] = lateimport("pandas", "DataFrame")
df = DataFrame({"col": [1, 2, 3]}) # df: DataFrameThe TYPE_CHECKING guard keeps the imports from executing at runtime — you get static type information without the import cost, which is the whole point.
For types that are always available no guard is needed:
from pathlib import Path
from lateimport import LateImport, lateimport
MkPath: LateImport[type[Path]] = lateimport("pathlib", "Path")
p = MkPath("/tmp/out") # p: PathFor packages using a dispatch-table pattern (e.g. generated by Exportify), create_late_getattr creates a __getattr__ hook that imports attributes on demand:
# mypackage/__init__.py
from types import MappingProxyType
from lateimport import create_late_getattr
# NOTE: for IDE support, import any dynamic imports in a TYPE_CHECKING block:
from typing import TYPE_CHECKING:
from mypackage.core.models import MyClass
from mypackage.utils.helpers import my_function
from mypackage import SubModule
_dynamic_imports = MappingProxyType({
"MyClass": ("mypackage.core", "models"),
"my_function": ("mypackage.utils", "helpers"),
# for the current package you can just use __spec__.parent:
"SubModule": (__spec__.parent, "__module__"),
})
__getattr__ = create_late_getattr(_dynamic_imports, globals(), __name__)
__all__ = ("MyClass", "my_function", "SubModule")
# Make sure dir calls use __all__ and not globals:
__dir__ = lambda: list(__all__)The dispatch tuple is (package, submodule):
- Normal attributes: imports
package.submoduleand returnsgetattr(submodule, attr_name) "__module__": imports the submodule itself as the attribute (import_module(f".{attr_name}", package=package))
Resolved attributes are cached in globals() so subsequent accesses are direct.
Create a lazy proxy for module_name. Optional attrs are an attribute chain traversed after import. The function is generic — annotate the variable as LateImport[T] to propagate the resolved type to callers.
lateimport("os") # proxy for the os module
lateimport("os.path", "join") # proxy for os.path.join
lateimport("mypackage", "A", "B") # proxy for mypackage.A.BThe generic proxy class returned by lateimport. Annotate the variable with LateImport[T] to tell the type checker what type the proxy resolves to — __call__ and _resolve() are typed to return T. Supports call, attribute access, dir(), and repr(). Thread-safe.
| Method | Description |
|---|---|
is_resolved() |
True if the import has been triggered |
_resolve() |
Force immediate resolution and return the object (typed as T) |
Create a __getattr__ function for package-level lazy loading.
| Parameter | Type | Description |
|---|---|---|
dynamic_imports |
MappingProxyType[str, tuple[str, str]] |
Dispatch table |
module_globals |
dict[str, object] |
globals() of the calling module |
module_name |
str |
__name__ of the calling module |
frozenset of dunder attribute names that are resolved immediately rather than proxied (e.g. __doc__, __name__, __module__). Available for reference when implementing custom proxy patterns.
MIT OR Apache-2.0 — see LICENSE-MIT and LICENSE-Apache-2.0.