https://github.com/python/cpython/commit/8cad740557f3a0860497bde5b02ad9c2b8ea9cf6
commit: 8cad740557f3a0860497bde5b02ad9c2b8ea9cf6
branch: main
author: David Ellis <[email protected]>
committer: savannahostrowski <[email protected]>
date: 2026-05-06T18:24:18Z
summary:

gh-148823: Avoid importing `_colorize` when creating an `ArgumentParser` 
(#148827)

Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Savannah Ostrowski <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-05-06-14-26-37.gh-issue-148823.ySmOE4.rst
M Lib/argparse.py
M Lib/test/support/import_helper.py
M Lib/test/test_argparse.py

diff --git a/Lib/argparse.py b/Lib/argparse.py
index 9bc3ea64431e52..6d21823e652429 100644
--- a/Lib/argparse.py
+++ b/Lib/argparse.py
@@ -92,6 +92,8 @@
 from gettext import gettext as _
 from gettext import ngettext
 
+lazy import _colorize
+
 SUPPRESS = '==SUPPRESS=='
 
 OPTIONAL = '?'
@@ -156,6 +158,15 @@ def _identity(value):
 # Formatting Help
 # ===============
 
+class _ColorlessTheme:
+    # A 'fake' theme for no colors
+    def __getattr__(self, name):
+        # _colorize's no_color themes are just all empty strings
+        # by directly using empty strings the import is avoided
+        return ""
+
+_colorless_theme = _ColorlessTheme()
+
 
 class HelpFormatter(object):
     """Formatter for generating usage messages and argument help strings.
@@ -196,14 +207,32 @@ def __init__(
         self._set_color(False)
 
     def _set_color(self, color, *, file=None):
-        from _colorize import can_colorize, decolor, get_theme
-
-        if color and can_colorize(file=file):
-            self._theme = get_theme(force_color=True).argparse
-            self._decolor = decolor
+        # Set a new color setting and file, clear caches for theme and decolor
+        self._theme_color = color
+        self._theme_file = file
+        self._cached_theme = None
+        self._cached_decolor = None
+
+    def _get_theme_and_decolor(self):
+        # If self._theme_color is false, this prevents _colorize from importing
+        if self._theme_color and _colorize.can_colorize(file=self._theme_file):
+            self._cached_theme = _colorize.get_theme(force_color=True).argparse
+            self._cached_decolor = _colorize.decolor
         else:
-            self._theme = get_theme(force_no_color=True).argparse
-            self._decolor = _identity
+            self._cached_theme = _colorless_theme
+            self._cached_decolor = _identity
+
+    @property
+    def _theme(self):
+        if self._cached_theme is None:
+            self._get_theme_and_decolor()
+        return self._cached_theme
+
+    @property
+    def _decolor(self):
+        if self._cached_decolor is None:
+            self._get_theme_and_decolor()
+        return self._cached_decolor
 
     # ===============================
     # Section and indentation methods
@@ -2817,8 +2846,12 @@ def _get_formatter(self, file=None):
     def _get_validation_formatter(self):
         # Return cached formatter for read-only validation operations
         # (_expand_help and _format_args). Avoids repeated slow _set_color 
calls.
+        # Validation never renders output, so force color off to avoid
+        # importing _colorize during add_argument.
         if self._cached_formatter is None:
-            self._cached_formatter = self._get_formatter()
+            formatter = self.formatter_class(prog=self.prog)
+            formatter._set_color(False)
+            self._cached_formatter = formatter
         return self._cached_formatter
 
     # =====================
@@ -2858,12 +2891,11 @@ def _print_message(self, message, file=None):
                 pass
 
     def _get_theme(self, file=None):
-        from _colorize import can_colorize, get_theme
-
-        if self.color and can_colorize(file=file):
-            return get_theme(force_color=True).argparse
+        # If self.color is False, _colorize is not imported
+        if self.color and _colorize.can_colorize(file=file):
+            return _colorize.get_theme(force_color=True).argparse
         else:
-            return get_theme(force_no_color=True).argparse
+            return _colorless_theme
 
     # ===============
     # Exiting methods
diff --git a/Lib/test/support/import_helper.py 
b/Lib/test/support/import_helper.py
index e8a58ed77061f5..e8a3d176ad6943 100644
--- a/Lib/test/support/import_helper.py
+++ b/Lib/test/support/import_helper.py
@@ -325,7 +325,7 @@ def ready_to_import(name=None, source=""):
                 sys.modules.pop(name, None)
 
 
-def ensure_lazy_imports(imported_module, modules_to_block):
+def ensure_lazy_imports(imported_module, modules_to_block, *, 
additional_code=None):
     """Test that when imported_module is imported, none of the modules in
     modules_to_block are imported as a side effect."""
     modules_to_block = frozenset(modules_to_block)
@@ -343,6 +343,16 @@ def ensure_lazy_imports(imported_module, modules_to_block):
             raise AssertionError(f'unexpectedly imported after importing 
{imported_module}: {{after}}')
         """
     )
+    if additional_code:
+        script += additional_code
+        script += textwrap.dedent(
+            f"""
+            if unexpected := modules_to_block & sys.modules.keys():
+                after = ", ".join(unexpected)
+                raise AssertionError(f'unexpectedly imported after additional 
code: {{after}}')
+            """
+        )
+
     from .script_helper import assert_python_ok
     assert_python_ok("-S", "-c", script)
 
diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py
index 88c1a21aa28551..4ea5b6f53a0426 100644
--- a/Lib/test/test_argparse.py
+++ b/Lib/test/test_argparse.py
@@ -80,6 +80,66 @@ def test_skip_invalid_stdout(self):
                 self.assertRegex(mocked_stderr.getvalue(), r'usage:')
 
 
+class TestLazyImports(unittest.TestCase):
+    LAZY_IMPORTS = {
+        "_colorize",
+        "copy",
+        "difflib",
+        "shutil",
+        "textwrap",
+        "warnings",
+    }
+    def test_module_import(self):
+        import_helper.ensure_lazy_imports(
+            "argparse",
+            self.LAZY_IMPORTS,
+        )
+
+    def test_create_parser(self):
+        # Test imports are still unused after
+        # creating a parser
+        create_parser = "argparse.ArgumentParser()"
+        imported_modules = {"shutil"}
+
+        import_helper.ensure_lazy_imports(
+            "argparse",
+            self.LAZY_IMPORTS - imported_modules,
+            additional_code=create_parser,
+        )
+
+    def test_add_subparser(self):
+        add_subparser = textwrap.dedent(
+            """
+            parser = argparse.ArgumentParser()
+            parser.add_subparsers(dest='command', required=False)
+            """
+        )
+        imported_modules = {"shutil"}
+
+        import_helper.ensure_lazy_imports(
+            "argparse",
+            self.LAZY_IMPORTS - imported_modules,
+            additional_code=add_subparser,
+        )
+
+    def test_parse_args(self):
+        example_parser = textwrap.dedent(
+            """
+            parser = argparse.ArgumentParser(prog='PROG')
+            parser.add_argument('-f', '--foo')
+            parser.add_argument('bar')
+            parser.parse_args(['BAR'])
+            parser.parse_args(['BAR', '--foo', 'FOO'])
+            """
+        )
+        imported_modules = {"shutil"}
+        import_helper.ensure_lazy_imports(
+            "argparse",
+            self.LAZY_IMPORTS - imported_modules,
+            additional_code=example_parser
+        )
+
+
 class TestArgumentParserPickleable(unittest.TestCase):
 
     @force_not_colorized
@@ -7801,6 +7861,14 @@ def fake_can_colorize(*, file=None):
         self.assertIn(output, calls)
         self.assertNotIn('\x1b[', output.getvalue())
 
+    def test_fake_color_theme_matches_real(self):
+        from argparse import _colorless_theme
+        _colorize_nocolor = _colorize.get_theme(force_no_color=True).argparse
+        for k in _colorize_nocolor:
+            self.assertEqual(
+                getattr(_colorless_theme, k), getattr(_colorize_nocolor, k)
+            )
+
 
 class TestModule(unittest.TestCase):
     def test_deprecated__version__(self):
diff --git 
a/Misc/NEWS.d/next/Library/2026-05-06-14-26-37.gh-issue-148823.ySmOE4.rst 
b/Misc/NEWS.d/next/Library/2026-05-06-14-26-37.gh-issue-148823.ySmOE4.rst
new file mode 100644
index 00000000000000..e362fab604b704
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-06-14-26-37.gh-issue-148823.ySmOE4.rst
@@ -0,0 +1 @@
+Defer the import of ``_colorize`` in ``argparse`` until needed for coloring 
output.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to