flake8-lazy is a flake8 plugin that finds imports which can be made lazy in Python 3.15 (following PEP 810). See the post here for more on the development of this tool!
flake8-lazy helps keep import-time overhead low by detecting imports that can be
declared as lazy in __lazy_modules__. For this package itself,
flake8-lazy --help runs roughly twice as fast when using Python 3.15's new
lazy import system.
Error messages will mention __lazy_modules__ since that is backward compatible
with older Python versions, but the lazy keyword is supported too.
There's a standalone flake8-lazy runner. If you use uv or pipx, you can run it
from anywhere without installation:
uvx flake8-lazy <filenames>
# OR
pipx run flake8-lazy <filenames>Try --format=lazy-modules to get copy-paste lines or even --apply=list to
have the tool update your lazy modules automatically! For maximum laziness, try
--apply=dynamic.
The standalone runner also reads defaults from a [tool.flake8-lazy.standalone]
table in your pyproject.toml. A file can set its own options in a
[tool.flake8-lazy] table inside a PEP 723 # /// script block; this also
works under flake8. See the
CLI docs for details.
python -m pip install flake8-lazyUsually you would include this in some sort of dependency-group in your project,
e.g. dev or lint.
flake8 will automatically discover the plugin.
flake8-lazy ships a pre-commit /
prek hook. Add it to your .pre-commit-config.yaml:
- repo: https://github.com/henryiii/flake8-lazy
rev: v0.11.0
hooks:
- id: flake8-lazyThe hook reports diagnostics by default; add args: [--apply=list] (or another
--apply mode) to have it rewrite files in place on commit. This is the
built-in runner, use flake8's pre-commit integration if you want flake8 to run
it.
See the full documentation for details, examples, and the standalone CLI runner.
This repo ships a skill, setup-lazy-imports, that adopts flake8-lazy for you:
it adds the pre-commit hook (defaulting to --apply=set), runs it once to
declare lazy imports, and verifies the result with your test suite and an import
check.
Add it with gh (npx skills should work too):
gh skill install henryiii/flake8-lazy setup-lazy-imports(Add --agent ... to pick your tooling.) Then run directly or ask to "set up
lazy imports" in any Python project.
LZY101: Missing lazy stdlib module in__lazy_modules__LZY102: Missing lazy third-party or local module in__lazy_modules__
LZY201:__lazy_modules__list is not sorted (string entries first, relativef"{__spec__.parent}..."entries last, as an editor line sort produces)LZY202: Module listed in__lazy_modules__is never importedLZY203: Module listed in__lazy_modules__appears more than onceLZY204:__lazy_modules__is assigned after importing modules it namesLZY205: Module name in__lazy_modules__is relative (.name) instead of absolute
LZY301: Lazy import insidesuppress(ImportError)is misleadingLZY302: Module is declared lazy by bothlazykeyword and__lazy_modules__LZY303: Module is imported both eagerly and lazily
LZY401: Module is declared lazy but accessed at the top levelLZY402: Module is an enclosing package for this file and should not be lazy
__lazy_modules__ = ["argparse", "requests"]
import argparse
import requests
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("url")
args = parser.parse_args()
response = requests.get(args.url, timeout=5)
print(response.status_code)In this example, requests is only used inside main, so it can be lazy. The
checker expects it in __lazy_modules__ and emits LZY102 until you add it.
Running --help will not import requests, resulting in a more responsive app!
Use a static, sorted list of strings:
__lazy_modules__ = [
"argparse",
"numpy",
"pathlib",
]You can also use sets; sets are slower to construct, but faster to check - around 2 items is enough to make sets faster.
Dynamic values (i.e. a custom object assigned to __lazy_modules__) are also
supported. If flake8-lazy detects a non-static assignment it treats the file as
fully covered and suppresses all LZY1xx/LZY2xx diagnostics. Use
--apply=dynamic to have the tool write a simple catch-all object:
class AllLazy:
@staticmethod
def __contains__(_: str) -> bool:
return True
__lazy_modules__ = AllLazy()flake8-lazy inspects module-scope imports and module runtime usage.
- Counts top-level
importandfrom ... import ...statements. - Currently treats annotation-only usage as lazy-capable
(
from __future__ import annotationsif not using 3.14+). - Treats usage inside
if typing.TYPE_CHECKING:as type-only. - Handles static
sys.version_infochecks - Skips
from __future__ import .... - Skips imports inside
try/except/finallyblocks, which can never be made lazy. - Requires exact module entries for nested imports. Parent packages are not
needed:
import a.b.conly needs"a.b.c". Older versions recommended the parents;LZY202now reports them and--applyremoves them. - Treats enclosing package names as non-lazy for a file. For example, in
a/b/c.py,aanda.bshould not be listed as lazy. The exception isfrom a import name: a lazy import defers the attribute fetch, which can load a submodule, soais recommended there.
The project also provides a direct CLI runner:
flake8-lazy path/to/file.py another_file.py
# or
uvx flake8-lazy path/to/file.py another_file.pyArguments can be files or directories; directories are searched recursively for
Python files, skipping hidden directories and __pycache__:
flake8-lazy src/To skip more files or directories, use --exclude with a comma-separated list
of glob patterns. A pattern matches the base name of a file or directory. A
pattern with a path separator matches the full path, relative to the current
directory (or to the directory of pyproject.toml when set there). Excluded
directories are not searched, and files given explicitly on the command line are
also skipped if they match:
flake8-lazy --exclude='tests,*_pb2.py,src/pkg/_vendor' src/The default output format matches flake8-style diagnostics:
$ flake8-lazy path/to/file.py
path/to/file.py:12:0: LZY102 module 'numpy' should be listed in __lazy_modules__You can also ask for a copy-pasteable recommendation instead:
$ flake8-lazy --format lazy-modules path/to/file.py
path/to/file.py: __lazy_modules__ = ["numpy", "pandas"]To rewrite files in place with the recommended declaration, use --apply:
flake8-lazy --apply=list path/to/file.py another_file.pyAvailable modes: list, set, native (3.15+ syntax), dynamic. Long list
and set assignments are split one module per line with a trailing comma
(black/ruff style) when they exceed --line-length (default 88), so the
output needs no reformatting. When an __lazy_modules__ assignment already
exists, its quote style (single or double) is preserved, so --apply stays a
no-op once the module set is correct and does not fight a single-quote
formatter; new assignments use double quotes. The command exits with status code
1 if any error is found.
To skip modules in recommendations, use these options. They also work under
flake8 and in [tool.flake8-lazy.standalone] in pyproject.toml:
--lazy-import-preset=none|minimal|default: the built-in set of modules that Python always imports at startup (default:default).--lazy-exclude-modules=MODULES: a comma-separated list of more modules to skip. A name matches only that module. Usenumpy.*to also match all submodules ofnumpy.
See the CLI docs for all options.
See CONTRIBUTING.md for local development instructions.
It's really new, and it's a bit complex. Maybe someday it will be? :)
Open an issue! If it's clear and detailed and in-scope, I might even be able to assign it to copilot!
GitHub Copilot in VS Code was used to help develop this package. The Scientific Python Development Guide template was used as a starting point.