PEP 810: Explicit lazy imports

I don’t, it’s hand-waving :slight_smile: It matches my experience with many Python projects, but YMMV, of course. I would note that importing modules is also usually (again, in my experience) without side effects, you just use a type/function from it later, so I don’t see that being different from importing a function directly.

I think that’s fine on its own. I just wondered if the potential community aspect of people spamming PRs to random projects to “optimize” them, or Python developers proactively using lazy import everywhere, was considered. It’s something that we have been thinking about a lot for new opt-in perf-oriented features in Rust, so I thought I’d mention it here, as I didn’t see it being discussed.

But ignoring all of that, making eager the opt-in is a bit hostile towards beginners where reasoning about lazy imports is harder than lazy imports. In that spirit, I don’t think asking us to type 5 more characters when the perf matters is a big deal.

I think that the reasoning is complicated in both situations. It’s not trivial to benchmark the effect of making a single import in a Python program lazy, it might be difficult to understand the performance characteristics, which could lead to the “premature lazy optimization” behavior I described above.

Features normally have a set of trade-offs. If developers knew that this feature can make their code 5x slower or 5x faster, it’s clear that they would have to think hard about its usage, and not use it willy-nilly everywhere. But if the story is more like “this either has essentially no performance downside or it can make your code faster and take less memory” (which, as far as I understand, is the case), then that might lead to a world where the optimal behavior is to simply use a lazy import everywhere.

2 Likes

Maybe @thomas and I will still need to duke it out, but here’s my suggestion for addition to the filter section:

The filter function is called at the point of execution of the lazy import or lazy from import statement, not at the point of reification. The filter function may be called concurrently.

There’s still the performance impact of the filter function to address. I’m assuming that if there is no filter function (likely the common case), there’s a fast short-circuit. If so, it begs the question of how to unset the filter function, and I assume that calling set_lazy_imports_filter(None) will do that. It might be worth mentioning in the PEP, but it’s definitely worth mentioning in any future documentation.

I added a full new section about this and tagged you on the PR :wink:

Short story:

  • No filter function by default: quick NULL check and therefore no impact.
  • if you install a function the baseline impact is negligible
  • If your installed function does stuff then is as expensive as what you do

I also tagged you on a big PR addressing some of your proposed nitpicking :slight_smile:

Check the PEP for more details :slight_smile:

2 Likes

Since you’re gonna move the filter function getter and setter to importlib :wink: it makes sense to me for the type to be there too.

Ok, it’s poll time!

Where should we place the lazy imported module type?
  • importlib
  • types
0 voters

Blockquote

It’s a “secret” type instantiated internally by the interpreter/runtime - to me, that says types. Save importlib for things that are part of an API that users may need to use/implement directly.


Oh, and if you’re going to call it a “lazy object type” and not a “lazily imported module type”, then you’d better think about how it will one day be extended to apply to any object at any time :wink: (or just name it something specific to imports)

6 Likes

Definitely “ lazily imported module type”. Updated the poll

Once concern I have, andpip doesn’t have this problem, since it’s usually not used as a library, is library authors using sys.set_lazy_imports(“disabled”) inside their module and then not re-enabling it. It’s also unclear to me if they can actually re-enable it in all use cases, since there’s not really anywhere to put it, if they want it disabled for all calls to their functions.

This will diminish the lazy-import performance gains for the rest of the application (even more so if the user set it to enabled).

It seems destructive to me to keep this in it’s current state - I’m worried some library down the chain will do this and it’ll make the feature much less useful to a lot of people, without them even knowing. I tried to come up with a solution, but I couldn’t think of one that works well.

I read the entire thread and hasn’t seen anyone mention this, so please let me know if I’m missing something here and it’s not actually an issue :slight_smile:

2 Likes

Library authors should not disable lazy imports inside their libraries. Please see the previous discussion regarding the global flag to disable and the PEP updates but short story: it’s an advanced feature for end users (here pip is an end user because its exposed as a program on its own) and you need to be aware of the consequences. See also this section for some related extra context

4 Likes

types has the advantage of having the “lazily imported module type” living together with types.ModuleType.

I like the general idea of lazy import, and as many posters noted the purported benefits of lazy import is quite large and people including me will try to use it everywhere. I get the impression thata most modules, except in a few cases, would support being lazily imported out-of-the-box.

Still, users will want find out if a module can be lazily imported. All is good if the library author explicitly states that a module can be lazily imported (or not). If that’s not the case, as with legacy modules, the PEP states

Test that side-effect timing changes don’t break functionality.

and

When in doubt, test lazy imports with your specific use cases.

Testing is obviously important but nonetheless certain forms of breakages are things getting subtly wrong timings and thus hard to test, for example reading a config file at a wrong time, initializing an event loop or database connection from a wrong thread. These are all classic heavyweight operations that will certainly benefit from lazy import when someone just wants --help from the command line. But what should users and authors do about them? How can library authors make their code compatible with lazy import? Will there be a HOWTO documentation on lazy import patterns?

3 Likes

Yeah but importlib is where the rest of the features are, so I think that’s not an direct winning argument.

As I understand the PEP, library authors don’t need to do anything to “support” lazy imports in the default mode. When someone writes lazy import your_library, that’s the importing code’s decision and responsibility not yours as the library author. The important part is that your library code doesn’t change at all internally by someone lazy importing it. Whether someone imports your library with import your_library or lazy import your_library, your module executes exactly the same way and the only difference is when it executes (at import time vs. first use). This timing shift is the importer’s concern to manage, not yours.

The guidance in “How to Teach This” (avoiding import-time side effects, using explicit initialization functions, etc.) is primarily for those who want to use the advanced global mode out of the box. Even then, as someone noted before library maintainers can reasonably decide not to support global mode.

The PEP’s “How to Teach This” section already provides concrete points for supporting the global mode. But I assume this will translate into a better “How to” document about it. Indeed: if we sneakily spy on this in progress PR by the authors you can see that they indeed want to do exactly that.

1 Like

I apologise for too many back-and-forths with a spec supporters who are not the PEP authors.

Mine is only one person’s opinion:

  • A proposal to change language syntax must to clear a very high bar
  • There has to be a working tentative implementation :white_check_mark:
  • There have to be examples of real-world projects using this implementation :hourglass_not_done:

Prior art is useful, however a curated, reproducible evidence is what’s needed.

Shifting the burden to the readers is a show-stopper for me (one person).

A technical question: what is the proposed C API to load modules lazily and to resolve lazily-loaded modules?

In my (published, no users) extension, I currently have:

Which is the equivalent of import functools at the start of a module.

What would that look like (API) and work like (semantics) if I wanted a lazy import instead?

Since top-level module code is run when a module is imported, the “import convention” should be considered an entry point and thus part of a library’s public API. It is as much a responsibility of the library author to document the sensitivity of their module-level code (which gets run on import) to external state and thus requires additional care on import.

1 Like

This thread contains direct feedback from maintainers of several projects such as attrs, FastAPI, Scientific Python, stamina, PySide, and other major libraries stating they would immediately benefit from this feature. The PEP documents that several major companies Meta, Google, HRT, and Bloomberg already use similar implementations in production at scale with measured performance gains. Some users have shared resources in articles, bug tracker items and other links.

Python doesn’t require real-world adoption before accepting syntax changes. For example pattern matching was accepted without requiring projects to prove adoption. The evidence here (production use + direct enthusiasm from library maintainers in this discussion) exceeds typical standards.

The “burden” you mention isn’t being shifted. I think your requests here are unfair

13 Likes

I think you’re misunderstanding how this works. As a library author, you don’t write your code any differently. Your module executes identically whether someone uses import your_library or lazy import your_library.

The lazy keyword affects the importer, not the library being imported. When someone chooses to lazy import your library, they’re accepting the timing change. That’s their responsibility, not yours.

Unless I am missing something you don’t need to document anything special or make your code “compatible with lazy imports.” (unless you want it to work under the global flag). With the normal mode your library’s behavior is unchanged.

1 Like

Put another way: library authors already have no control over when their module is imported. Users might have code like this today:

def func_a():
    import your_module
    your_module.some_func()


def main():
    …. # lots of code here
    func_a()

main()

With lazy imports, they might change this to be:

lazy import your_module

def func_a():
    your_module.some_func()

with no practical difference at runtime.

Thus: if your module has special requirements about when it can be imported, it’s already incumbent on you to clearly document these to consuming code. The only thing that would change is to include consideration of lazy imports into that documentation.

12 Likes

Switching to a fallback/alternative upon a failed import is a rather common pattern and it’d be somewhat of a bummer if one has to revert to using eager import for handling ImportError.

Maybe there can be an extended syntax for registering a block of exception handler that’s executed in the same frame upon ImportError when the name is reified:

try lazy from fastfoo import bar except: # implicitly handles ImportError only
    from oldfoo import bar
    # or: bar = more_compatible_bar
    # or: raise RuntimeError("Friendlier error message.")

if action == "print":
    print(bar())
elif action == "errorcode":
    sys.exit(bar())
else:
    print("Usage: foo <print | errorcode>")

so that there’ll be no need to handle ImportError in multiple places downstream where bar may be first used.

When needed, this can be accomplished with a very simple wrapper module…

main.py:
---
lazy from whatever_bar import bar


whatever_bar.py:
---
try:
    from fastfoo import bar
except ImportError:
    from oldfoo import bar

It’d be a bunch of boilerplate to maintain a bunch of those wrappers, but it’s a lot simpler than adding syntax.

13 Likes

Is the following a correct rephrasing ?
“”"
Reification imports the module in the same way as it would be done eagerly at reification time, employing the current state of the import system (i.e. of sys.path, sys.meta_path, sys.path_hooks, __import__, importlib, sys.modules and PYTHONPATH).
“”"

1 Like