Skip to content

Commit 2f42063

Browse files
authored
PEP 810: Updates based on feedback (#4636)
1 parent 78004ed commit 2f42063

File tree

1 file changed

+179
-27
lines changed

1 file changed

+179
-27
lines changed

peps/pep-0810.rst

Lines changed: 179 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ The soft keyword is only allowed at the global (module) level, **not** inside
246246
functions, class bodies, with ``try``/``with`` blocks, or ``import *``. Import
247247
statements that use the soft keyword are *potentially lazy*. Imports that
248248
can't be lazy are unaffected by the global lazy imports flag, and instead are
249-
always eager.
249+
always eager. Additionally, ``from __future__ import`` statements cannot be
250+
lazy.
250251

251252
Examples of syntax errors:
252253

@@ -273,6 +274,9 @@ Examples of syntax errors:
273274
# SyntaxError: lazy from ... import * is not allowed
274275
lazy from json import *
275276
277+
# SyntaxError: lazy from __future__ import is not allowed
278+
lazy from __future__ import annotations
279+
276280
Semantics
277281
---------
278282

@@ -305,7 +309,7 @@ sequence of fully qualified module names (strings) to make *potentially lazy*
305309
that module are also lazy, but not necessarily imports of sub-modules.
306310

307311
The normal (non-lazy) import statement will check the global lazy imports
308-
flag. If it is "enabled", all imports are *potentially lazy* (except for
312+
flag. If it is "all", all imports are *potentially lazy* (except for
309313
imports that can't be lazy, as mentioned above.)
310314

311315
Example:
@@ -318,7 +322,7 @@ Example:
318322
result = json.dumps({"hello": "world"})
319323
print('json' in sys.modules) # True
320324
321-
If the global lazy imports flag is set to "disabled", no *potentially lazy*
325+
If the global lazy imports flag is set to "none", no *potentially lazy*
322326
import is ever imported lazily, and the behavior is equivalent to a regular
323327
import statement: the import is *eager* (as if the lazy keyword was not used).
324328

@@ -566,20 +570,27 @@ After several calls, ``LOAD_GLOBAL`` specializes to ``LOAD_GLOBAL_MODULE``:
566570
Lazy imports filter
567571
-------------------
568572

569-
This PEP adds two new functions to the ``sys`` module to manage the lazy
570-
imports filter:
573+
This PEP adds the following new functions to manage the lazy imports filter:
574+
575+
* ``importlib.set_lazy_imports_filter(func)`` - Sets the filter function. If
576+
``func=None`` then the import filter is removed. The ``func`` parameter must
577+
have the signature: ``func(importer: str, name: str, fromlist: tuple[str, ...] | None) -> bool``
571578

572-
* ``sys.set_lazy_imports_filter(func)`` - Sets the filter function. The
573-
``func`` parameter must have the signature: ``func(importer: str, name: str,
574-
fromlist: tuple[str, ...] | None) -> bool``
579+
* ``importlib.get_lazy_imports_filter()`` - Returns the currently installed
580+
filter function, or ``None`` if no filter is set.
575581

576-
* ``sys.get_lazy_imports_filter()`` - Returns the currently installed filter
577-
function, or ``None`` if no filter is set.
582+
* ``importlib.set_lazy_imports(enabled=None, /)`` - Programmatic API for
583+
controlling lazy imports at runtime. The ``enabled`` parameter can be
584+
``None`` (respect ``lazy`` keyword only), ``True`` (force all imports to be
585+
potentially lazy), or ``False`` (force all imports to be eager).
578586

579587
The filter function is called for every potentially lazy import, and must
580588
return ``True`` if the import should be lazy. This allows for fine-grained
581589
control over which imports should be lazy, useful for excluding modules with
582-
known side-effect dependencies or registration patterns.
590+
known side-effect dependencies or registration patterns. The filter function
591+
is called at the point of execution of the lazy import or lazy from import
592+
statement, not at the point of reification. The filter function may be
593+
called concurrently.
583594

584595
The filter mechanism serves as a foundation that tools, debuggers, linters,
585596
and other ecosystem utilities can leverage to provide better lazy import
@@ -619,7 +630,7 @@ Example:
619630
return True # Allow lazy import
620631
621632
# Install the filter
622-
sys.set_lazy_imports_filter(exclude_side_effect_modules)
633+
importlib.set_lazy_imports_filter(exclude_side_effect_modules)
623634
624635
# These imports are checked by the filter
625636
lazy import data_processor # Filter returns True -> stays lazy
@@ -639,31 +650,33 @@ The global lazy imports flag can be controlled through:
639650

640651
* The ``-X lazy_imports=<mode>`` command-line option
641652
* The ``PYTHON_LAZY_IMPORTS=<mode>`` environment variable
642-
* The ``sys.set_lazy_imports(mode)`` function (primarily for testing)
653+
* The ``importlib.set_lazy_imports(mode)`` function (primarily for testing)
643654

644655
Where ``<mode>`` can be:
645656

646-
* ``"default"`` (or unset): Only explicitly marked lazy imports are lazy
657+
* ``"normal"`` (or unset): Only explicitly marked lazy imports are lazy
647658

648-
* ``"enabled"``: All module-level imports (except in ``try`` or ``with``
659+
* ``"all"``: All module-level imports (except in ``try`` or ``with``
649660
blocks and ``import *``) become *potentially lazy*
650661

651-
* ``"disabled"``: No imports are lazy, even those explicitly marked with
662+
* ``"none"``: No imports are lazy, even those explicitly marked with
652663
``lazy`` keyword
653664

654-
When the global flag is set to ``"enabled"``, all imports at the global level
665+
When the global flag is set to ``"all"``, all imports at the global level
655666
of all modules are *potentially lazy* **except** for those inside a ``try`` or
656667
``with`` block or any wild card (``from ... import *``) import.
657668

658-
If the global lazy imports flag is set to ``"disabled"``, no *potentially
669+
If the global lazy imports flag is set to ``"none"``, no *potentially
659670
lazy* import is ever imported lazily, the import filter is never called, and
660671
the behavior is equivalent to a regular ``import`` statement: the import is
661672
*eager* (as if the lazy keyword was not used).
662673

663-
Python code can run the :func:`!sys.set_lazy_imports` function to override
674+
Python code can run the :func:`!importlib.set_lazy_imports` function to override
664675
the state of the global lazy imports flag inherited from the environment or CLI.
665676
This is especially useful if an application needs to ensure that all imports
666-
are evaluated eagerly, via ``sys.set_lazy_imports('disabled')``.
677+
are evaluated eagerly, via ``importlib.set_lazy_imports('none')``.
678+
Alternatively, :func:`!importlib.set_lazy_imports` can be used with boolean
679+
values for programmatic control.
667680

668681

669682
Backwards Compatibility
@@ -760,7 +773,7 @@ The `pyperformance suite`_ confirms the implementation is performance-neutral.
760773
Filter function performance
761774
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
762775

763-
The filter function (set via ``sys.set_lazy_imports_filter()``) is called for
776+
The filter function (set via ``importlib.set_lazy_imports_filter()``) is called for
764777
every *potentially lazy* import to determine whether it should actually be
765778
lazy. When no filter is set, this is simply a NULL check (testing whether a
766779
filter function has been registered), which is a highly predictable branch that
@@ -857,9 +870,18 @@ see:
857870
- `Inside HRT's Python Fork
858871
<https://www.hudsonrivertrading.com/hrtbeat/inside-hrts-python-fork/>`__
859872
(Hudson River Trading)
873+
- `Create an On-Demand Initializer for PySide
874+
<https://bugreports.qt.io/browse/PYSIDE-2404>`__
875+
(Qt for Python/PySide) - Christian Tismer's implementation of lazy
876+
initialization for PySide6 based on ideas from :pep:`690`, showing 10-20%
877+
startup time improvement for PySide applications. This demonstrates the
878+
particular value of lazy imports for frameworks with extensive
879+
initialization at import time.
860880

861881
The benefits scale with codebase complexity: the larger and more
862-
interconnected the codebase, the more dramatic the improvements.
882+
interconnected the codebase, the more dramatic the improvements. The
883+
PySide implementation particularly highlights how frameworks with heavy
884+
initialization overhead can benefit significantly from opt-in lazy loading.
863885

864886
Typing and tools
865887
----------------
@@ -874,6 +896,10 @@ Security Implications
874896
=====================
875897

876898
There are no known security vulnerabilities introduced by lazy imports.
899+
Security-sensitive tools that need to ensure all imports are evaluated eagerly
900+
can use :func:`!importlib.set_lazy_imports` with ``enabled=False`` to force
901+
eager evaluation, or use :func:`!importlib.set_lazy_imports_filter` for fine-grained
902+
control.
877903

878904
How to Teach This
879905
=================
@@ -889,6 +915,14 @@ profiling to understand the import time overhead in their codebase and mark
889915
the necessary imports as ``lazy``. In addition, developers can mark imports
890916
that will only be used for type annotations as ``lazy``.
891917

918+
Additional documentation will be added to the Python documentation, including
919+
guidance, a dedicated how-to guide, and updates to the import system
920+
documentation covering: identifying slow-loading modules with profiling tools
921+
(such as ``-X importtime``), migration strategies for existing codebases, best
922+
practices for avoiding common pitfalls with import-time side effects, and
923+
patterns for using lazy imports effectively with type annotations and circular
924+
imports.
925+
892926
Below is guidance on how to best take advantage of lazy imports and how to
893927
avoid incompatibilities:
894928

@@ -1215,14 +1249,14 @@ exclude specific modules that are known to have problematic side effects:
12151249
return False # Import eagerly
12161250
return True # Allow lazy import
12171251
1218-
sys.set_lazy_imports_filter(my_filter)
1252+
importlib.set_lazy_imports_filter(my_filter)
12191253
12201254
The filter function receives the importer module name, the module being
12211255
imported, and the fromlist (if using ``from ... import``). Returning ``False``
12221256
forces an eager import.
12231257

1224-
Alternatively, set the global mode to ``"disabled"`` via ``-X
1225-
lazy_imports=disabled`` to turn off all lazy imports for debugging.
1258+
Alternatively, set the global mode to ``"none"`` via ``-X
1259+
lazy_imports=none`` to turn off all lazy imports for debugging.
12261260

12271261
Can I use lazy imports inside functions?
12281262
----------------------------------------
@@ -1416,11 +1450,105 @@ No, future imports can't be lazy because they're parser/compiler directives.
14161450
It's technically possible for the runtime behavior to be lazy but there's no
14171451
real value in it.
14181452

1419-
Why you chose ``lazy`` as the keyword name?
1420-
-------------------------------------------
1453+
Why did you choose ``lazy`` as the keyword name?
1454+
------------------------------------------------
14211455

14221456
Not "why"... memorize! :)
14231457

1458+
Deferred Ideas
1459+
==============
1460+
1461+
The following ideas have been considered but are deliberately deferred to focus
1462+
on delivering a stable, usable core feature first. These may be considered for
1463+
future enhancements once we have real-world experience with lazy imports.
1464+
1465+
Alternative syntax and ergonomic improvements
1466+
----------------------------------------------
1467+
1468+
Several alternative syntax forms have been suggested to improve ergonomics:
1469+
1470+
* **Type-only imports**: A specialized syntax for imports used exclusively in
1471+
type annotations (similar to the ``type`` keyword in other contexts) could be
1472+
added, such as ``type from collections.abc import Sequence``. This would make
1473+
the intent clearer than using ``lazy`` for type-only imports and would signal
1474+
to readers that the import is never used at runtime. However, since ``lazy``
1475+
imports already solve the runtime cost problem for type annotations, we prefer
1476+
to start with the simpler, more general mechanism and evaluate whether
1477+
specialized syntax adds sufficient value after gathering usage data.
1478+
1479+
* **Block-based syntax**: Grouping multiple lazy imports in a block, such as:
1480+
1481+
.. code-block:: python
1482+
1483+
as lazy:
1484+
import foo
1485+
from bar import baz
1486+
1487+
This could reduce repetition when marking many imports as lazy. However, it
1488+
would require introducing an entirely new statement form (``as lazy:`` blocks)
1489+
that doesn't fit into Python's existing grammar patterns. It's unclear how
1490+
this would interact with other language features or what the precedent would
1491+
be for similar block-level modifiers. This approach also makes it less clear
1492+
when scanning code whether a particular import is lazy, since you must look at
1493+
the surrounding context rather than the import line itself.
1494+
1495+
While these alternatives could provide different ergonomics in certain contexts,
1496+
they share similar drawbacks: they would require introducing new statement
1497+
forms or overloading existing syntax in non-obvious ways, and they open the
1498+
door to many other potential uses of similar syntax patterns that would
1499+
significantly expand the language. We prefer to start with the explicit
1500+
``lazy import`` syntax and gather real-world feedback before considering
1501+
additional syntax variations. Any future ergonomic improvements should be
1502+
evaluated based on actual usage patterns rather than speculative benefits.
1503+
1504+
Automatic lazy imports for ``if TYPE_CHECKING`` blocks
1505+
-------------------------------------------------------
1506+
1507+
A future enhancement could automatically treat all imports inside
1508+
``if TYPE_CHECKING:`` blocks as lazy:
1509+
1510+
.. code-block:: python
1511+
1512+
from typing import TYPE_CHECKING
1513+
1514+
if TYPE_CHECKING:
1515+
from foo import Bar # Could be automatically lazy
1516+
1517+
However, this would require significant changes to make this work at compile
1518+
time, since ``TYPE_CHECKING`` is currently just a runtime variable. The
1519+
compiler would need special knowledge of this pattern, similar to how
1520+
``from __future__ import`` statements are handled. Additionally, making
1521+
``TYPE_CHECKING`` a built-in would be required for this to work reliably.
1522+
Since ``lazy`` imports already solve the runtime cost problem for type-only
1523+
imports, we prefer to start with the explicit syntax and evaluate whether
1524+
this optimization adds sufficient value.
1525+
1526+
Module-level lazy import mode
1527+
------------------------------
1528+
1529+
A module-level declaration to make all imports in that module lazy by default:
1530+
1531+
.. code-block:: python
1532+
1533+
from __future__ import lazy_imports
1534+
import foo # Automatically lazy
1535+
1536+
This was discussed but deferred because it raises several questions. Using
1537+
``from __future__ import`` implies this would become the default behavior in a
1538+
future Python version, which is unclear and not currently planned. It also
1539+
raises questions about how such a mode would interact with the global flag and
1540+
what the transition path would look like. The current explicit syntax and
1541+
``__lazy_modules__`` provide sufficient control for initial adoption.
1542+
1543+
Package metadata for lazy-safe declarations
1544+
--------------------------------------------
1545+
1546+
Future enhancements could allow packages to declare in their metadata whether
1547+
they are safe for lazy importing (e.g., no import-time side effects). This
1548+
could be used by the filter mechanism or by static analysis tools. The current
1549+
filter API is designed to accommodate such future additions without requiring
1550+
changes to the core language specification.
1551+
14241552
Alternate Implementation Ideas
14251553
==============================
14261554

@@ -1560,6 +1688,30 @@ to add more specific re-enabling mechanisms later, when we have a clearer
15601688
picture of real-world use and patterns, than it is to remove a hastily added
15611689
mechanism that isn't quite right.
15621690

1691+
Using a decorator syntax for lazy imports
1692+
------------------------------------------
1693+
1694+
A decorator-based syntax could mark imports as lazy:
1695+
1696+
.. code-block:: python
1697+
1698+
@lazy
1699+
import json
1700+
1701+
@lazy
1702+
from foo import bar
1703+
1704+
This approach was rejected because it introduces too many open questions and
1705+
complications. Decorators in Python are designed to wrap and transform callable
1706+
objects (functions, classes, methods), not statements. Allowing decorators on
1707+
import statements would open the door to many other potential statement
1708+
decorators (``@cached``, ``@traced``, ``@deprecated``, etc.), significantly
1709+
expanding the language's syntax in ways we don't want to explore. Furthermore,
1710+
this raises the question of where such decorators would come from: they would
1711+
need to be either imported or built-in, creating a bootstrapping problem for
1712+
import-related decorators. This is far more speculative and generic than the
1713+
focused ``lazy import`` syntax.
1714+
15631715
Using a context manager instead of a new soft keyword
15641716
-----------------------------------------------------
15651717

0 commit comments

Comments
 (0)