Skip to main content

flake8-lazy

Actions Status Documentation Status

PyPI version PyPI platforms

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.

Quick run

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.

Install

python -m pip install flake8-lazy

Usually you would include this in some sort of dependency-group in your project, e.g. dev or lint.

flake8 will automatically discover the plugin.

Pre-commit

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.10.1
  hooks:
    - id: flake8-lazy

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

Agent skill

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.

Rule codes

1xx: Missing lazy declarations

  • LZY101: Missing lazy stdlib module in __lazy_modules__
  • LZY102: Missing lazy third-party or local module in __lazy_modules__

2xx: __lazy_modules__ validation

  • LZY201: __lazy_modules__ list is not sorted (string entries first, relative f"{__spec__.parent}..." entries last, as an editor line sort produces)
  • LZY202: Module listed in __lazy_modules__ is never imported
  • LZY203: Module listed in __lazy_modules__ appears more than once
  • LZY204: __lazy_modules__ is assigned after importing modules it names
  • LZY205: Module name in __lazy_modules__ is relative (.name) instead of absolute

3xx: Native lazy keyword (Python 3.15+)

  • LZY301: Lazy import inside suppress(ImportError) is misleading
  • LZY302: Module is declared lazy by both lazy keyword and __lazy_modules__
  • LZY303: Module is imported both eagerly and lazily

4xx: Lazy import safety and semantics

  • LZY401: Module is declared lazy but accessed at the top level
  • LZY402: Module is an enclosing package for this file and should not be lazy

Basic example

__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!

Authoring __lazy_modules__

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()

How detection works

flake8-lazy inspects module-scope imports and module runtime usage.

  • Counts top-level import and from ... import ... statements.
  • Currently treats annotation-only usage as lazy-capable (from __future__ import annotations if not using 3.14+).
  • Treats usage inside if typing.TYPE_CHECKING: as type-only.
  • Handles static sys.version_info checks
  • Skips from __future__ import ....
  • Skips imports inside try/except/finally blocks, which can never be made lazy.
  • Requires exact module entries for nested imports. Parent packages are not needed: import a.b.c only needs "a.b.c". Older versions recommended the parents; LZY202 now reports them and --apply removes them.
  • Treats enclosing package names as non-lazy for a file. For example, in a/b/c.py, a and a.b should not be listed as lazy. The exception is from a import name: a lazy import defers the attribute fetch, which can load a submodule, so a is recommended there.

CLI

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

Arguments can be files or directories; directories are searched recursively for Python files, skipping hidden directories and __pycache__:

flake8-lazy 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.py

Available 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. Use numpy.* to also match all submodules of numpy.

See the CLI docs for all options.

Local development

See CONTRIBUTING.md for local development instructions.

FAQ

Why is this not in Ruff?

It's really new, and it's a bit complex. Maybe someday it will be? :)

It's missing something!

Open an issue! If it's clear and detailed and in-scope, I might even be able to assign it to copilot!

Acknowledgements

GitHub Copilot in VS Code was used to help develop this package. The Scientific Python Development Guide template was used as a starting point.

Metadata

Release files for flake8-lazy 0.10.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flake8-lazy 0.10.1
File Size Uploaded
flake8_lazy-0.10.1.tar.gz 56.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flake8-lazy 0.10.1
File Interpreter ABI Platform
flake8_lazy-0.10.1-py3-none-any.whl Python 3 none any Details

Total release size: 97.8 kB

Release files / flake8_lazy-0.10.1.tar.gz

Download URL flake8_lazy-0.10.1.tar.gz
Size 56.8 kB
Tags Source
SHA-256 checksum
How to use checksums
3a29f054b5f7b7c7bd2c1c4b55a715a0ac6b2220b4203f6a5958e2f2419865a0
BLAKE2b-256 checksum
How to use checksums
db7087c00959879c88ed464771d2af8aa9b0690b288c2acfe33be1c6daa63a53
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / flake8_lazy-0.10.1-py3-none-any.whl

Download URL flake8_lazy-0.10.1-py3-none-any.whl
Size 41.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b74b6aaeaffc131b02571418971c95ea636eb5d5045dcd3faa39337c9e003f66
BLAKE2b-256 checksum
How to use checksums
97a09b14eb8bd13daacb17f8d6cf4fe48c958a02cd760cb9a2de462175ac4168
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.10.1 This release

2 release files

0.9.0

2 release files

0.8.5

2 release files

0.8.4

2 release files

0.8.3

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page