diff --git a/Doc/reference/lexical_analysis.rst b/Doc/reference/lexical_analysis.rst index 9322d8571f7..3665088a933 100644 --- a/Doc/reference/lexical_analysis.rst +++ b/Doc/reference/lexical_analysis.rst @@ -457,6 +457,7 @@ Some names are only reserved under specific contexts. These are known as - ``match``, ``case``, and ``_``, when used in the :keyword:`match` statement. - ``type``, when used in the :keyword:`type` statement. +- ``lazy``, when used before an :keyword:`import` statement. These syntactically act as keywords in their specific contexts, but this distinction is done at the parser level, not when tokenizing. @@ -468,6 +469,9 @@ identifier names. .. versionchanged:: 3.12 ``type`` is now a soft keyword. +.. versionchanged:: next + ``lazy`` is now a soft keyword. + .. index:: single: _, identifiers single: __, identifiers diff --git a/Doc/reference/simple_stmts.rst b/Doc/reference/simple_stmts.rst index 9c022570e7e..7299531d0ec 100644 --- a/Doc/reference/simple_stmts.rst +++ b/Doc/reference/simple_stmts.rst @@ -748,14 +748,15 @@ The :keyword:`!import` statement pair: name; binding pair: keyword; from pair: keyword; as + pair: keyword; lazy pair: exception; ImportError single: , (comma); import statement .. productionlist:: python-grammar - import_stmt: "import" `module` ["as" `identifier`] ("," `module` ["as" `identifier`])* - : | "from" `relative_module` "import" `identifier` ["as" `identifier`] + import_stmt: ["lazy"] "import" `module` ["as" `identifier`] ("," `module` ["as" `identifier`])* + : | ["lazy"] "from" `relative_module` "import" `identifier` ["as" `identifier`] : ("," `identifier` ["as" `identifier`])* - : | "from" `relative_module` "import" "(" `identifier` ["as" `identifier`] + : | ["lazy"] "from" `relative_module` "import" "(" `identifier` ["as" `identifier`] : ("," `identifier` ["as" `identifier`])* [","] ")" : | "from" `relative_module` "import" "*" module: (`identifier` ".")* `identifier` @@ -870,6 +871,57 @@ determine dynamically the modules to be loaded. .. audit-event:: import module,filename,sys.path,sys.meta_path,sys.path_hooks import + +.. _lazy-imports: + +Lazy imports +------------ + +.. index:: + pair: lazy; import + single: lazy import + +When an import statement is preceded by the :keyword:`lazy` keyword, +the import becomes *lazy*: the module is not loaded immediately at the import +statement. Instead, a lazy proxy object is created and bound to the name. The +actual module is loaded on first use of that name. + +.. keyword:: lazy import + + The ``lazy`` keyword marks an import as lazy. It is a :ref:`soft keyword + ` that only has special meaning when it appears immediately + before an :keyword:`import` or :keyword:`from` statement. + +Lazy imports are only permitted at module scope. Using ``lazy`` inside a +function, class body, or :keyword:`try`/:keyword:`except`/:keyword:`finally` +block raises a :exc:`SyntaxError`. Star imports cannot be lazy (``lazy from +module import *`` is a syntax error), and :ref:`future statements ` +cannot be lazy. + +When using ``lazy from ... import``, each imported name is bound to a lazy +proxy object. The first access to any of these names triggers loading of the +entire module and resolves only that specific name to its actual value. Other +names remain as lazy proxies until they are accessed. + +Example:: + + lazy import json + + print('json' in sys.modules) # False - module not loaded yet + + # First use triggers loading + result = json.dumps({"hello": "world"}) + + print('json' in sys.modules) # True - now loaded + +If an error occurs during module loading (such as :exc:`ImportError` or +:exc:`SyntaxError`), it is raised at the point where the lazy import is first +used, not at the import statement itself. + +See :pep:`810` for the full specification of lazy imports. + +.. versionadded:: next + .. _future: Future statements diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index a3cd0afce6a..ffba99248cd 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -139,15 +139,17 @@ runtime. For more selective control, :func:`sys.set_lazy_imports_filter` accepts a callable that determines whether a specific module should be loaded lazily. The filter -receives the fully-qualified module name and returns a boolean. This allows -patterns like making only your own application's modules lazy while keeping -third-party dependencies eager: +receives three arguments: the importing module's name (or ``None``), the imported +module's name, and the fromlist (or ``None`` for regular imports). It should +return ``True`` to allow the import to be lazy, or ``False`` to force eager loading. +This allows patterns like making only your own application's modules lazy while +keeping third-party dependencies eager: .. code-block:: python import sys - sys.set_lazy_imports_filter(lambda name: name.startswith("myapp.")) + sys.set_lazy_imports_filter(lambda importing, imported, fromlist: imported.startswith("myapp.")) sys.set_lazy_imports("all") import myapp.slow_module # lazy (matches filter)