Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

I'm using argparse in Python 2.7 for parsing input options. One of my options is a multiple choice. I want to make a list in its help text, e.g.

from argparse import ArgumentParser
parser = ArgumentParser(description='test')
parser.add_argument('-g', choices=['a', 'b', 'g', 'd', 'e'], default='a',
    help="Some option, where\n"
         " a = alpha\n"
         " b = beta\n"
         " g = gamma\n"
         " d = delta\n"
         " e = epsilon")
parser.parse_args()

However, argparse strips all newlines and consecutive spaces. The result looks like

~/Downloads:52$ python2.7 x.py -h usage: x.py [-h] [-g {a,b,g,d,e}] optional arguments: -h, --help show this help message and exit -g {a,b,g,d,e} Some option, where a = alpha b = beta g = gamma d = delta e = epsilon

How to insert newlines in the help text?

I don't have python 2.7 with me so I can test out my ideas. How about using help text in triple quotes (""" """). Do the new lines survive using this? – pyfunc Oct 4, 2010 at 8:48 @pyfunc: No. The stripping is done in runtime by argparse, not the interpreter, so switching to """...""" won't help. – kennytm Oct 4, 2010 at 8:50

Try using RawTextHelpFormatter to preserve all of your formatting:

from argparse import RawTextHelpFormatter
parser = ArgumentParser(description='test', formatter_class=RawTextHelpFormatter)

It's similar to RawDescriptionHelpFormatter but instead of only applying to the description and epilog, RawTextHelpFormatter also applies to all help text (including arguments).

I think it's not. You could subclass it, but unfortunately Only the name of this class is considered a public API. All the methods provided by the class are considered an implementation detail. So probably not a great idea, although it might not matter, since 2.7 is meant to be the last 2.x python and you'll be expected to refactor lots of things for 3.x anyway. I'm actually running 2.6 with argparse installed via easy_install so that documentation may itself be out of date. – intuited Oct 4, 2010 at 9:00 Some links: for python 2.7, and python 3.*. The 2.6 package should, according to its wiki, comply with the official 2.7 one. From the doc: "Passing RawDescriptionHelpFormatter as formatter_class= indicates that description and epilog are already correctly formatted and should not be line-wrapped" – Stefano Nov 21, 2011 at 15:34 Try instead formatter_class=RawDescriptionHelpFormatter which only works on description and epilog rather than help text. – MarkHu May 24, 2014 at 4:47 I have noticed that even with RawTextHelpFormatter, leading and trailing newlines are removed. To work around this, you can simply add two or more consecutive newlines; all but one newline will survive. – MrMas Mar 18, 2016 at 16:04 You can combine formatters too, e.g. class Formatter( argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass and then formatter_class=Formatter. – Terry Brown Jul 28, 2016 at 16:44

If you just want to override the one option, you should not use RawTextHelpFormatter. Instead subclass the HelpFormatter and provide a special intro for the options that should be handled "raw" (I use "R|rest of help"):

import argparse
class SmartFormatter(argparse.HelpFormatter):
    def _split_lines(self, text, width):
        if text.startswith('R|'):
            return text[2:].splitlines()  
        # this is the RawTextHelpFormatter._split_lines
        return argparse.HelpFormatter._split_lines(self, text, width)

And use it:

from argparse import ArgumentParser
parser = ArgumentParser(description='test', formatter_class=SmartFormatter)
parser.add_argument('-g', choices=['a', 'b', 'g', 'd', 'e'], default='a',
    help="R|Some option, where\n"
         " a = alpha\n"
         " b = beta\n"
         " g = gamma\n"
         " d = delta\n"
         " e = epsilon")
parser.parse_args()

Any other calls to .add_argument() where the help does not start with R| will be wrapped as normal.

This is part of my improvements on argparse. The full SmartFormatter also supports adding the defaults to all options, and raw input of the utilities description. The full version has its own _split_lines method, so that any formatting done to e.g. version strings is preserved:

parser.add_argument('--version', '-v', action="version",
                    version="version...\n   42!")
                I want to do this for a version message, but this SmartFormatter only seems to work with help text, not the special version text. parser.add_argument('-v', '--version', action='version',version=get_version_str())  Is it possible to extend it to that case?
– mc_electron
                Apr 2, 2014 at 3:14
                @mc_electron the full version of the SmartFormatter also has its own _split_lines and preserves line breaks (no need to specify "R|" at the beginning, if you want that option, patch the _VersionAction.__call__ method
– Anthon
                Apr 2, 2014 at 11:18
                I'm not entirely grokking the first part of your comment, although I can see in _VersionAction.__call__ that I would probably want it to just parser.exit(message=version) instead of using the formatted version. Is there any way to do that without releasing a patched copy of argparse though?
– mc_electron
                Apr 3, 2014 at 20:45
                @mc_electron I am referring to the improvements I published on bitbucket (as per the link to my improvements on argparse in the answer). But  you can also patch the __call__ in _VersionAction by doing argparse._VersionAction.__call__ = smart_version after defining def smart_version(self, parser, namespace, values, option_string=None): ...
– Anthon
                Apr 3, 2014 at 21:36
                Hey this is sweet!  Though not present in the source code for the full version, the comment at the top of _split_lines that says # this is the RawTextHelpFormatter._split_lines is a little misleading.  The comment may be less confusing if you move it to below the if statement since that is the original RawTextHelpFormatter return.  Regardless, this answer gave me exactly what I needed, thanks again!
– svenevs
                Mar 26, 2016 at 18:07
import argparse, textwrap
parser = argparse.ArgumentParser(description='some information',
        usage='use "python %(prog)s --help" for more information',
        formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--argument', default=somedefault, type=sometype,
        help= textwrap.dedent('''\
        First line
        Second line
        More lines ... '''))

In this way, we can avoid the long empty space in front of each output line.

usage: use "python your_python_program.py --help" for more information
Prepare input file
optional arguments:
-h, --help            show this help message and exit
--argument ARGUMENT
                      First line
                      Second line
                      More lines ...

I've faced similar issue (Python 2.7.6). I've tried to break down description section into several lines using RawTextHelpFormatter:

parser = ArgumentParser(description="""First paragraph 
                                       Second paragraph
                                       Third paragraph""",  
                                       usage='%(prog)s [OPTIONS]', 
                                       formatter_class=RawTextHelpFormatter)
options = parser.parse_args()

And got:

usage: play-with-argparse.py [OPTIONS] First paragraph Second paragraph Third paragraph optional arguments: -h, --help show this help message and exit

So RawTextHelpFormatter is not a solution. Because it prints description as it appears in source code, preserving all whitespace characters (I want to keep extra tabs in my source code for readability but I don't want to print them all. Also raw formatter doesn't wrap line when it is too long, more than 80 characters for example).

Thanks to @Anton who inspired the right direction above. But that solution needs slight modification in order to format description section.

Anyway, custom formatter is needed. I extended existing HelpFormatter class and overrode _fill_text method like this:

import textwrap as _textwrap
class MultilineFormatter(argparse.HelpFormatter):
    def _fill_text(self, text, width, indent):
        text = self._whitespace_matcher.sub(' ', text).strip()
        paragraphs = text.split('|n ')
        multiline_text = ''
        for paragraph in paragraphs:
            formatted_paragraph = _textwrap.fill(paragraph, width, initial_indent=indent, subsequent_indent=indent) + '\n\n'
            multiline_text = multiline_text + formatted_paragraph
        return multiline_text

Compare with the original source code coming from argparse module:

def _fill_text(self, text, width, indent):
    text = self._whitespace_matcher.sub(' ', text).strip()
    return _textwrap.fill(text, width, initial_indent=indent,
                                       subsequent_indent=indent)

In the original code the whole description is being wrapped. In custom formatter above the whole text is split into several chunks, and each of them is formatted independently.

So with aid of custom formatter:

parser = ArgumentParser(description= """First paragraph 
                                        Second paragraph
                                        Third paragraph""",  
                usage='%(prog)s [OPTIONS]',
                formatter_class=MultilineFormatter)
options = parser.parse_args()

the output is:

usage: play-with-argparse.py [OPTIONS] First paragraph Second paragraph Third paragraph optional arguments: -h, --help show this help message and exit This is wonderful --- happened on this after almost giving up and contemplating just reimplementing the help argument altogether... saved me a good amount of hassle. – Paul Gowder Nov 30, 2015 at 19:38 subclassing HelpFormatter is problematic since the argparse developers only guarantee that the class name will survive in future versions of argparse. They've basically written themselves a blank check so they can change method names if it would be convenient for them to do so. I find this frustrating; the least they could have done is exposed a few methods in the API. – MrMas Mar 18, 2016 at 16:14

I admit I found this a very frustrating experience as it seems many others have, given the number of solutions I see posted and the number of times I see this asked across the web. But I find most of these solutions far too complicated for my likes and I'd like share the tersest simplest solution I have for it.

Here is the script to demonstrate:

#!/usr/bin/python3
import textwrap
from argparse import ArgumentParser, HelpFormatter
class RawFormatter(HelpFormatter):
    def _fill_text(self, text, width, indent):
        return "\n".join([textwrap.fill(line, width) for line in textwrap.indent(textwrap.dedent(text), indent).splitlines()])
program_descripton = f'''
    FunkyTool v1.0
    Created by the Funky Guy on January 1 2020
    Licensed under The Hippocratic License 2.1
    https://firstdonoharm.dev/
    Distributed on an "AS IS" basis without warranties
    or conditions of any kind, either express or implied.
    USAGE:
parser = ArgumentParser(description=program_descripton, formatter_class=RawFormatter)
args = parser.parse_args()

And here is what it looks like in test.py:

$ ./test.py --help
usage: test.py [-h]
FunkyTool v1.0
Created by the Funky Guy on January 1 2020
Licensed under The Hippocratic License 2.1
https://firstdonoharm.dev/
Distributed on an "AS IS" basis without warranties
or conditions of any kind, either express or implied.
USAGE:
optional arguments:
  -h, --help  show this help message and exit

And so, all the basic formatting in the original description is preserved neatly and we've had, alas, to use a custom formatter, but it's a oneliner. It can be written more lucidly as:

class RawFormatter(HelpFormatter):
    def _fill_text(self, text, width, indent):
        text = textwrap.dedent(text)          # Strip the indent from the original python definition that plagues most of us.
        text = textwrap.indent(text, indent)  # Apply any requested indent.
        text = text.splitlines()              # Make a list of lines
        text = [textwrap.fill(line, width) for line in text] # Wrap each line 
        text = "\n".join(text)                # Join the lines again
        return text

But I prefer it on one line myself.

This is a great answer! But it doesn't apply to the argument help text in OP's question. I added that functionality in my answer. – idbrii Nov 8, 2022 at 23:04

Starting from SmartFomatter described above, I ended to that solution:

class SmartFormatter(argparse.HelpFormatter):
         Custom Help Formatter used to split help text when '\n' was 
         inserted in it.
    def _split_lines(self, text, width):
        r = []
        for t in text.splitlines(): r.extend(argparse.HelpFormatter._split_lines(self, t, width))
        return r

Note that strangely the formatter_class argument passed to top level parser is not inheritated by sub_parsers, one must pass it again for each created sub_parser.

Yet another simple way of getting new lines using RawTextHelpFormatter and deal with the indentation is

import argparse
parser = argparse.ArgumentParser(
    description='test', formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('-g', choices=['a', 'b', 'g', 'd', 'e'], default='a',
                    help=('Some option, where\n'
                          ' a = alpha\n'
                          ' b = beta\n'
                          ' g = gamma\n'
                          ' d = delta\n'
                          ' e = epsilon'))
parser.parse_args()

The output is

$ python2 x.py -h
usage: x.py [-h] [-g {a,b,g,d,e}]
optional arguments:
  -h, --help      show this help message and exit
  -g {a,b,g,d,e}  Some option, where
                   a = alpha
                   b = beta
                   g = gamma
                   d = delta
                   e = epsilon

I wanted to have both manual line breaks in the description text, and auto wrapping of it; but none of the suggestions here worked for me - so I ended up modifying the SmartFormatter class given in the answers here; the issues with the argparse method names not being a public API notwithstanding, here is what I have (as a file called test.py):

import argparse
from argparse import RawDescriptionHelpFormatter
# call with: python test.py -h
class SmartDescriptionFormatter(argparse.RawDescriptionHelpFormatter):
  #def _split_lines(self, text, width): # RawTextHelpFormatter, although function name might change depending on Python
  def _fill_text(self, text, width, indent): # RawDescriptionHelpFormatter, although function name might change depending on Python
    #print("splot",text)
    if text.startswith('R|'):
      paragraphs = text[2:].splitlines()
      rebroken = [argparse._textwrap.wrap(tpar, width) for tpar in paragraphs]
      #print(rebroken)
      rebrokenstr = []
      for tlinearr in rebroken:
        if (len(tlinearr) == 0):
          rebrokenstr.append("")
        else:
          for tlinepiece in tlinearr:
            rebrokenstr.append(tlinepiece)
      #print(rebrokenstr)
      return '\n'.join(rebrokenstr) #(argparse._textwrap.wrap(text[2:], width))
    # this is the RawTextHelpFormatter._split_lines
    #return argparse.HelpFormatter._split_lines(self, text, width)
    return argparse.RawDescriptionHelpFormatter._fill_text(self, text, width, indent)
parser = argparse.ArgumentParser(formatter_class=SmartDescriptionFormatter, description="""R|Blahbla bla blah blahh/blahbla (bla blah-blabla) a blahblah bl a blaha-blah .blah blah
Blah blah bla blahblah, bla blahblah blah blah bl blblah bl blahb; blah bl blah bl bl a blah, bla blahb bl:
  blah blahblah blah bl blah blahblah""")
options = parser.parse_args()

This is how it works in 2.7 and 3.4:

$ python test.py -h
usage: test.py [-h]
Blahbla bla blah blahh/blahbla (bla blah-blabla) a blahblah bl a blaha-blah
.blah blah
Blah blah bla blahblah, bla blahblah blah blah bl blblah bl blahb; blah bl
blah bl bl a blah, bla blahb bl:
  blah blahblah blah bl blah blahblah
optional arguments:
  -h, --help  show this help message and exit

For this question, argparse.RawTextHelpFormatter is helpful to me.

Now, I want to share how do I use the argparse.

I know it may not be related to question,

but these questions have been bothered me for a while.

So I want to share my experience, hope that will be helpful for someone.

Here we go.

3rd Party Modules

colorama: for change the text color: pip install colorama

Makes ANSI escape character sequences (for producing colored terminal text and cursor positioning) work under MS Windows

Example

import colorama
from colorama import Fore, Back
from pathlib import Path
from os import startfile, system
SCRIPT_DIR = Path(__file__).resolve().parent
TEMPLATE_DIR = SCRIPT_DIR.joinpath('.')
def main(args):
if __name__ == '__main__':
    colorama.init(autoreset=True)
    from argparse import ArgumentParser, RawTextHelpFormatter
    format_text = FormatText([(20, '<'), (60, '<')])
    yellow_dc = format_text.new_dc(fore_color=Fore.YELLOW)
    green_dc = format_text.new_dc(fore_color=Fore.GREEN)
    red_dc = format_text.new_dc(fore_color=Fore.RED, back_color=Back.LIGHTYELLOW_EX)
    script_description = \
        '\n'.join([desc for desc in
                   [f'\n{green_dc(f"python {Path(__file__).name} [REFERENCE TEMPLATE] [OUTPUT FILE NAME]")} to create template.',
                    f'{green_dc(f"python {Path(__file__).name} -l *")} to get all available template',
                    f'{green_dc(f"python {Path(__file__).name} -o open")} open template directory so that you can put your template file there.',
                    # <- add your own description
    arg_parser = ArgumentParser(description=yellow_dc('CREATE TEMPLATE TOOL'),
                                # conflict_handler='resolve',
                                usage=script_description, formatter_class=RawTextHelpFormatter)
    arg_parser.add_argument("ref", help="reference template", nargs='?')
    arg_parser.add_argument("outfile", help="output file name", nargs='?')
    arg_parser.add_argument("action_number", help="action number", nargs='?', type=int)
    arg_parser.add_argument('--list', "-l", dest='list',
                            help=f"example: {green_dc('-l *')} \n"
                                 "description: list current available template. (accept regex)")
    arg_parser.add_argument('--option', "-o", dest='option',
                            help='\n'.join([format_text(msg_data_list) for msg_data_list in [
                                ['example', 'description'],
                                [green_dc('-o open'), 'open template directory so that you can put your template file there.'],
                                [green_dc('-o run'), '...'],
                                [green_dc('-o ...'), '...'],
                                # <- add your own description
    g_args = arg_parser.parse_args()
    task_run_list = [[False, lambda: startfile('.')] if g_args.option == 'open' else None,
                     [False, lambda: [print(template_file_path.stem) for template_file_path in TEMPLATE_DIR.glob(f'{g_args.list}.py')]] if g_args.list else None,
                     # <- add your own function
    for leave_flag, func in [task_list for task_list in task_run_list if task_list]:
        func()
        if leave_flag:
            exit(0)
    # CHECK POSITIONAL ARGUMENTS
    for attr_name, value in vars(g_args).items():
        if attr_name.startswith('-') or value is not None:
            continue
        system('cls')
        print(f'error required values of {red_dc(attr_name)} is None')
        print(f"if you need help, please use help command to help you: {red_dc(f'python {__file__} -h')}")
        exit(-1)
    main(g_args)
            format_text = FormatText([(20, '<'), (60, '<')])
            red_dc = format_text.new_dc(fore_color=Fore.RED)
            print(red_dc(['column 1', 'column 2']))
            print(red_dc('good morning'))
        :param align_list:
        :param autoreset:
        self.align_list = align_list
        colorama.init(autoreset=autoreset)
    def __call__(self, text_list: list):
        if len(text_list) != len(self.align_list):
            if isinstance(text_list, str):
                return text_list
            raise AttributeError
        return ' '.join(f'{txt:{flag}{int_align}}' for txt, (int_align, flag) in zip(text_list, self.align_list))
    def new_dc(self, fore_color: Fore = Fore.GREEN, back_color: Back = ""):  # DECORATOR
        """create a device context"""
        def wrap(msgs):
            return back_color + fore_color + self(msgs) + Fore.RESET
        return wrap

The following python 3 formatter appends the default value if one exists and preserves line lengths.

from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, \ 
                     RawTextHelpFormatter
import textwrap
class CustomArgumentFormatter(ArgumentDefaultsHelpFormatter, RawTextHelpFormatter):
    """Formats argument help which maintains line length restrictions as well as appends default value if present."""
    def _split_lines(self, text, width):
        text = super()._split_lines(text, width)
        new_text = []
        # loop through all the lines to create the correct wrapping for each line segment.
        for line in text:
            if not line:
                # this would be a new line.
                new_text.append(line)
                continue
            # wrap the line's help segment which preserves new lines but ensures line lengths are
            # honored
            new_text.extend(textwrap.wrap(line, width))
        return new_text

Then create your argument parser with your new formatter:

my_arg_parser = ArgumentParser(formatter_class=CustomArgumentFormatter)
# ... add your arguments ...
print(my_arg_parser.format_help())

I came here looking for way to get the behavior of ArgumentDefaultsHelpFormatter but with newlines and tabs honored. Troy's code code got me close, but the final result ended up being a bit simpler:

class CustomArgumentFormatter(argparse.ArgumentDefaultsHelpFormatter):
    Formats help text to honor newlines and tabs (and show default values).
    # Match multiples of regular spaces only.
    _SPACE_MATCHER = re.compile(r' +', re.ASCII)
    def _split_lines(self, text, width):
        new_text = []
        for line in text.splitlines():
          # For each newline in the help message, replace any multiples of
          # whitespaces (due to indentation in source code) with one space.
          line = self._SPACE_MATCHER.sub(' ', line).rstrip()
          # Fit the line length to the console width
          new_text.extend(textwrap.wrap(line, width))
        return new_text

Then newlines and tabs will appear as intended:

parser = argparse.ArgumentParser(formatter_class=CustomArgumentFormatter)
parser.add_argument(
    '--ethernet_config', type=str, required=False, default=None,
    help='Path to a text file that specifies Ethernet network IP settings \
      to use on the board. For example: \
      \n\t ip=192.0.2.100 \
      \n\t subnet_mask=255.255.255.0 \
      \n\t gateway=192.0.2.1')

# 12 years late to the party, but I needed this too.

OP asked for new lines in help (not description) and as such the solutions here actually do not fully work, because if a line is longer than the screen width then it loses indentation when wrapped (gets wrapped to column 1 instead of preserving indentation of help text) which looks really ugly, or empty lines are gobbled, which I don't want as I need empty lines sometimes in long help texts.

Working solution below:

import textwrap
class CustomHelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
    def _split_lines(self, text, width):
        wrapper = textwrap.TextWrapper(width=width)
        lines = []
        for line in text.splitlines():
            if len(line) > width:
                lines.extend(wrapper.wrap(line))
            else:
                lines.append(line)
        return lines
parser = argparse.ArgumentParser(formatter_class=CustomArgumentFormatter)

Bernd's answer is very helpful, but doesn't apply to argument help strings. Here's an extension of it that applies to all help text (following the example of RawTextHelpFormatter).

DescriptionWrappedNewlineFormatter is his original RawFormatter and WrappedNewlineFormatter will additionally wrap arguments.

import argparse
import textwrap
class DescriptionWrappedNewlineFormatter(argparse.HelpFormatter):
    """An argparse formatter that:
    * preserves newlines (like argparse.RawDescriptionHelpFormatter),
    * removes leading indent (great for multiline strings),
    * and applies reasonable text wrapping.
    Source: https://stackoverflow.com/a/64102901/79125
    def _fill_text(self, text, width, indent):
        # Strip the indent from the original python definition that plagues most of us.
        text = textwrap.dedent(text)
        text = textwrap.indent(text, indent)  # Apply any requested indent.
        text = text.splitlines()  # Make a list of lines
        text = [textwrap.fill(line, width) for line in text]  # Wrap each line
        text = "\n".join(text)  # Join the lines again
        return text
class WrappedNewlineFormatter(DescriptionWrappedNewlineFormatter):
    """An argparse formatter that:
    * preserves newlines (like argparse.RawTextHelpFormatter),
    * removes leading indent and applies reasonable text wrapping (like DescriptionWrappedNewlineFormatter),
    * applies to all help text (description, arguments, epilogue).
    def _split_lines(self, text, width):
        # Allow multiline strings to have common leading indentation.
        text = textwrap.dedent(text)
        text = text.splitlines()
        lines = []
        for line in text:
            wrapped_lines = textwrap.fill(line, width).splitlines()
            lines.extend(subline for subline in wrapped_lines)
            if line:
                lines.append("")  # Preserve line breaks.
        return lines
if __name__ == "__main__":
    def demo_formatter(formatter):
        parser = argparse.ArgumentParser(
            description="""
                A program that does things.
                Lots of description that describes how the program works.
                very long lines are wrapped. very long lines are wrapped. very long lines are wrapped. very long lines are wrapped. very long lines are wrapped. very long lines are wrapped.
                existing wrapping will be preserved if within width. existing
                wrapping is preserved. existing wrapping will be preserved.
                existing wrapping is preserved. existing wrapping will be
                preserved. existing wrapping is preserved. existing wrapping
                will be preserved. existing wrapping is preserved unless it goes too long for the display width.
            formatter_class=formatter,
        parser.add_argument(
            "--option",
            choices=[
                "red",
                "blue",
            help="""
                Lots of text describing different choices.
                    red: a warning colour
                    text on the next line
                    blue: a longer blah blah keeps going going going going going going going going going going
        print("\n\nDemo for {}\n".format(formatter.__name__))
        parser.print_help()
    demo_formatter(DescriptionWrappedNewlineFormatter)
    demo_formatter(WrappedNewlineFormatter)

Demo output for WrappedNewlineFormatter

usage: arg.py [-h] [--option {red,blue}]
A program that does things.
Lots of description that describes how the program works.
very long lines are wrapped. very long lines are wrapped. very long lines are
wrapped. very long lines are wrapped. very long lines are wrapped. very long
lines are wrapped.
existing wrapping will be preserved if within width. existing
wrapping is preserved. existing wrapping will be preserved.
existing wrapping is preserved. existing wrapping will be
preserved. existing wrapping is preserved. existing wrapping
will be preserved. existing wrapping is preserved unless it goes too long for
the display width.
optional arguments:
  -h, --help           show this help message and exit
  --option {red,blue}  Lots of text describing different choices.
                           red: a warning colour
                           text on the next line
                           blue: a longer blah blah keeps going going going
                       going going going going going going going

Demo output for DescriptionWrappedNewlineFormatter

usage: arg.py [-h] [--option {red,blue}]
A program that does things.
Lots of description that describes how the program works.
very long lines are wrapped. very long lines are wrapped. very long lines are
wrapped. very long lines are wrapped. very long lines are wrapped. very long
lines are wrapped.
existing wrapping will be preserved if within width. existing
wrapping is preserved. existing wrapping will be preserved.
existing wrapping is preserved. existing wrapping will be
preserved. existing wrapping is preserved. existing wrapping
will be preserved. existing wrapping is preserved unless it goes too long for
the display width.
optional arguments:
  -h, --help           show this help message and exit
  --option {red,blue}  Lots of text describing different choices. red: a
                       warning colour text on the next line blue: a longer
                       blah blah keeps going going going going going going
                       going going going going

This is the simplest solution I found that keeps the ability to automatically wrap text to the terminal's with while keeping manual newlines intact. It boils down to replacing the default formatter's calls to textwrap.wrap() with a version that keeps the manual newlines.

The _split_lines() and _fill_text() that I've overridden are of course implementation details of argparse, so this might break in the future. I'd be glad if you note so in the comments, if that happens to you! 😊

import textwrap
from argparse import ArgumentParser, HelpFormatter
def wrap_paragraphs(text: str, width: int, indent: str):
    Wrapper around `textwrap.wrap()` which keeps newlines in the input string
    intact.
    lines = list[str]()
    for i in text.splitlines():
        paragraph_lines = \
            textwrap.wrap(i, width, initial_indent=indent, subsequent_indent=indent)
        # `textwrap.wrap()` will return an empty list when passed an empty
        # string (which happens when there are two consecutive line breaks in
        # the input string). This would lead to those line breaks being
        # collapsed into a single line break, effectively removing empty lines
        # from the input. Thus, we add an empty line in that case.
        lines.extend(paragraph_lines or [''])
    return lines
class Formatter(HelpFormatter):
    def _split_lines(self, text, width):
        return wrap_paragraphs(text, width, '')
    def _fill_text(self, text, width, indent):
        return '\n'.join(wrap_paragraphs(text, width, indent))
parser = ArgumentParser(
    prog='guide',
    formatter_class=Formatter,
    description='The Hitch Hiker\'s Guide to the Galaxy is a wholly remarkable '
                'book. It has been compiled and recompiled many times over '
                'many years and under many different editorships. It contains'
                'contributions from countless numbers of travellers and '
                'researchers.\n'
                'The introduction begins like this:\n'
                '"Space," it says "is big. Really big"\n')
parser.add_argument(
    '--probability',
    help='"But what does it mean?" cried Arthur.\n'
         '"What, the custard?"\n'
         '"No, the measurement of probability!"\n')
parser.print_help()
usage: guide [-h] [--probability PROBABILITY]
The Hitch Hiker's Guide to the Galaxy is a wholly remarkable book. It has been
compiled and recompiled many times over many years and under many different
editorships. It containscontributions from countless numbers of travellers and
researchers.
The introduction begins like this:
"Space," it says "is big. Really big"
options:
  -h, --help            show this help message and exit
  --probability PROBABILITY
                        "But what does it mean?" cried Arthur.
                        "What, the custard?"
                        "No, the measurement of probability!"
        

Thanks for contributing an answer to Stack Overflow!

  • Please be sure to answer the question. Provide details and share your research!

But avoid

  • Asking for help, clarification, or responding to other answers.
  • Making statements based on opinion; back them up with references or personal experience.

To learn more, see our tips on writing great answers.