Skip to content

[3.14] GH-130645: Default to color help in argparse (GH-136809) #136886

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 8 additions & 16 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ ArgumentParser objects
prefix_chars='-', fromfile_prefix_chars=None, \
argument_default=None, conflict_handler='error', \
add_help=True, allow_abbrev=True, exit_on_error=True, \
*, suggest_on_error=False, color=False)
*, suggest_on_error=False, color=True)

Create a new :class:`ArgumentParser` object. All parameters should be passed
as keyword arguments. Each parameter has its own more detailed description
Expand Down Expand Up @@ -119,7 +119,7 @@ ArgumentParser objects
* suggest_on_error_ - Enables suggestions for mistyped argument choices
and subparser names (default: ``False``)

* color_ - Allow color output (default: ``False``)
* color_ - Allow color output (default: ``True``)

.. versionchanged:: 3.5
*allow_abbrev* parameter was added.
Expand Down Expand Up @@ -626,27 +626,19 @@ keyword argument::
color
^^^^^

By default, the help message is printed in plain text. If you want to allow
color in help messages, you can enable it by setting ``color`` to ``True``::
By default, the help message is printed in color using `ANSI escape sequences
<https://en.wikipedia.org/wiki/ANSI_escape_code>`__.
If you want plain text help messages, you can disable this :ref:`in your local
environment <using-on-controlling-color>`, or in the argument parser itself
by setting ``color`` to ``False``::

>>> parser = argparse.ArgumentParser(description='Process some integers.',
... color=True)
... color=False)
>>> parser.add_argument('--action', choices=['sum', 'max'])
>>> parser.add_argument('integers', metavar='N', type=int, nargs='+',
... help='an integer for the accumulator')
>>> parser.parse_args(['--help'])

Even if a CLI author has enabled color, it can be
:ref:`controlled using environment variables <using-on-controlling-color>`.

If you're writing code that needs to be compatible with older Python versions
and want to opportunistically use ``color`` when it's available, you
can set it as an attribute after initializing the parser instead of using the
keyword argument::

>>> parser = argparse.ArgumentParser(description='Process some integers.')
>>> parser.color = True

.. versionadded:: 3.14


Expand Down
9 changes: 4 additions & 5 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1228,11 +1228,10 @@ argparse

.. _whatsnew314-color-argparse:

* Introduced the optional *color* parameter to
:class:`argparse.ArgumentParser`, enabling color for help text.
This can be controlled by :ref:`environment variables
<using-on-controlling-color>`. Color has also been enabled for help in the
:ref:`stdlib CLIs <library-cmdline>` which use :mod:`!argparse`.
* Enable color for help text, which can be disabled with the optional *color*
parameter to :class:`argparse.ArgumentParser`.
This can also be controlled by :ref:`environment variables
<using-on-controlling-color>`.
(Contributed by Hugo van Kemenade in :gh:`130645`.)


Expand Down
6 changes: 3 additions & 3 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def __init__(
indent_increment=2,
max_help_position=24,
width=None,
color=False,
color=True,
):
# default setting for width
if width is None:
Expand Down Expand Up @@ -1231,7 +1231,7 @@ def __init__(self,
self._name_parser_map = {}
self._choices_actions = []
self._deprecated = set()
self._color = False
self._color = True

super(_SubParsersAction, self).__init__(
option_strings=option_strings,
Expand Down Expand Up @@ -1878,7 +1878,7 @@ def __init__(self,
exit_on_error=True,
*,
suggest_on_error=False,
color=False,
color=True,
):
superinit = super(ArgumentParser, self).__init__
superinit(description=description,
Expand Down
23 changes: 22 additions & 1 deletion Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
import warnings

from enum import StrEnum
from test.support import captured_stderr
from test.support import (
captured_stderr,
force_not_colorized,
force_not_colorized_test_class,
)
from test.support import import_helper
from test.support import os_helper
from test.support import script_helper
Expand Down Expand Up @@ -1007,6 +1011,7 @@ def test_parse_enum_value(self):
args = parser.parse_args(['--color', 'red'])
self.assertEqual(args.color, self.Color.RED)

@force_not_colorized
def test_help_message_contains_enum_choices(self):
parser = argparse.ArgumentParser()
parser.add_argument('--color', choices=self.Color, help='Choose a color')
Expand Down Expand Up @@ -2403,6 +2408,7 @@ def test_modified_invalid_action(self):
# Subparsers tests
# ================

@force_not_colorized_test_class
class TestAddSubparsers(TestCase):
"""Test the add_subparsers method"""

Expand Down Expand Up @@ -3009,6 +3015,7 @@ def test_nested_argument_group(self):
# Parent parser tests
# ===================

@force_not_colorized_test_class
class TestParentParsers(TestCase):
"""Tests that parsers can be created with parent parsers"""

Expand Down Expand Up @@ -3216,6 +3223,7 @@ def test_mutex_groups_parents(self):
# Mutually exclusive group tests
# ==============================

@force_not_colorized_test_class
class TestMutuallyExclusiveGroupErrors(TestCase):

def test_invalid_add_argument_group(self):
Expand Down Expand Up @@ -3344,21 +3352,25 @@ def test_successes_when_required(self):
actual_ns = parse_args(args_string.split())
self.assertEqual(actual_ns, expected_ns)

@force_not_colorized
def test_usage_when_not_required(self):
format_usage = self.get_parser(required=False).format_usage
expected_usage = self.usage_when_not_required
self.assertEqual(format_usage(), textwrap.dedent(expected_usage))

@force_not_colorized
def test_usage_when_required(self):
format_usage = self.get_parser(required=True).format_usage
expected_usage = self.usage_when_required
self.assertEqual(format_usage(), textwrap.dedent(expected_usage))

@force_not_colorized
def test_help_when_not_required(self):
format_help = self.get_parser(required=False).format_help
help = self.usage_when_not_required + self.help
self.assertEqual(format_help(), textwrap.dedent(help))

@force_not_colorized
def test_help_when_required(self):
format_help = self.get_parser(required=True).format_help
help = self.usage_when_required + self.help
Expand Down Expand Up @@ -4030,11 +4042,13 @@ def _test(self, tester, parser_text):
tester.maxDiff = None
tester.assertEqual(expected_text, parser_text)

@force_not_colorized
def test_format(self, tester):
parser = self._get_parser(tester)
format = getattr(parser, 'format_%s' % self.func_suffix)
self._test(tester, format())

@force_not_colorized
def test_print(self, tester):
parser = self._get_parser(tester)
print_ = getattr(parser, 'print_%s' % self.func_suffix)
Expand All @@ -4047,6 +4061,7 @@ def test_print(self, tester):
setattr(sys, self.std_name, old_stream)
self._test(tester, parser_text)

@force_not_colorized
def test_print_file(self, tester):
parser = self._get_parser(tester)
print_ = getattr(parser, 'print_%s' % self.func_suffix)
Expand Down Expand Up @@ -4788,6 +4803,7 @@ class TestHelpUsageMetavarsSpacesParentheses(HelpTestCase):
version = ''


@force_not_colorized_test_class
class TestHelpUsageNoWhitespaceCrash(TestCase):

def test_all_suppressed_mutex_followed_by_long_arg(self):
Expand Down Expand Up @@ -5469,6 +5485,7 @@ def custom_type(string):
version = ''


@force_not_colorized_test_class
class TestHelpCustomHelpFormatter(TestCase):
maxDiff = None

Expand Down Expand Up @@ -5765,6 +5782,7 @@ def test_conflict_error(self):
self.assertRaises(argparse.ArgumentError,
parser.add_argument, '--spam')

@force_not_colorized
def test_resolve_error(self):
get_parser = argparse.ArgumentParser
parser = get_parser(prog='PROG', conflict_handler='resolve')
Expand Down Expand Up @@ -6031,6 +6049,7 @@ def test_argument_error(self):

class TestArgumentTypeError(TestCase):

@force_not_colorized
def test_argument_type_error(self):

def spam(string):
Expand Down Expand Up @@ -6829,6 +6848,7 @@ def setUp(self):
metavar = '<http[s]://example:1234>'
self.parser.add_argument('--proxy', metavar=metavar)

@force_not_colorized
def test_help_with_metavar(self):
help_text = self.parser.format_help()
self.assertEqual(help_text, textwrap.dedent('''\
Expand Down Expand Up @@ -6994,6 +7014,7 @@ def test_os_error(self):
self.parser.parse_args, ['@no-such-file'])


@force_not_colorized_test_class
class TestProgName(TestCase):
source = textwrap.dedent('''\
import argparse
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2792,6 +2792,7 @@ def test_cli_verbose(self):
out = self.expect_success("-v", fn)
self.assertEqual(out.strip(), fn)

@support.force_not_colorized
def test_cli_help(self):
out = self.expect_success("-h")
self.assertIn("usage: clinic.py", out)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enable color help by default in :mod:`argparse`.
Loading