From 85106f25e2a46b431ccb4fcd142ab58ebec6dbf6 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 15 May 2026 18:45:13 +0300 Subject: [PATCH 1/4] gh-142349: Add `help("lazy")` support --- Doc/tools/extensions/pydoc_topics.py | 1 + Lib/pydoc.py | 1 + Lib/pydoc_data/module_docs.py | 2 +- Lib/pydoc_data/topics.py | 106 ++++++++++++++++-- ...-05-15-18-44-20.gh-issue-142349.fHK3v1.rst | 1 + 5 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-15-18-44-20.gh-issue-142349.fHK3v1.rst diff --git a/Doc/tools/extensions/pydoc_topics.py b/Doc/tools/extensions/pydoc_topics.py index a65d77433b255b..35878e2d1e43e9 100644 --- a/Doc/tools/extensions/pydoc_topics.py +++ b/Doc/tools/extensions/pydoc_topics.py @@ -68,6 +68,7 @@ "in", "integers", "lambda", + "lazy", "lists", "naming", "nonlocal", diff --git a/Lib/pydoc.py b/Lib/pydoc.py index a1a6aad434ddf4..497cc7d90a4245 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1845,6 +1845,7 @@ class Helper: 'in': ('in', 'SEQUENCEMETHODS'), 'is': 'COMPARISON', 'lambda': ('lambda', 'FUNCTIONS'), + 'lazy': ('lazy', 'MODULES'), 'nonlocal': ('nonlocal', 'global NAMESPACES'), 'not': 'BOOLEAN', 'or': 'BOOLEAN', diff --git a/Lib/pydoc_data/module_docs.py b/Lib/pydoc_data/module_docs.py index 1a3126d3db9590..40fe997a133ce9 100644 --- a/Lib/pydoc_data/module_docs.py +++ b/Lib/pydoc_data/module_docs.py @@ -1,4 +1,4 @@ -# Autogenerated by Sphinx on Thu May 7 16:26:23 2026 +# Autogenerated by Sphinx on Fri May 15 18:47:55 2026 # as part of the release process. module_docs = { diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index 5f61001c46b79c..79b81fa7343f31 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -1,4 +1,4 @@ -# Autogenerated by Sphinx on Thu May 7 16:26:23 2026 +# Autogenerated by Sphinx on Fri May 15 18:47:55 2026 # as part of the release process. topics = { @@ -2344,8 +2344,8 @@ def foo(): The match statement is used for pattern matching. Syntax: match_stmt: 'match' subject_expr ":" NEWLINE INDENT case_block+ DEDENT - subject_expr: `!star_named_expression` "," `!star_named_expressions`? - | `!named_expression` + subject_expr: flexible_expression "," [flexible_expression_list [',']] + | assignment_expression case_block: 'case' patterns [guard] ":" `!block` Note: @@ -2437,7 +2437,7 @@ def foo(): Guards ------ - guard: "if" `!named_expression` + guard: "if" assignment_expression A "guard" (which is part of the "case") must succeed for code inside the "case" block to execute. It takes the form: "if" followed by an @@ -7231,6 +7231,94 @@ def (parameters): See section Function definitions for the syntax of parameter lists. Note that functions created with lambda expressions cannot contain statements or annotations. +''', + 'lazy': r'''Lazy imports +************ + +The "lazy" keyword is a soft keyword that only has special meaning +when it appears immediately before an "import" or "from" statement. +When an import statement is preceded by the "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. + +Lazy imports are only permitted at module scope. Using "lazy" inside a +function, class body, or "try"/"except"/"finally" block raises a +"SyntaxError". Star imports cannot be lazy ("lazy from module import +*" is a syntax error), and 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 + import sys + + print('json' in sys.modules) # False - json module not yet loaded + + # 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 "ImportError" or +"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. + +Added in version 3.15. + + +Compatibility via "__lazy_modules__" +==================================== + +As an alternative to using the "lazy" keyword, a module can opt into +lazy loading for specific imports by defining a module-level +"__lazy_modules__" variable. When present, it must be a container of +fully qualified module name strings. Any regular (non-"lazy") +"import" statement at module scope whose target appears in +"__lazy_modules__" is treated as a lazy import, exactly as if the +"lazy" keyword had been used. + +This provides a way to enable lazy loading for specific dependencies +without changing individual "import" statements. This is useful when +supporting Python versions older than 3.15 while using lazy imports in +3.15+: + + __lazy_modules__ = ["json", "pathlib"] + + import json # loaded lazily (name is in __lazy_modules__) + import os # loaded eagerly (name not in __lazy_modules__) + + import pathlib # loaded lazily + +Relative imports are resolved to their absolute name before the +lookup, so "__lazy_modules__" must always contain fully qualified +module names. + +For "from"-style imports, the relevant name is the module following +"from", not the names of its members: + + # In mypackage/mymodule.py + __lazy_modules__ = ["mypackage", "mypackage.sub.utils"] + + from . import helper # loaded lazily: . resolves to mypackage + from .sub.utils import func # loaded lazily: .sub.utils resolves to mypackage.sub.utils + import json # loaded eagerly (not in __lazy_modules__) + +Imports inside functions, class bodies, or "try"/"except"/"finally" +blocks are always eager, regardless of "__lazy_modules__". + +Setting "-X lazy_imports=none" (or the "PYTHON_LAZY_IMPORTS" +environment variable to "none") overrides "__lazy_modules__" and +forces all imports to be eager. + +Added in version 3.15. ''', 'lists': r'''List displays ************* @@ -12863,11 +12951,11 @@ def foo(): modules created dynamically using the "types.ModuleType" constructor. Previously the attribute was optional. - Deprecated since version 3.12, will be removed in version 3.16: - Setting "__loader__" on a module while failing to set - "__spec__.loader" is deprecated. In Python 3.16, "__loader__" will - cease to be set or taken into consideration by the import system or - the standard library. + Deprecated since version 3.12, removed in version 3.16: Setting + "__loader__" on a module while failing to set "__spec__.loader" is + deprecated. In Python 3.16, "__loader__" will cease to be set or + taken into consideration by the import system or the standard + library. module.__path__ diff --git a/Misc/NEWS.d/next/Library/2026-05-15-18-44-20.gh-issue-142349.fHK3v1.rst b/Misc/NEWS.d/next/Library/2026-05-15-18-44-20.gh-issue-142349.fHK3v1.rst new file mode 100644 index 00000000000000..fa667c4110941e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-15-18-44-20.gh-issue-142349.fHK3v1.rst @@ -0,0 +1 @@ +Add :keyword:`lazy` to the list of support topic by :func:`help`. From a6992321b1383e1400aa5ccd3a7390ade6f0b9b8 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 15 May 2026 18:49:21 +0300 Subject: [PATCH 2/4] Keep `Lib/pydoc_data/module_docs.py` as is --- Lib/pydoc_data/module_docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pydoc_data/module_docs.py b/Lib/pydoc_data/module_docs.py index 40fe997a133ce9..1a3126d3db9590 100644 --- a/Lib/pydoc_data/module_docs.py +++ b/Lib/pydoc_data/module_docs.py @@ -1,4 +1,4 @@ -# Autogenerated by Sphinx on Fri May 15 18:47:55 2026 +# Autogenerated by Sphinx on Thu May 7 16:26:23 2026 # as part of the release process. module_docs = { From 1ab9c5a36c327baef87b7592f3e2350e8c299854 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 15 May 2026 18:51:18 +0300 Subject: [PATCH 3/4] Revert `Lib/pydoc_data/topics.py` changes --- Lib/pydoc_data/topics.py | 106 ++++----------------------------------- 1 file changed, 9 insertions(+), 97 deletions(-) diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index 79b81fa7343f31..5f61001c46b79c 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -1,4 +1,4 @@ -# Autogenerated by Sphinx on Fri May 15 18:47:55 2026 +# Autogenerated by Sphinx on Thu May 7 16:26:23 2026 # as part of the release process. topics = { @@ -2344,8 +2344,8 @@ def foo(): The match statement is used for pattern matching. Syntax: match_stmt: 'match' subject_expr ":" NEWLINE INDENT case_block+ DEDENT - subject_expr: flexible_expression "," [flexible_expression_list [',']] - | assignment_expression + subject_expr: `!star_named_expression` "," `!star_named_expressions`? + | `!named_expression` case_block: 'case' patterns [guard] ":" `!block` Note: @@ -2437,7 +2437,7 @@ def foo(): Guards ------ - guard: "if" assignment_expression + guard: "if" `!named_expression` A "guard" (which is part of the "case") must succeed for code inside the "case" block to execute. It takes the form: "if" followed by an @@ -7231,94 +7231,6 @@ def (parameters): See section Function definitions for the syntax of parameter lists. Note that functions created with lambda expressions cannot contain statements or annotations. -''', - 'lazy': r'''Lazy imports -************ - -The "lazy" keyword is a soft keyword that only has special meaning -when it appears immediately before an "import" or "from" statement. -When an import statement is preceded by the "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. - -Lazy imports are only permitted at module scope. Using "lazy" inside a -function, class body, or "try"/"except"/"finally" block raises a -"SyntaxError". Star imports cannot be lazy ("lazy from module import -*" is a syntax error), and 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 - import sys - - print('json' in sys.modules) # False - json module not yet loaded - - # 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 "ImportError" or -"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. - -Added in version 3.15. - - -Compatibility via "__lazy_modules__" -==================================== - -As an alternative to using the "lazy" keyword, a module can opt into -lazy loading for specific imports by defining a module-level -"__lazy_modules__" variable. When present, it must be a container of -fully qualified module name strings. Any regular (non-"lazy") -"import" statement at module scope whose target appears in -"__lazy_modules__" is treated as a lazy import, exactly as if the -"lazy" keyword had been used. - -This provides a way to enable lazy loading for specific dependencies -without changing individual "import" statements. This is useful when -supporting Python versions older than 3.15 while using lazy imports in -3.15+: - - __lazy_modules__ = ["json", "pathlib"] - - import json # loaded lazily (name is in __lazy_modules__) - import os # loaded eagerly (name not in __lazy_modules__) - - import pathlib # loaded lazily - -Relative imports are resolved to their absolute name before the -lookup, so "__lazy_modules__" must always contain fully qualified -module names. - -For "from"-style imports, the relevant name is the module following -"from", not the names of its members: - - # In mypackage/mymodule.py - __lazy_modules__ = ["mypackage", "mypackage.sub.utils"] - - from . import helper # loaded lazily: . resolves to mypackage - from .sub.utils import func # loaded lazily: .sub.utils resolves to mypackage.sub.utils - import json # loaded eagerly (not in __lazy_modules__) - -Imports inside functions, class bodies, or "try"/"except"/"finally" -blocks are always eager, regardless of "__lazy_modules__". - -Setting "-X lazy_imports=none" (or the "PYTHON_LAZY_IMPORTS" -environment variable to "none") overrides "__lazy_modules__" and -forces all imports to be eager. - -Added in version 3.15. ''', 'lists': r'''List displays ************* @@ -12951,11 +12863,11 @@ def foo(): modules created dynamically using the "types.ModuleType" constructor. Previously the attribute was optional. - Deprecated since version 3.12, removed in version 3.16: Setting - "__loader__" on a module while failing to set "__spec__.loader" is - deprecated. In Python 3.16, "__loader__" will cease to be set or - taken into consideration by the import system or the standard - library. + Deprecated since version 3.12, will be removed in version 3.16: + Setting "__loader__" on a module while failing to set + "__spec__.loader" is deprecated. In Python 3.16, "__loader__" will + cease to be set or taken into consideration by the import system or + the standard library. module.__path__ From 088f4dd8f8df9d26ce8a52d319faa9f96e3d3e34 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 15 May 2026 19:00:50 +0300 Subject: [PATCH 4/4] Fix tests --- Lib/test/test_pydoc/test_pydoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 2e190d1b81be8e..5cd26923f75c31 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -2172,7 +2172,7 @@ def mock_getline(prompt): def test_keywords(self): self.assertEqual(sorted(pydoc.Helper.keywords), - sorted(keyword.kwlist)) + sorted(keyword.kwlist + ['lazy'])) def test_interact_empty_line_continues(self): # gh-138568: test pressing Enter without input should continue in help session