Every import at the top of a file runs that module — right then.
import
import argparse import numpy # loaded even for `--help` def main(): parser = argparse.ArgumentParser() parser.add_argument("--foo", action="store_true") args = parser.parse_args() if args.foo: print(numpy.array([1, 2, 3]))
--help
uv
The re-export __init__.py:
__init__.py
from . import a from . import b __all__ = ["a", "b"]
import lib drags in b even if you only touch lib.a.
import lib
b
lib.a
Subcommand CLIs:
Careful libraries (like rich) ask for explicit imports — many older ones don't.
rich
lazy import
PEP 810 makes imports opt-in lazy.
lazy import argparse lazy import numpy
numpy
uv python install 3.15 # beta, but one command away
A back-compat spelling — just not lazy before 3.15:
__lazy_modules__ = ["argparse", "numpy"] import argparse import numpy
Testing knobs: -X lazy_imports=all / PYTHON_LAZY_IMPORTS=all (also normal, none) — flip all on to estimate the payoff.
-X lazy_imports=all
PYTHON_LAZY_IMPORTS=all
normal
none
all
Import side effects — a guarded optional dep can't be lazy:
try: import numpy except ModuleNotFoundError: ...
Semi-lazy alternative:
if find_spec("numpy") is None: ... lazy import numpy
Top-level use buys nothing:
lazy import re REGEX = re.compile(...) # loaded now
Defer it behind a cache:
@functools.cache def regex() -> re.Pattern: return re.compile(...)
Making these lazy just relocates the import — leave them eager.
Deciding what to defer by hand is fiddly. flake8-lazy finds it for you.
flake8-lazy
uvx flake8-lazy <files> # flake8-style errors uvx flake8-lazy --format=lazy-modules # the lines to add uvx flake8-lazy --apply=list <files> # just add them
1xx
LZY101
LZY102
2xx
__lazy_modules__
3xx
lazy
4xx
--apply writes list, set, native, or dynamic — in place.
--apply
list
set
native
dynamic
Don't run it on your test suite — tests use what they import
Profile with -X importtime; what went lazy drops off the list
-X importtime
Skip typing at runtime:
typing
TYPE_CHECKING = False if TYPE_CHECKING: import numpy
Relative imports stay static — build absolute names:
__lazy_modules__ = [f"{__spec__.parent}.thing"] from . import thing
Timing --help on Python 3.15 after running the tool:
Floor is ~15 ms (Python startup) — and uv's no-precompile means the first-run savings are bigger than these warm numbers show.
uvx flake8-lazy --apply=list