Skip to content

Commit f98c4ef

Browse files
committed
PEP 810: Updates based on feedback
1 parent 3768b70 commit f98c4ef

File tree

1 file changed

+170
-25
lines changed

1 file changed

+170
-25
lines changed

peps/pep-0810.rst

Lines changed: 170 additions & 25 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

@@ -362,10 +366,10 @@ Reification
362366

363367
When a lazy object is first used, it needs to be reified. This means resolving
364368
the import at that point in the program and replacing the lazy object with the
365-
concrete one. Reification imports the module in the same way as it would have
366-
been if it had been imported eagerly, barring intervening changes to the
367-
import system (e.g. to ``sys.path``, ``sys.meta_path``, ``sys.path_hooks`` or
368-
``__import__``).
369+
concrete one. Lazy modules are imported using the import system at the time of
370+
reification. The value of ``__import__`` and all import system state (e.g.,
371+
``sys.path``, ``sys.meta_path``, ``sys.path_hooks``) are resolved at
372+
reification time, not when the lazy import statement is executed.
369373

370374
Reification still calls ``__import__`` to resolve the import. When the module
371375
is first reified, it's removed from ``sys.lazy_modules`` (even if there are
@@ -565,15 +569,19 @@ After several calls, ``LOAD_GLOBAL`` specializes to ``LOAD_GLOBAL_MODULE``:
565569
Lazy imports filter
566570
-------------------
567571

568-
This PEP adds two new functions to the ``sys`` module to manage the lazy
569-
imports filter:
572+
This PEP adds the following new functions to manage the lazy imports filter:
570573

571-
* ``sys.set_lazy_imports_filter(func)`` - Sets the filter function. The
574+
* ``importlib.set_lazy_imports_filter(func)`` - Sets the filter function. The
572575
``func`` parameter must have the signature: ``func(importer: str, name: str,
573576
fromlist: tuple[str, ...] | None) -> bool``
574577

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

578586
The filter function is called for every potentially lazy import, and must
579587
return ``True`` if the import should be lazy. This allows for fine-grained
@@ -618,7 +626,7 @@ Example:
618626
return True # Allow lazy import
619627
620628
# Install the filter
621-
sys.set_lazy_imports_filter(exclude_side_effect_modules)
629+
importlib.set_lazy_imports_filter(exclude_side_effect_modules)
622630
623631
# These imports are checked by the filter
624632
lazy import data_processor # Filter returns True -> stays lazy
@@ -642,27 +650,29 @@ The global lazy imports flag can be controlled through:
642650

643651
Where ``<mode>`` can be:
644652

645-
* ``"default"`` (or unset): Only explicitly marked lazy imports are lazy
653+
* ``"normal"`` (or unset): Only explicitly marked lazy imports are lazy
646654

647-
* ``"enabled"``: All module-level imports (except in ``try`` or ``with``
655+
* ``"all"``: All module-level imports (except in ``try`` or ``with``
648656
blocks and ``import *``) become *potentially lazy*
649657

650-
* ``"disabled"``: No imports are lazy, even those explicitly marked with
658+
* ``"none"``: No imports are lazy, even those explicitly marked with
651659
``lazy`` keyword
652660

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

657-
If the global lazy imports flag is set to ``"disabled"``, no *potentially
665+
If the global lazy imports flag is set to ``"none"``, no *potentially
658666
lazy* import is ever imported lazily, the import filter is never called, and
659667
the behavior is equivalent to a regular ``import`` statement: the import is
660668
*eager* (as if the lazy keyword was not used).
661669

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

667677

668678
Backwards Compatibility
@@ -759,7 +769,7 @@ The `pyperformance suite`_ confirms the implementation is performance-neutral.
759769
Filter function performance
760770
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
761771

762-
The filter function (set via ``sys.set_lazy_imports_filter()``) is called for
772+
The filter function (set via ``importlib.set_lazy_imports_filter()``) is called for
763773
every *potentially lazy* import to determine whether it should actually be
764774
lazy. When no filter is set, this is simply a NULL check (testing whether a
765775
filter function has been registered), which is a highly predictable branch that
@@ -873,6 +883,10 @@ Security Implications
873883
=====================
874884

875885
There are no known security vulnerabilities introduced by lazy imports.
886+
Security-sensitive tools that need to ensure all imports are evaluated eagerly
887+
can use :func:`!importlib.set_lazy_imports` with ``enabled=False`` to force
888+
eager evaluation, or use :func:`!importlib.set_lazy_imports_filter` for fine-grained
889+
control.
876890

877891
How to Teach This
878892
=================
@@ -888,6 +902,14 @@ profiling to understand the import time overhead in their codebase and mark
888902
the necessary imports as ``lazy``. In addition, developers can mark imports
889903
that will only be used for type annotations as ``lazy``.
890904

905+
Additional documentation will be added to the Python documentation, including
906+
guidance, a dedicated how-to guide, and updates to the import system
907+
documentation covering: identifying slow-loading modules with profiling tools
908+
(such as ``-X importtime``), migration strategies for existing codebases, best
909+
practices for avoiding common pitfalls with import-time side effects, and
910+
patterns for using lazy imports effectively with type annotations and circular
911+
imports.
912+
891913
Below is guidance on how to best take advantage of lazy imports and how to
892914
avoid incompatibilities:
893915

@@ -1214,14 +1236,14 @@ exclude specific modules that are known to have problematic side effects:
12141236
return False # Import eagerly
12151237
return True # Allow lazy import
12161238
1217-
sys.set_lazy_imports_filter(my_filter)
1239+
importlib.set_lazy_imports_filter(my_filter)
12181240
12191241
The filter function receives the importer module name, the module being
12201242
imported, and the fromlist (if using ``from ... import``). Returning ``False``
12211243
forces an eager import.
12221244

1223-
Alternatively, set the global mode to ``"disabled"`` via ``-X
1224-
lazy_imports=disabled`` to turn off all lazy imports for debugging.
1245+
Alternatively, set the global mode to ``"none"`` via ``-X
1246+
lazy_imports=none`` to turn off all lazy imports for debugging.
12251247

12261248
Can I use lazy imports inside functions?
12271249
----------------------------------------
@@ -1408,18 +1430,117 @@ any eager import.
14081430
14091431
print('json' in sys.modules) # True
14101432
1433+
Note that the import system state (including ``sys.path`` and ``__import__``)
1434+
is evaluated at reification time, not when the lazy import statement executes.
1435+
This means that modifications to the import system between the lazy import
1436+
statement and first use will affect how the module is resolved.
1437+
14111438
Does ``lazy from __future__ import feature`` work?
14121439
--------------------------------------------------
14131440

14141441
No, future imports can't be lazy because they're parser/compiler directives.
14151442
It's technically possible for the runtime behavior to be lazy but there's no
14161443
real value in it.
14171444

1418-
Why you chose ``lazy`` as the keyword name?
1419-
-------------------------------------------
1445+
Why did you choose ``lazy`` as the keyword name?
1446+
------------------------------------------------
14201447

14211448
Not "why"... memorize! :)
14221449

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

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

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

0 commit comments

Comments
 (0)