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]