Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions Doc/c-api/import.rst
Original file line number Diff line number Diff line change
Expand Up @@ -393,11 +393,6 @@ Importing Modules

Make all imports lazy by default.

.. c:enumerator:: PyImport_LAZY_NONE

Disable lazy imports entirely. Even explicit ``lazy`` statements become
eager imports.

.. versionadded:: 3.15

.. c:function:: PyObject* PyImport_CreateModuleFromInitfunc(PyObject *spec, PyObject* (*initfunc)(void))
Expand Down
4 changes: 0 additions & 4 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -919,8 +919,6 @@ always available. Unless explicitly noted otherwise, all variables are read-only
* ``"normal"``: Only imports explicitly marked with the ``lazy`` keyword
are lazy
* ``"all"``: All top-level imports are potentially lazy
* ``"none"``: All lazy imports are suppressed (even explicitly marked
ones)

See also :func:`set_lazy_imports` and :pep:`810`.

Expand Down Expand Up @@ -1757,8 +1755,6 @@ always available. Unless explicitly noted otherwise, all variables are read-only
* ``"normal"``: Only imports explicitly marked with the ``lazy`` keyword
are lazy
* ``"all"``: All top-level imports become potentially lazy
* ``"none"``: All lazy imports are suppressed (even explicitly marked
ones)

This function is intended for advanced users who need to control lazy
imports across their entire application. Library developers should
Expand Down
4 changes: 0 additions & 4 deletions Doc/reference/simple_stmts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -965,10 +965,6 @@ Imports inside functions, class bodies, or
:keyword:`try`/:keyword:`except`/:keyword:`finally` blocks are always eager,
regardless of :attr:`!__lazy_modules__`.

Setting ``-X lazy_imports=none`` (or the :envvar:`PYTHON_LAZY_IMPORTS`
environment variable to ``none``) overrides :attr:`!__lazy_modules__` and
forces all imports to be eager.

.. versionadded:: 3.15

.. _future:
Expand Down
2 changes: 2 additions & 0 deletions Doc/tools/removed-ids.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ c-api/allocation.html: deprecated-aliases
c-api/file.html: deprecated-api

library/asyncio-task.html: terminating-a-task-group

c-api/import.html: c.PyImport_LazyImportsMode.PyImport_LAZY_NONE
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please have a "Removed APIs" section, otherwise this list will be quite messy?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
c-api/import.html: c.PyImport_LazyImportsMode.PyImport_LAZY_NONE
# Removed APIs
c-api/import.html: c.PyImport_LazyImportsMode.PyImport_LAZY_NONE

14 changes: 6 additions & 8 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -705,10 +705,9 @@ Miscellaneous options

.. versionadded:: 3.14

* :samp:`-X lazy_imports={all,none,normal}` controls lazy import behavior.
``all`` makes all imports lazy by default, ``none`` disables lazy imports
entirely (even explicit ``lazy`` statements become eager), and ``normal``
(the default) respects the ``lazy`` keyword in source code.
* :samp:`-X lazy_imports={all,normal}` controls lazy import behavior.
``all`` makes all imports lazy by default, and ``normal`` (the default)
respects the ``lazy`` keyword in source code.
See also :envvar:`PYTHON_LAZY_IMPORTS`.

.. versionadded:: 3.15
Expand Down Expand Up @@ -1416,10 +1415,9 @@ conflict.

.. envvar:: PYTHON_LAZY_IMPORTS

Controls lazy import behavior. Accepts three values: ``all`` makes all
imports lazy by default, ``none`` disables lazy imports entirely (even
explicit ``lazy`` statements become eager), and ``normal`` (the default)
respects the ``lazy`` keyword in source code.
Controls lazy import behavior. Accepts two values: ``all`` makes all
imports lazy by default, and ``normal`` (the default) respects the
``lazy`` keyword in source code.

See also the :option:`-X lazy_imports <-X>` command-line option.

Expand Down
9 changes: 4 additions & 5 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,10 @@ making it straightforward to diagnose and debug the failure.
For cases where you want to enable lazy loading globally without modifying
source code, Python provides the :option:`-X lazy_imports <-X>` command-line
option and the :envvar:`PYTHON_LAZY_IMPORTS` environment variable. Both
accept three values: ``all`` makes all imports lazy by default, ``none``
disables lazy imports entirely (even explicit ``lazy`` statements become
eager), and ``normal`` (the default) respects the ``lazy`` keyword in source
code. The :func:`sys.set_lazy_imports` and :func:`sys.get_lazy_imports`
functions allow changing and querying this mode at runtime.
accept two values: ``all`` makes all imports lazy by default, and ``normal``
(the default) respects the ``lazy`` keyword in source code. The
:func:`sys.set_lazy_imports` and :func:`sys.get_lazy_imports` functions allow
changing and querying this mode at runtime.

For more selective control, :func:`sys.set_lazy_imports_filter` accepts a
callable that determines whether a specific module should be loaded lazily.
Expand Down
3 changes: 1 addition & 2 deletions Include/import.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ PyAPI_FUNC(int) PyImport_AppendInittab(

typedef enum {
PyImport_LAZY_NORMAL,
PyImport_LAZY_ALL,
PyImport_LAZY_NONE
PyImport_LAZY_ALL
} PyImport_LazyImportsMode;

#ifndef Py_LIMITED_API
Expand Down
152 changes: 24 additions & 128 deletions Lib/test/test_lazy_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ def tearDown(self):
sys.set_lazy_imports_filter(None)
sys.set_lazy_imports("normal")

def test_global_off(self):
"""Mode 'none' should disable lazy imports entirely."""
import test.test_lazy_import.data.global_off
self.assertIn("test.test_lazy_import.data.basic2", sys.modules)
def test_global_off_rejected(self):
"""Mode 'none' is not supported."""
with self.assertRaises(ValueError):
sys.set_lazy_imports("none")

def test_global_on(self):
"""Mode 'all' should make regular imports lazy."""
Expand Down Expand Up @@ -468,9 +468,6 @@ def test_get_lazy_imports_returns_string(self):
sys.set_lazy_imports("all")
self.assertEqual(sys.get_lazy_imports(), "all")

sys.set_lazy_imports("none")
self.assertEqual(sys.get_lazy_imports(), "none")

def test_get_lazy_imports_filter_default(self):
"""get_lazy_imports_filter should return None by default."""
sys.set_lazy_imports_filter(None)
Expand Down Expand Up @@ -1015,68 +1012,16 @@ def test_cli_lazy_imports_all_makes_regular_imports_lazy(self):
self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
self.assertIn("LAZY", result.stdout)

def test_cli_lazy_imports_none_forces_all_imports_eager(self):
"""-X lazy_imports=none should force all imports to be eager."""
code = textwrap.dedent("""
import sys
# Even explicit lazy imports should be eager in 'none' mode
lazy import json
if 'json' in sys.modules:
print("EAGER")
else:
print("LAZY")
""")
def test_cli_lazy_imports_none_is_rejected(self):
"""-X lazy_imports=none should be rejected."""
result = subprocess.run(
[sys.executable, "-X", "lazy_imports=none", "-c", code],
[sys.executable, "-X", "lazy_imports=none", "-c", "pass"],
capture_output=True,
text=True
)
self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
self.assertIn("EAGER", result.stdout)

@support.requires_resource("cpu")
def test_cli_lazy_imports_modes_import_stdlib_modules(self):
"""-X lazy_imports modes should import available stdlib modules."""
# Do not smoke-test modules with intentional import-time effects.
import_side_effect_modules = {"antigravity", "this"}
importable = []

for module in sorted(sys.stdlib_module_names):
if module in import_side_effect_modules:
continue

with self.subTest(module=module):
code = f"import {module}; print({module})"
baseline = subprocess.run(
[sys.executable, "-I", "-c", code],
capture_output=True,
text=True,
timeout=60,
)
if baseline.returncode:
# sys.stdlib_module_names includes modules for other
# platforms and optional extension modules not built here.
continue
importable.append(module)

for mode in ("normal", "none"):
with self.subTest(module=module, mode=mode):
result = subprocess.run(
[
sys.executable,
"-I",
"-X",
f"lazy_imports={mode}",
"-c",
code,
],
capture_output=True,
text=True,
timeout=60,
)
self.assertEqual(result.returncode, 0, result.stderr)

self.assertGreater(len(importable), 100)
self.assertNotEqual(result.returncode, 0)
self.assertIn("-X lazy_imports: invalid value", result.stderr)
self.assertIn("expected 'all' or 'normal'", result.stderr)

def test_cli_lazy_imports_normal_respects_lazy_keyword_only(self):
"""-X lazy_imports=normal should respect lazy keyword only."""
Expand Down Expand Up @@ -1125,101 +1070,51 @@ def test_env_var_lazy_imports_all_enables_global_lazy(self):
self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
self.assertIn("LAZY", result.stdout)

def test_env_var_lazy_imports_none_disables_all_lazy(self):
"""PYTHON_LAZY_IMPORTS=none should disable all lazy imports."""
code = textwrap.dedent("""
import sys
lazy import json
if 'json' in sys.modules:
print("EAGER")
else:
print("LAZY")
""")
def test_env_var_lazy_imports_none_is_rejected(self):
"""PYTHON_LAZY_IMPORTS=none should be rejected."""
import os
env = os.environ.copy()
env["PYTHON_LAZY_IMPORTS"] = "none"
result = subprocess.run(
[sys.executable, "-c", code],
[sys.executable, "-c", "pass"],
capture_output=True,
text=True,
env=env
)
self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
self.assertIn("EAGER", result.stdout)

def test_cli_lazy_imports_none_disables_dunder_lazy_modules(self):
"""-X lazy_imports=none should override __lazy_modules__."""
code = textwrap.dedent("""
import sys
__lazy_modules__ = ["json"]
import json
if 'json' in sys.modules:
print("EAGER")
else:
print("LAZY")
""")
result = subprocess.run(
[sys.executable, "-X", "lazy_imports=none", "-c", code],
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
self.assertIn("EAGER", result.stdout)

def test_env_var_lazy_imports_none_disables_dunder_lazy_modules(self):
"""PYTHON_LAZY_IMPORTS=none should override __lazy_modules__."""
code = textwrap.dedent("""
import sys
__lazy_modules__ = ["json"]
import json
if 'json' in sys.modules:
print("EAGER")
else:
print("LAZY")
""")
import os

env = os.environ.copy()
env["PYTHON_LAZY_IMPORTS"] = "none"
result = subprocess.run(
[sys.executable, "-c", code],
capture_output=True,
text=True,
env=env,
)
self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
self.assertIn("EAGER", result.stdout)
self.assertNotEqual(result.returncode, 0)
self.assertIn("PYTHON_LAZY_IMPORTS: invalid value", result.stderr)
self.assertIn("expected 'all' or 'normal'", result.stderr)

def test_cli_overrides_env_var(self):
"""Command-line option should take precedence over environment variable."""
# PEP 810: -X lazy_imports takes precedence over PYTHON_LAZY_IMPORTS
code = textwrap.dedent("""
import sys
lazy import json
import json
if 'json' in sys.modules:
print("EAGER")
else:
print("LAZY")
""")
import os
env = os.environ.copy()
env["PYTHON_LAZY_IMPORTS"] = "all" # env says all
env["PYTHON_LAZY_IMPORTS"] = "all" # env says all imports are lazy
result = subprocess.run(
[sys.executable, "-X", "lazy_imports=none", "-c", code], # CLI says none
[sys.executable, "-X", "lazy_imports=normal", "-c", code],
capture_output=True,
text=True,
env=env
)
self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
# CLI should win - imports should be eager
# CLI should win, so a regular import should stay eager.
self.assertIn("EAGER", result.stdout)

def test_sys_set_lazy_imports_overrides_cli(self):
"""sys.set_lazy_imports() should take precedence over CLI option."""
code = textwrap.dedent("""
import sys
sys.set_lazy_imports("none") # Override CLI
lazy import json
sys.set_lazy_imports("normal") # Override CLI
import json
if 'json' in sys.modules:
print("EAGER")
else:
Expand Down Expand Up @@ -2001,9 +1896,10 @@ def tearDown(self):

def test_set_matches_sys(self):
self.assertEqual(_testcapi.PyImport_GetLazyImportsMode(), sys.get_lazy_imports())
for mode in ("normal", "all", "none"):
for mode in ("normal", "all"):
_testcapi.PyImport_SetLazyImportsMode(mode)
self.assertEqual(_testcapi.PyImport_GetLazyImportsMode(), sys.get_lazy_imports())
self.assertRaises(ValueError, _testcapi.PyImport_SetLazyImportsMode, "none")

def test_filter_matches_sys(self):
self.assertEqual(_testcapi.PyImport_GetLazyImportsFilter(), sys.get_lazy_imports_filter())
Expand Down
5 changes: 0 additions & 5 deletions Lib/test/test_lazy_import/data/global_off.py

This file was deleted.

10 changes: 0 additions & 10 deletions Misc/NEWS.d/3.15.0a8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,6 @@ dealing with contradictions in ``make_bottom``.

..
.. date: 2026-03-24-13-06-52
.. gh-issue: 146369
.. nonce: 6wDI6S
.. section: Core and Builtins
Ensure ``-X lazy_imports=none`` and ``PYTHON_LAZY_IMPORTS=none`` override
:attr:`~module.__lazy_modules__`. Patch by Hugo van Kemenade.

..
.. date: 2026-03-22-19-30-00
.. gh-issue: 146308
.. nonce: AxnRVA
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Do not support ``none`` as a lazy imports mode.
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
Fix import cycles exposed by running standard library modules with
``-X lazy_imports=none``.
Fix import cycles exposed when lazy imports are globally disabled.
4 changes: 0 additions & 4 deletions Modules/_testcapi/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ pyimport_setlazyimportsmode(PyObject *self, PyObject *args)
PyImport_SetLazyImportsMode(PyImport_LAZY_NORMAL);
} else if (strcmp(PyUnicode_AsUTF8(mode), "all") == 0) {
PyImport_SetLazyImportsMode(PyImport_LAZY_ALL);
} else if (strcmp(PyUnicode_AsUTF8(mode), "none") == 0) {
PyImport_SetLazyImportsMode(PyImport_LAZY_NONE);
} else {
PyErr_SetString(PyExc_ValueError, "invalid mode");
return NULL;
Expand All @@ -59,8 +57,6 @@ pyimport_getlazyimportsmode(PyObject *self, PyObject *args)
return PyUnicode_FromString("normal");
case PyImport_LAZY_ALL:
return PyUnicode_FromString("all");
case PyImport_LAZY_NONE:
return PyUnicode_FromString("none");
default:
PyErr_SetString(PyExc_ValueError, "unknown mode");
return NULL;
Expand Down
5 changes: 1 addition & 4 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -3067,17 +3067,14 @@ _PyEval_LazyImportName(PyThreadState *tstate, PyObject *builtins,
PyObject *res = NULL;
// Check if global policy overrides the local syntax
switch (PyImport_GetLazyImportsMode()) {
case PyImport_LAZY_NONE:
lazy = 0;
break;
case PyImport_LAZY_ALL:
lazy = 1;
break;
case PyImport_LAZY_NORMAL:
break;
}

if (!lazy && PyImport_GetLazyImportsMode() != PyImport_LAZY_NONE) {
if (!lazy) {
// See if __lazy_modules__ forces this to be lazy.
lazy = check_lazy_import_compatibility(tstate, globals, name, level);
if (lazy < 0) {
Expand Down
Loading
Loading