Working to Make Python Lazy Python 3.15a7 introduces lazy imports, a feature proposed in PEP 810 that defers module loading until first use, potentially speeding up CLI applications and large codebases. The author has developed a helper tool, flake8-lazy, to automate converting existing code to use lazy imports, and reports that it was the first library they developed with heavy AI assistance. The feature supports both a new 'lazy import' syntax and a backward-compatible '__lazy_modules__' list, with flags like '-X lazy_imports=all' for testing. Python 3.15a7, which is now just a uv python install 3.15 away on all major platforms, has lazy imports This exciting feature, proposed in PEP 810 https://peps.python.org/pep-0810 , promises to make CLI applications faster especially when using flags like --help , and could make a lot of large code with lots of imports that don’t always get used faster too. Unlike the earlier, failed attempt, this requires libraries to put in some work. I’ve developed a helper tool to make it easy; I’d like to cover what lazy imports are and how to use my tool. Since this is the first library that I used AI heavily in developing, the second half of the post will cover how my experience with AI for a task like this went. TL;DR: run uvx flake8-lazy --apply=list to make your code magically faster on Python 3.15 What is a lazy import? Imagine you have a file like this, with a standard Python argparse CLI: python import argparse import numpy 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 What happens if you run this with --help ? The numpy library will be imported, even though it is never used. If you are using modern uv tooling, this can be even worse, since uv doesn’t pre-compile bytecode unless you ask it to; that makes the install faster, but imports are slower the first time. The above is just one example; this can also happen when you have this common pattern: python init .py from . import a from . import b all = "a", "b" The idea behind this is that a user can just use lib.a.stuff with just import lib , rather than import lib.a , but you pay the cost of import even if they never use all the imports. Some libraries, like rich , are careful to avoid this and ask users to import explicitly, but many older libraries did this. And there are also libraries that can do multiple things like CLI libraries with subcommands , but you don’t need the dependencies for every subcommand. How to use Python 3.15’s lazy imports Take the first example. In Python 3.15, you can now write: python lazy import argparse lazy import numpy 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 Now, both imports are “lazy”, meaning nothing happens at all when you import them. They might not even be installed. The first time you try to use the object, though, it becomes a real, imported object. So if you do --help , numpy is never accessed and never imported. There is also a backward-compatible syntax: python lazy modules = "argparse", "numpy" import argparse import numpy This works on older Pythons it’s just not lazy , and you can also dynamically generate or manipulate that list if you want. Linters like Ruff have already updated to allow this to be placed above your imports without triggering a lint violation. I should mention there’s a flag and a variable to make Python treat all imports as lazy, -X lazy imports=all and PYTHON LAZY IMPORTS=all also normal and none . That’s mostly for testing. none doesn’t disable lazy modules ; it only disables the syntax version Will be fixed https://github.com/python/cpython/pull/146371 . none should only disable the back-compat lazy modules variation; lazy import should be a guaranteed lazy import. The existence of this option as it stands is currently blocking use in the standard library. Why not lazy? Shouldn’t you just mark everything as lazy? You don’t have to. There are some modules that have side effects when you import them; if those side effects need to happen at the import site, then those can’t be lazy. This pattern, for example, can’t be lazy: python try: import numpy except ModuleNotFoundError: ... The error here will move to the first usage of something from numpy . There is a semi-lazy alternative: python import importlib.util if importlib.util.find spec "numpy" is None: ... whatever you wanted to do if numpy is missing lazy import numpy This is slightly more expensive than doing nothing at all which is why lazy importing doesn’t do it , will import packages to get to subpackages a.b imports a , and some types of import errors won’t trigger when just finding the spec for the above example, numpy. core could be missing/broken if someone didn’t compile numpy correctly - this is rare, though . Regardless, this is a pretty good way to check to see if a package is installed. The other case you don’t need lazy is if you use something at top level. For example: python lazy import re REGEX = re.compile ... not lazy here Here, the lazy import is not needed, since you can’t process the file without importing this anyway. You can work around this by caching: php import functools lazy import re @functools.cache def regex - re.Pattern: return re.compile ... python from future import annotations lazy modules = "re" import functools import re @functools.cache def regex - re.Pattern: return re.compile ... Notice I don’t need from future import annotations to make this work; the annotation doesn’t cause the re module to be loaded because in Python 3.14 annotations became lazy by default in that version. You can make these sorts of imports lazy, but you are just moving the import errors for no good reason, so it’s a bit better not to. If you want to make everything in a file lazy, you can do it like this: php class AllLazy: @staticmethod def contains : str - bool: return True lazy modules = AllLazy This simply is used by testing with in on full module names, and you can put your own object in here. The static tool below doesn’t look for this yet. A tool to help So libraries ideally should start adding these lazy modules , but it’s a little more complex than just putting all modules into it. So I wrote a tool, flake8-lazy https://github.com/henryiii/flake8-lazy , to help with figuring out exactly what to add, and with keeping it tidy. This is the first library I’ve used AI tools heavily in developing I’ve started using them to help maintain plumbum, but that’s not from scratch , so I’ll end with a section about how that went very well . I’ve developed flake8-errmsg https://github.com/henryiii/flake8-errmsg in the past, so it’s not my first flake8 plugin. Like that project, there’s also a built-in standalone runner; early in the 3.15 lifecycle, I rather expect that to be the main way to use it. To use it: Show flake8-style errors uvx flake8-lazy