Skip to content

Guidelines

slothy offers advanced use cases for highly aware Python developers.

Tip

Pick your weapons carefully. If lazy importing with slothy doesn't make your codebase significantly easier to maintain, then maybe it's a bad idea to use it.

This document aims to present general DOs and DON'Ts to follow if using slothy. They gather opinions from the author(s) of slothy.

Warning

The rest of this document can be very technical sometimes. Check out the tutorial for a more beginner-friendly description of the intended uses of slothy.

1. Don't use slothy without a reason

Here are some obviously unnecessary uses of slothy:

Instead of

with lazy_importing():
    import pandas as pd

# In the line below, right under the lazy import, you import pandas eagerly.
df = pd.DataFrame()

You can just do

import pandas as pd
df = pd.DataFrame()

and that will be faster.

This could make sense if you make a module that creates df lazily, e.g. when a function is called:

with lazy_importing():
    import pandas as pd

def make_df() -> pd.DataFrame:
    return pd.DataFrame()

# make_df() not called at the module level at all!
# Someone else could import us and THEN call make_df(), perhaps
# due to the instruction stack initiated in their
# `if __name__ == "__main__"` section.
import pandas as pd

with lazy_importing():
    from pandas import DataFrame

You don't need slothy in this case. pandas.DataFrame was already imported by the first line.

Just use

import pandas as pd
from pandas import DataFrame

instead.

Similarly, in

with lazy_importing():
    # Declares a lazy import.
    import pandas as pd  

# Immediately destroys the entire point of declaring a lazy import.
import pandas as pd

you can just

import pandas as pd
instead.

Mixing eager and lazy imports might make sense if there's a logical branch of your code where the lazy import is not eventually performed.

2. Don't rely on delayed imports programmatically

While slothy changes the underlying import system behavior to delay imports, you should never rely on the side effects it brings.

For example, if from spam.py you import a module eggs.py that is located in the same directory:

# ./spam.py

with lazy_importing():
    from .eggs import Ham

you should never rely on from .eggs import Ham not executing some code it otherwise would if it wasn't lazy. For example, don't base off on the fact that Ham is not present in Ham.__base__.__subclasses__() after that lazy import.

In other words, stick to decoupling and give your separate modules autonomy. Never make decisions based on side effect mutations to the global state of your program.

3. Don't prevent eager imports in libraries

Note

You can ignore this rule if you need type_importing() in your library.

As a general recommendation:

  • Use with lazy_importing() in apps.
  • Use with lazy_importing(prevent_eager=False) in libraries.

Try to make libraries as much compatible with non-CPython implementations as possible. slothy, tailored for applications, raises a RuntimeError if it can't ensure imports aren't lazy inside a with lazy_importing() block.

4. Use type_importing() for type-checking imports that may eventually be needed at runtime

If you need slothy for delaying imports of typing-only items that might eventually be requested at runtime (for example by Pydantic), use a dedicated context manager type_importing(). That manager falls back to typing.Any (or other configured runtime-available type) when it cannot import the actual type-checking item.

For instance:

>>> with type_importing():
...     from _typeshed import StrPath
...
>>> print(StrPath)
typing.Any

Warning

If your type-checking imports aren't used at runtime, simply drop using type_importing() (or even entire slothy) at all. However, if you need to pick up type-checking items at runtime, use type_importing() consistently in the entire codebase to minimize the "I'm confused" factor (amongst other factors).

5. Don't lazy-import in class scopes

This won't work:

class Foo:
    with lazy_importing():
        from whatever import maybe_descriptor

The reason for this is that Foo.maybe_descriptor might potentially be a descriptor, which implies slothy would need to broker between its __get__, __set__, __delete__ and even __set_name__ which is already a reason to import it eagerly.

You can imagine that maybe_descriptor can be

# whatever.py

@property
def maybe_descriptor(self) -> None:
    print("I'm here against all the linting rules!")
    return None

@maybe_descriptor.setter
def maybe_descriptor(self, _value: object) -> None:
    print("What's up?")

Even though this is legal in Python, slothy, similarly to mypy, doesn't support descriptors bound via imports.

If you need to lazy-import inside classes, use a class-local property (or a custom descriptor):

with lazy_importing():
    from whatever import anything


class BetterFoo:
    @property
    def descriptor(self) -> Anything:
        return anything  # this binds the item in globals() forever

or, even better:

class EvenBetterFoo:
    @property
    def descriptor(self) -> ???:
        from whatever import anything
        return anything

or, even better than that, don't do such a thing at all.

Note

Opinion: Property getters should ideally not have any side effects. A kind reminder: inspect.getmembers_static was only added in 3.11!