https://github.com/python/cpython/commit/40096da95a592ac0b2ad6aa9c731631784c3b393
commit: 40096da95a592ac0b2ad6aa9c731631784c3b393
branch: main
author: Savannah Ostrowski <[email protected]>
committer: savannahostrowski <[email protected]>
date: 2025-11-04T16:31:35Z
summary:

GH-139946: Colorize error and warning messages in argparse (#140695)

Co-authored-by: Hugo van Kemenade <[email protected]>

files:
A Misc/NEWS.d/next/Library/2025-10-28-02-46-56.gh-issue-139946.aN3_uY.rst
M Lib/_colorize.py
M Lib/argparse.py
M Lib/test/test_argparse.py
M Lib/test/test_clinic.py
M Lib/test/test_gzip.py
M Lib/test/test_uuid.py
M Lib/test/test_webbrowser.py

diff --git a/Lib/_colorize.py b/Lib/_colorize.py
index 63e951d6488547..57b712bc068d4e 100644
--- a/Lib/_colorize.py
+++ b/Lib/_colorize.py
@@ -170,6 +170,9 @@ class Argparse(ThemeSection):
     label: str = ANSIColors.BOLD_YELLOW
     action: str = ANSIColors.BOLD_GREEN
     reset: str = ANSIColors.RESET
+    error: str = ANSIColors.BOLD_MAGENTA
+    warning: str = ANSIColors.BOLD_YELLOW
+    message: str = ANSIColors.MAGENTA
 
 
 @dataclass(frozen=True, kw_only=True)
diff --git a/Lib/argparse.py b/Lib/argparse.py
index 1f4413a9897eeb..6b79747572f48f 100644
--- a/Lib/argparse.py
+++ b/Lib/argparse.py
@@ -2749,6 +2749,14 @@ def _print_message(self, message, file=None):
             except (AttributeError, OSError):
                 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
+        else:
+            return get_theme(force_no_color=True).argparse
+
     # ===============
     # Exiting methods
     # ===============
@@ -2768,13 +2776,21 @@ def error(self, message):
         should either exit or raise an exception.
         """
         self.print_usage(_sys.stderr)
+        theme = self._get_theme(file=_sys.stderr)
+        fmt = _('%(prog)s: error: %(message)s\n')
+        fmt = fmt.replace('error: %(message)s',
+                        f'{theme.error}error:{theme.reset} 
{theme.message}%(message)s{theme.reset}')
+
         args = {'prog': self.prog, 'message': message}
-        self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
+        self.exit(2, fmt % args)
 
     def _warning(self, message):
+        theme = self._get_theme(file=_sys.stderr)
+        fmt = _('%(prog)s: warning: %(message)s\n')
+        fmt = fmt.replace('warning: %(message)s',
+                        f'{theme.warning}warning:{theme.reset} 
{theme.message}%(message)s{theme.reset}')
         args = {'prog': self.prog, 'message': message}
-        self._print_message(_('%(prog)s: warning: %(message)s\n') % args, 
_sys.stderr)
-
+        self._print_message(fmt % args, _sys.stderr)
 
 def __getattr__(name):
     if name == "__version__":
diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py
index d6c9c1ef2c81e8..3a8be68a5468b0 100644
--- a/Lib/test/test_argparse.py
+++ b/Lib/test/test_argparse.py
@@ -2283,6 +2283,7 @@ class TestNegativeNumber(ParserTestCase):
         ('--complex -1e-3j', NS(int=None, float=None, complex=-0.001j)),
     ]
 
+@force_not_colorized_test_class
 class TestArgumentAndSubparserSuggestions(TestCase):
     """Test error handling and suggestion when a user makes a typo"""
 
@@ -6147,6 +6148,7 @@ def spam(string_to_convert):
 # Check that deprecated arguments output warning
 # ==============================================
 
+@force_not_colorized_test_class
 class TestDeprecatedArguments(TestCase):
 
     def test_deprecated_option(self):
@@ -7370,6 +7372,45 @@ def test_subparser_prog_is_stored_without_color(self):
         help_text = demo_parser.format_help()
         self.assertNotIn('\x1b[', help_text)
 
+    def test_error_and_warning_keywords_colorized(self):
+        parser = argparse.ArgumentParser(prog='PROG')
+        parser.add_argument('foo')
+
+        with self.assertRaises(SystemExit):
+            with captured_stderr() as stderr:
+                parser.parse_args([])
+
+        err = stderr.getvalue()
+        error_color = self.theme.error
+        reset = self.theme.reset
+        self.assertIn(f'{error_color}error:{reset}', err)
+
+        with captured_stderr() as stderr:
+            parser._warning('test warning')
+
+        warn = stderr.getvalue()
+        warning_color = self.theme.warning
+        self.assertIn(f'{warning_color}warning:{reset}', warn)
+
+    def test_error_and_warning_not_colorized_when_disabled(self):
+        parser = argparse.ArgumentParser(prog='PROG', color=False)
+        parser.add_argument('foo')
+
+        with self.assertRaises(SystemExit):
+            with captured_stderr() as stderr:
+                parser.parse_args([])
+
+        err = stderr.getvalue()
+        self.assertNotIn('\x1b[', err)
+        self.assertIn('error:', err)
+
+        with captured_stderr() as stderr:
+            parser._warning('test warning')
+
+        warn = stderr.getvalue()
+        self.assertNotIn('\x1b[', warn)
+        self.assertIn('warning:', warn)
+
 
 class TestModule(unittest.TestCase):
     def test_deprecated__version__(self):
diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py
index e0dbb062eb0372..e71f9fc181bb43 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -4,6 +4,7 @@
 
 from functools import partial
 from test import support, test_tools
+from test.support import force_not_colorized_test_class
 from test.support import os_helper
 from test.support.os_helper import TESTFN, unlink, rmtree
 from textwrap import dedent
@@ -2758,6 +2759,7 @@ def 
test_allow_negative_accepted_by_py_ssize_t_converter_only(self):
                 with self.assertRaisesRegex((AssertionError, TypeError), 
errmsg):
                     self.parse_function(block)
 
+@force_not_colorized_test_class
 class ClinicExternalTest(TestCase):
     maxDiff = None
 
diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py
index f14a882d386866..442d30fc970fa9 100644
--- a/Lib/test/test_gzip.py
+++ b/Lib/test/test_gzip.py
@@ -11,7 +11,7 @@
 import unittest
 from subprocess import PIPE, Popen
 from test.support import catch_unraisable_exception
-from test.support import import_helper
+from test.support import force_not_colorized_test_class, import_helper
 from test.support import os_helper
 from test.support import _4G, bigmemtest, requires_subprocess
 from test.support.script_helper import assert_python_ok, assert_python_failure
@@ -1057,6 +1057,7 @@ def wrapper(*args, **kwargs):
     return decorator
 
 
+@force_not_colorized_test_class
 class TestCommandLine(unittest.TestCase):
     data = b'This is a simple test with gzip'
 
diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py
index 33045a78721aac..5f9ab048cdeb6c 100755
--- a/Lib/test/test_uuid.py
+++ b/Lib/test/test_uuid.py
@@ -13,7 +13,7 @@
 from unittest import mock
 
 from test import support
-from test.support import import_helper, warnings_helper
+from test.support import force_not_colorized_test_class, import_helper, 
warnings_helper
 from test.support.script_helper import assert_python_ok
 
 py_uuid = import_helper.import_fresh_module('uuid', blocked=['_uuid'])
@@ -1250,10 +1250,12 @@ def test_cli_uuid8(self):
         self.do_test_standalone_uuid(8)
 
 
+@force_not_colorized_test_class
 class TestUUIDWithoutExtModule(CommandLineTestCases, BaseTestUUID, 
unittest.TestCase):
     uuid = py_uuid
 
 
+@force_not_colorized_test_class
 @unittest.skipUnless(c_uuid, 'requires the C _uuid module')
 class TestUUIDWithExtModule(CommandLineTestCases, BaseTestUUID, 
unittest.TestCase):
     uuid = c_uuid
diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py
index 6b577ae100e419..20d347168b3af8 100644
--- a/Lib/test/test_webbrowser.py
+++ b/Lib/test/test_webbrowser.py
@@ -7,6 +7,7 @@
 import unittest
 import webbrowser
 from test import support
+from test.support import force_not_colorized_test_class
 from test.support import import_helper
 from test.support import is_apple_mobile
 from test.support import os_helper
@@ -503,6 +504,7 @@ def test_environment_preferred(self):
             self.assertEqual(webbrowser.get().name, sys.executable)
 
 
+@force_not_colorized_test_class
 class CliTest(unittest.TestCase):
     def test_parse_args(self):
         for command, url, new_win in [
diff --git 
a/Misc/NEWS.d/next/Library/2025-10-28-02-46-56.gh-issue-139946.aN3_uY.rst 
b/Misc/NEWS.d/next/Library/2025-10-28-02-46-56.gh-issue-139946.aN3_uY.rst
new file mode 100644
index 00000000000000..4c68d4cd94bf78
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-10-28-02-46-56.gh-issue-139946.aN3_uY.rst
@@ -0,0 +1 @@
+Error and warning keywords in ``argparse.ArgumentParser`` messages are now 
colorized when color output is enabled, fixing a visual inconsistency in which 
they remained plain text while other output was colorized.

_______________________________________________
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