https://github.com/python/cpython/commit/7cea70e14dac091cbd7e0601b96a59458f8c9bee
commit: 7cea70e14dac091cbd7e0601b96a59458f8c9bee
branch: main
author: Hugo van Kemenade <[email protected]>
committer: hugovk <[email protected]>
date: 2026-05-06T16:07:43Z
summary:

gh-144384: Lazily import `_colorize` (#149318)

files:
A Misc/NEWS.d/next/Library/2026-05-03-17-32-24.gh-issue-144384.q-8jSr.rst
M .github/workflows/mypy.yml
M Lib/difflib.py
M Lib/doctest.py
M Lib/profiling/sampling/live_collector/collector.py
M Lib/profiling/sampling/pstats_collector.py
M Lib/profiling/sampling/sample.py
M Lib/test/test_difflib.py
M Lib/test/test_traceback.py
M Lib/traceback.py
M Lib/unittest/runner.py

diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml
index 7f6571ef954576..490c32ecfc9a62 100644
--- a/.github/workflows/mypy.yml
+++ b/.github/workflows/mypy.yml
@@ -71,7 +71,8 @@ jobs:
           persist-credentials: false
       - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 
v6.2.0
         with:
-          python-version: "3.13"
+          python-version: "3.15"
+          allow-prereleases: true
           cache: pip
           cache-dependency-path: Tools/requirements-dev.txt
       - run: pip install -r Tools/requirements-dev.txt
diff --git a/Lib/difflib.py b/Lib/difflib.py
index eb249e3e288923..7a4ff15c34267b 100644
--- a/Lib/difflib.py
+++ b/Lib/difflib.py
@@ -30,10 +30,10 @@
            'Differ','IS_CHARACTER_JUNK', 'IS_LINE_JUNK', 'context_diff',
            'unified_diff', 'diff_bytes', 'HtmlDiff', 'Match']
 
-from _colorize import can_colorize, get_theme
 from heapq import nlargest as _nlargest
 from collections import namedtuple as _namedtuple
 from types import GenericAlias
+lazy from _colorize import can_colorize, get_theme
 
 Match = _namedtuple('Match', 'a b size')
 
diff --git a/Lib/doctest.py b/Lib/doctest.py
index 05acac1745ace9..be950079e396de 100644
--- a/Lib/doctest.py
+++ b/Lib/doctest.py
@@ -106,8 +106,8 @@ def _test():
 import unittest
 from io import StringIO, TextIOWrapper, BytesIO
 from collections import namedtuple
-import _colorize  # Used in doctests
-from _colorize import ANSIColors, can_colorize
+lazy import _colorize  # Used in doctests
+lazy from _colorize import ANSIColors, can_colorize
 
 
 class TestResults(namedtuple('TestResults', 'failed attempted')):
diff --git a/Lib/profiling/sampling/live_collector/collector.py 
b/Lib/profiling/sampling/live_collector/collector.py
index c03df4075277cd..a53cfc6b719a10 100644
--- a/Lib/profiling/sampling/live_collector/collector.py
+++ b/Lib/profiling/sampling/live_collector/collector.py
@@ -9,7 +9,7 @@
 import sys
 import sysconfig
 import time
-import _colorize
+lazy import _colorize
 
 from ..collector import Collector, extract_lineno
 from ..constants import (
diff --git a/Lib/profiling/sampling/pstats_collector.py 
b/Lib/profiling/sampling/pstats_collector.py
index 6be1d698ffaa9a..50500296c15acc 100644
--- a/Lib/profiling/sampling/pstats_collector.py
+++ b/Lib/profiling/sampling/pstats_collector.py
@@ -1,8 +1,8 @@
 import collections
 import marshal
 import pstats
+lazy from _colorize import ANSIColors
 
-from _colorize import ANSIColors
 from .collector import Collector, extract_lineno
 from .constants import MICROSECONDS_PER_SECOND, PROFILING_MODE_CPU
 
diff --git a/Lib/profiling/sampling/sample.py b/Lib/profiling/sampling/sample.py
index 9e315c080c353d..5bbe2483581333 100644
--- a/Lib/profiling/sampling/sample.py
+++ b/Lib/profiling/sampling/sample.py
@@ -6,7 +6,7 @@
 import sysconfig
 import time
 from collections import deque
-from _colorize import ANSIColors
+lazy from _colorize import ANSIColors
 
 from .pstats_collector import PstatsCollector
 from .stack_collector import CollapsedStackCollector, FlamegraphCollector
diff --git a/Lib/test/test_difflib.py b/Lib/test/test_difflib.py
index 771fd46e042a41..46c9b2c1d8c9fc 100644
--- a/Lib/test/test_difflib.py
+++ b/Lib/test/test_difflib.py
@@ -1,5 +1,7 @@
 import difflib
+from test import support
 from test.support import findfile, force_colorized
+from test.support.import_helper import ensure_lazy_imports
 import unittest
 import doctest
 import sys
@@ -644,6 +646,12 @@ def setUpModule():
     difflib.HtmlDiff._default_prefix = 0
 
 
+class LazyImportTest(unittest.TestCase):
+    @support.cpython_only
+    def test_lazy_import(self):
+        ensure_lazy_imports("difflib", {"_colorize"})
+
+
 def load_tests(loader, tests, pattern):
     tests.addTest(doctest.DocTestSuite(difflib))
     return tests
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 6624191f164bc1..bb64153b91c92c 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -23,7 +23,7 @@
                           requires_subprocess, os_helper)
 from test.support.os_helper import TESTFN, temp_dir, unlink
 from test.support.script_helper import assert_python_ok, 
assert_python_failure, make_script
-from test.support.import_helper import forget
+from test.support.import_helper import ensure_lazy_imports, forget
 from test.support import force_not_colorized, force_not_colorized_test_class
 
 import json
@@ -5632,5 +5632,11 @@ def 
test_suggestion_still_works_for_non_lazy_attributes(self):
         self.assertNotIn(b"BAR_MODULE_LOADED", stdout)
 
 
+class LazyImportTest(unittest.TestCase):
+    @support.cpython_only
+    def test_lazy_import(self):
+        ensure_lazy_imports("traceback", {"_colorize"})
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 66e88d0a588af3..88529e1c259a29 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -16,9 +16,9 @@
 import io
 import importlib.util
 import pathlib
-import _colorize
 
 from contextlib import suppress
+lazy import _colorize
 
 try:
     from _missing_stdlib_info import _MISSING_STDLIB_MODULE_MESSAGES
@@ -32,6 +32,36 @@
            'FrameSummary', 'StackSummary', 'TracebackException',
            'walk_stack', 'walk_tb', 'print_list']
 
+
+class _ShutdownTheme:
+    """Empty stand-in if `_colorize` cannot be imported during late 
shutdown."""
+    def __getattr__(self, _): return self
+    def __getitem__(self, _): return ""
+    def __format__(self, _): return ""
+    def __str__(self): return ""
+    def __add__(self, other): return other
+    __radd__ = __add__
+
+
+_shutdown_theme = _ShutdownTheme()
+
+
+def _safe_get_theme(*, force_color=False, force_no_color=False):
+    try:
+        return _colorize.get_theme(
+            force_color=force_color, force_no_color=force_no_color
+        )
+    except ImportError:
+        return _shutdown_theme
+
+
+def _safe_can_colorize(*, file=None):
+    try:
+        return _colorize.can_colorize(file=file)
+    except ImportError:
+        return False
+
+
 #
 # Formatting and printing lists of traceback lines.
 #
@@ -151,7 +181,7 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, 
limit=None, \
 def _print_exception_bltin(exc, file=None, /):
     if file is None:
         file = sys.stderr if sys.stderr is not None else sys.__stderr__
-    colorize = _colorize.can_colorize(file=file)
+    colorize = _safe_can_colorize(file=file)
     return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, 
colorize=colorize)
 
 
@@ -199,9 +229,9 @@ def _format_final_exc_line(etype, value, *, 
insert_final_newline=True, colorize=
     valuestr = _safe_string(value, 'exception')
     end_char = "\n" if insert_final_newline else ""
     if colorize:
-        theme = _colorize.get_theme(force_color=True).traceback
+        theme = _safe_get_theme(force_color=True).traceback
     else:
-        theme = _colorize.get_theme(force_no_color=True).traceback
+        theme = _safe_get_theme(force_no_color=True).traceback
     if value is None or not valuestr:
         line = f"{theme.type}{etype}{theme.reset}{end_char}"
     else:
@@ -555,9 +585,9 @@ def format_frame_summary(self, frame_summary, **kwargs):
         if frame_summary.filename.startswith("<stdin-") and 
frame_summary.filename.endswith('>'):
             filename = "<stdin>"
         if colorize:
-            theme = _colorize.get_theme(force_color=True).traceback
+            theme = _safe_get_theme(force_color=True).traceback
         else:
-            theme = _colorize.get_theme(force_no_color=True).traceback
+            theme = _safe_get_theme(force_no_color=True).traceback
         row.append(
             '  File {}"{}"{}, line {}{}{}, in {}{}{}\n'.format(
                 theme.filename,
@@ -1344,9 +1374,9 @@ def format_exception_only(self, *, show_group=False, 
_depth=0, **kwargs):
         """
         colorize = kwargs.get("colorize", False)
         if colorize:
-            theme = _colorize.get_theme(force_color=True).traceback
+            theme = _safe_get_theme(force_color=True).traceback
         else:
-            theme = _colorize.get_theme(force_no_color=True).traceback
+            theme = _safe_get_theme(force_no_color=True).traceback
 
         indent = 3 * _depth * ' '
         if not self._have_exc_type:
@@ -1494,9 +1524,9 @@ def _format_syntax_error(self, stype, **kwargs):
         # Show exactly where the problem was found.
         colorize = kwargs.get("colorize", False)
         if colorize:
-            theme = _colorize.get_theme(force_color=True).traceback
+            theme = _safe_get_theme(force_color=True).traceback
         else:
-            theme = _colorize.get_theme(force_no_color=True).traceback
+            theme = _safe_get_theme(force_no_color=True).traceback
         filename_suffix = ''
         if self.lineno is not None:
             yield '  File {}"{}"{}, line {}{}{}\n'.format(
diff --git a/Lib/unittest/runner.py b/Lib/unittest/runner.py
index 5f22d91aebd05f..893fcba968c3ef 100644
--- a/Lib/unittest/runner.py
+++ b/Lib/unittest/runner.py
@@ -4,11 +4,10 @@
 import time
 import warnings
 
-from _colorize import get_theme
-
 from . import result
 from .case import _SubTest
 from .signals import registerResult
+lazy from _colorize import get_theme
 
 __unittest = True
 
diff --git 
a/Misc/NEWS.d/next/Library/2026-05-03-17-32-24.gh-issue-144384.q-8jSr.rst 
b/Misc/NEWS.d/next/Library/2026-05-03-17-32-24.gh-issue-144384.q-8jSr.rst
new file mode 100644
index 00000000000000..aad4b716e05372
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-03-17-32-24.gh-issue-144384.q-8jSr.rst
@@ -0,0 +1 @@
+Lazily import :mod:`!_colorize`. Patch by Hugo van Kemenade.

_______________________________________________
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