Table of Contents

If you are a maintainer of a Python package, it’s nice if you pay attention to the time it takes to import your package.

Further, if you’ve got a Python package with multiple components where it’s probable that many users will only use part of the package, then it’s super nice if you set up your __init__.py files for lazy importing.

Previously - lazy importing other packages

In Python lazy imports you can use today, I discussed:

  • “PEP 810 – Explicit lazy imports” proposed for Python 3.15.
  • Using python -X importtime to measure how long it takes to import something.
  • Speeding up startup time by lazily importing your code’s dependencies using various techniques:
    • Moving imports into the first (or any) function that needs it.
    • Using importlib.import_module()
    • Considerations during testing.
    • For testing, using fixtures to handle lazy imports.

What if it’s your own package that needs the speedup?

The focus of the last article was on speeding up startup time for an application by delaying the import of dependencies until they are needed.

However, what if you are building someone elses dependencies? Meaning, what if your package is used by other applications and packages?

Then you can help out those applications and packages by making your own package super fast to import. That way, your package isn’t the problem in their startup time or test collection time.

Taking this problem further, if your package has a bunch of useful tools, but it’s totally reasonable for a user of your package to not need ALL of them always, you can break up your import in a way that even when someone does import your package, they only import your sub-parts until they need them.

That’d be awesome. How do we do that now?

Using __getattr__() in __init__.py to cache components and lazy load as needed

Well, I’m sure lots of experienced Python package devs already know this, but I just learned how to do it from Will McGugan, creator of Rich and Textual.

Textual has a widgets sub-package with widgets like Button, Checkbox, ListView, Input, etc. When someone imports, say Button, they can say from textual.widgets import Button, and it ends up only importing the the portion of textual needed for Button.

The code for how this is done is in textual/src/textual/widgets/__init__.py

Here’s a shortened version, focusing just on a couple of objects:

from __future__ import annotations
import typing
from importlib import import_module
from textual.case import camel_to_snake

if typing.TYPE_CHECKING:
    ...
    from textual.widget import Widget
    from textual.widgets._button import Button
    from textual.widgets._checkbox import Checkbox

__all__ = [
    "Button",
    "Checkbox",
    ...
]

_WIDGETS_LAZY_LOADING_CACHE: dict[str, type[Widget]] = {}


# Let's decrease startup time by lazy loading our Widgets:
def __getattr__(widget_class: str) -> type[Widget]:
    try:
        return _WIDGETS_LAZY_LOADING_CACHE[widget_class]
    except KeyError:
        pass

    if widget_class not in __all__:
        raise AttributeError(f"Package 'textual.widgets' has no class '{widget_class}'")

    widget_module_path = f"._{camel_to_snake(widget_class)}"
    module = import_module(widget_module_path, package="textual.widgets")
    class_ = getattr(module, widget_class)

    _WIDGETS_LAZY_LOADING_CACHE[widget_class] = class_
    return class_

The camel_to_snake() method is regular expression search and replace to go from class name to module name, so Button to _button, etc. It’s here if you want to take a look. And also elegant, of course.

As always, reading the Rich and Textual implementation is a master class in cool yet simple (once you see it) Python techniques.

How’s it work?

There are a couple of dunder methods at play when an attribute is accessed:

  • __getattr__() is called whenever an attribute is missing.
  • __getattribute() is called on every access.

When someone access any of the widget classes (attributes) of textual.widgets, Python won’t find them, so the __getattr__() method will get called.

On the first access of Button, the Button class will not be found in initially empty dictionary _WIDGETS_LAZY_LOADING_CACHE, so it gets imported with import_module(), saved to the _WIDGETS_LAZY_LOADING_CACHE dictionary, and returned.

Every other access to Button will find it in the dictionary, and just return it.

Simple. Cool.

Weighing the complexity and import vs runtime speed

Even though this implementation works and isn’t too complicated, it’s not as simple as NOT lazy importing.

For runtime speed as well. Dictionary access is obviously something we do all the time. However, running through a custom __getattr__() can’t be free. I haven’t measured it, but it’s gotta be non-zero. But honestly, I think the complexity increase is the main consideration.

So it’s a tradeoff. If you are providing separate services with your package that can be used relatively independently, and using lazy imports can provide speedup of import and/or memory advantages, then I think lazy loading is worth the slight complexity increase and probably very slight runtime speed hit.

However, if your package sub-components are usually used together, or optimizing import doesn’t significantly speed up either import time or memory footprint, then it’s not worth it.

It’s the middle ground that’s tricky, and a judgement call.

Feedback, of course

I’m obviously thinking about this a lot lately, so if I’ve missed something or you know of some other cool techniques, please let me know