🪶 Performance: lazy imports¶

import python_utils is intentionally cheap. The package uses PEP 562 module-level __getattr__ so that nothing is imported when you import the package — each submodule and each exported name is loaded on first access, then cached.

Why it matters¶

Utility libraries are often imported everywhere, including in code paths that only need one or two synchronous helpers. Eagerly importing every submodule would drag in asyncio (via the async helpers) and other machinery you may never use, inflating startup time and memory. Lazy loading keeps the import graph minimal.

In particular:

  • Only need the synchronous helpers? asyncio is never imported.

  • Even typing_extensions is deferred, so the base import stays tiny.

See it for yourself¶

import sys
import python_utils

'asyncio' in sys.modules             # False
'typing_extensions' in sys.modules   # False

python_utils.acount                  # touches `aio`, which imports asyncio...
'asyncio' in sys.modules             # True

How it works¶

The package __init__ maps every public name to the submodule that defines it and resolves them on demand:

def __getattr__(name):
    # look up which submodule provides `name`, import it lazily,
    # cache the result in globals() so __getattr__ runs only once,
    # and return it.
    ...

Type checkers and IDEs still see everything: the same names are declared under a typing.TYPE_CHECKING block and listed in __all__, so autocompletion and static analysis work exactly as if the imports were eager. __dir__ is implemented too, so dir(python_utils) and tab-completion list every lazily-available name.

Practical tips¶

  • Import what you use directly (python_utils.to_int) — the first access pays the one-time import cost for that module only.

  • Importing from a submodule (from python_utils import converters) is equally lazy with respect to the other submodules.

  • The cost is paid once per process; subsequent accesses are plain attribute lookups.