https://github.com/python/cpython/commit/9a3263ff8f87114285fe7c4cf61c075fd67a2ca3
commit: 9a3263ff8f87114285fe7c4cf61c075fd67a2ca3
branch: main
author: Savannah Ostrowski <[email protected]>
committer: savannahostrowski <[email protected]>
date: 2026-01-07T17:39:47Z
summary:

GH-142950: Process format specifiers before colourization in argparse help 
(#142960)

files:
A Misc/NEWS.d/next/Library/2025-12-18-22-58-46.gh-issue-142950.EJ8w-T.rst
M Lib/argparse.py
M Lib/test/test_argparse.py

diff --git a/Lib/argparse.py b/Lib/argparse.py
index 633fec69ea4615..0494b545f2f1d3 100644
--- a/Lib/argparse.py
+++ b/Lib/argparse.py
@@ -688,11 +688,41 @@ def _expand_help(self, action):
                 params[name] = value.__name__
         if params.get('choices') is not None:
             params['choices'] = ', '.join(map(str, params['choices']))
-        # Before interpolating, wrap the values with color codes
+
         t = self._theme
-        for name, value in params.items():
-            params[name] = f"{t.interpolated_value}{value}{t.reset}"
-        return help_string % params
+
+        result = help_string % params
+
+        if not t.reset:
+            return result
+
+        # Match format specifiers like: %s, %d, %(key)s, etc.
+        fmt_spec = r'''
+            %
+            (?:
+                %                           # %% escape
+                |
+                (?:\((?P<key>[^)]*)\))?     # key
+                [-#0\ +]*                   # flags
+                (?:\*|\d+)?                 # width
+                (?:\.(?:\*|\d+))?           # precision
+                [hlL]?                      # length modifier
+                [diouxXeEfFgGcrsa]          # conversion type
+            )
+        '''
+
+        def colorize(match):
+            spec, key = match.group(0, 'key')
+            if spec == '%%':
+                return '%'
+            if key is not None:
+                # %(key)... - format and colorize
+                formatted = spec % {key: params[key]}
+                return f'{t.interpolated_value}{formatted}{t.reset}'
+            # bare %s etc. - format with full params dict, no colorization
+            return spec % params
+
+        return _re.sub(fmt_spec, colorize, help_string, flags=_re.VERBOSE)
 
     def _iter_indented_subactions(self, action):
         try:
diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py
index 758af98d5cb046..77170244675474 100644
--- a/Lib/test/test_argparse.py
+++ b/Lib/test/test_argparse.py
@@ -7663,6 +7663,38 @@ def test_backtick_markup_special_regex_chars(self):
         help_text = parser.format_help()
         self.assertIn(f'{prog_extra}grep "foo.*bar" | sort{reset}', help_text)
 
+    def test_help_with_format_specifiers(self):
+        # GH-142950: format specifiers like %x should work with color=True
+        parser = argparse.ArgumentParser(prog='PROG', color=True)
+        parser.add_argument('--hex', type=int, default=255,
+                            help='hex: %(default)x, alt: %(default)#x')
+        parser.add_argument('--zero', type=int, default=7,
+                            help='zero: %(default)05d')
+        parser.add_argument('--str', default='test',
+                            help='str: %(default)s')
+        parser.add_argument('--pct', type=int, default=50,
+                            help='pct: %(default)d%%')
+        parser.add_argument('--literal', help='literal: 100%%')
+        parser.add_argument('--prog', help='prog: %(prog)s')
+        parser.add_argument('--type', type=int, help='type: %(type)s')
+        parser.add_argument('--choices', choices=['a', 'b'],
+                            help='choices: %(choices)s')
+
+        help_text = parser.format_help()
+
+        interp = self.theme.interpolated_value
+        reset = self.theme.reset
+
+        self.assertIn(f'hex: {interp}ff{reset}', help_text)
+        self.assertIn(f'alt: {interp}0xff{reset}', help_text)
+        self.assertIn(f'zero: {interp}00007{reset}', help_text)
+        self.assertIn(f'str: {interp}test{reset}', help_text)
+        self.assertIn(f'pct: {interp}50{reset}%', help_text)
+        self.assertIn('literal: 100%', help_text)
+        self.assertIn(f'prog: {interp}PROG{reset}', help_text)
+        self.assertIn(f'type: {interp}int{reset}', help_text)
+        self.assertIn(f'choices: {interp}a, b{reset}', help_text)
+
     def test_print_help_uses_target_file_for_color_decision(self):
         parser = argparse.ArgumentParser(prog='PROG', color=True)
         parser.add_argument('--opt')
diff --git 
a/Misc/NEWS.d/next/Library/2025-12-18-22-58-46.gh-issue-142950.EJ8w-T.rst 
b/Misc/NEWS.d/next/Library/2025-12-18-22-58-46.gh-issue-142950.EJ8w-T.rst
new file mode 100644
index 00000000000000..219930c638384f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-12-18-22-58-46.gh-issue-142950.EJ8w-T.rst
@@ -0,0 +1 @@
+Fix regression in :mod:`argparse` where format specifiers in help strings 
raised :exc:`ValueError`.

_______________________________________________
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