Skip to content

Truly lazy imports; import on attribute access or when called. Type checker friendly.

License

Notifications You must be signed in to change notification settings

knitli/lateimport

Repository files navigation

lateimport

Lazy import utilities for Python — defer module loading until an imported object is actually used.

PyPI Python License CI

Overview

lateimport provides two complementary tools:

  • lateimport — an explicit proxy for deferring imports in application code
  • create_late_getattr — a factory for the __getattr__ hook in package __init__.py files, for use with dispatch-table-based lazy loading (e.g. as generated by Exportify)

Both are stdlib-only. No dependencies.

Requirements

Python 3.12+

Installation

pip install lateimport

Usage

Explicit lazy imports

Use 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()   # True

Type annotations

LateImport[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: DataFrame

The 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: Path

Package-level lazy __getattr__

For 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.submodule and returns getattr(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.

API

lateimport(module_name, *attrs) -> LateImport[T]

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.B

class LateImport[T]

The 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_late_getattr(dynamic_imports, module_globals, module_name)

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

INTROSPECTION_ATTRIBUTES

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.

License

MIT OR Apache-2.0 — see LICENSE-MIT and LICENSE-Apache-2.0.

About

Truly lazy imports; import on attribute access or when called. Type checker friendly.

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •  

Languages