Visitar URL original
Argparse wrapping is bugged when colors are involved · Issue #142035 · python/cpython · GitHub
Skip to content

Argparse wrapping is bugged when colors are involved #142035

Description

@alexprengere

Bug report

Bug description:

While working on coloring interpolated values in #141940, I realized that in some cases the wrapping is broken.

For example in this case, just a color change around the commas makes the wrapping different, because textwrap.wrap is sensitive to ANSI escape codes (those change the string length).

Image

There are 2 ways to fix this:

  • have a more clever wrapping that is not sensitive to ANSI escape codes
  • make sure the wrapping always happens before ANSI escape codes are inserted

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Nov 28, 2025
  2. picnixz commented on Nov 28, 2025

    @picnixz
    Member
  3. added
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Nov 28, 2025
  4. alexprengere commented on Nov 28, 2025

    @alexprengere
    ContributorAuthor

    Looking a bit more at the code, I believe it would make more sense to add a parameter to textwrap to ignore ANSI escape codes when wrapping/filling, then leverage it in argparse.

    With the recent addition of colors in the stdlib, I think it makes sense to have that option in textwrap directly. I would argue it should default to True, but I will leave that to the experts 😉.

    I opened #142040 as a proof-of-concept, if this is viable for the code owners, I can add the tests, docs, etc.

    EDIT: this is also an issue with multiple-code-points glyphs, textwrap is not handling them cleanly, so it can cause other wrapping issues:

    >>> textwrap.wrap("Cafe\u0301 is good", width=4)
    ['Cafe', '́ is', 'good']
    >>> textwrap.wrap("Cafe\u0301 is good", width=5)
    ['Café', 'is', 'good']

    AFAICT there is no clean way in stdlib to get the actual rendering width of such strings, like what is done by third party libs like wcwidth.

  5. hugovk commented on Nov 29, 2025

    @hugovk
    Member

    Please can you share some example code that causes this?

  6. alexprengere commented on Nov 29, 2025

    @alexprengere
    ContributorAuthor

    Running on the current CPython main:

    import argparse
    
    parser = argparse.ArgumentParser(
        description="A simple argument parser example.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument("--verbose", action="store_true", help="A l o n g d e s c r i p t i o n   f o r   t h e   v e r b o s e   f l a g   t o   d e m o n s t r a t e   t h e   f o r m a t t e r   c l a s s   u s e d   i n   t h i s   a r g u m e n t   p a r s e r .")
    parser.add_argument("--input", type=str, default="input.txt", help="Input file path")
    args = parser.parse_args()

    Then:

    $ COLUMNS=70 python test.py --help
    usage: test.py [-h] [--verbose] [--input INPUT]
    
    A simple argument parser example.
    
    options:
      -h, --help     show this help message and exit
      --verbose      A l o n g d e s c r i p t i o n f o r t h e v e r b
                     o s e f l a g t o d e m o n s t r a t e t h e f o r
                     m a t t e r c l a s s u s e d i n t h i s a r g u m
                     e n t p a r s e r . (default:
                     False)
      --input INPUT  Input file path (default:
                     input.txt)
    

    You can see the False for --verbose is not wrapping correctly.
    If you modify the script to manually write the default value, so it does not have the colors introduced in #141680

    import argparse
    
    parser = argparse.ArgumentParser(
        description="A simple argument parser example.",
    )
    parser.add_argument("--verbose", action="store_true", help="A l o n g d e s c r i p t i o n   f o r   t h e   v e r b o s e   f l a g   t o   d e m o n s t r a t e   t h e   f o r m a t t e r   c l a s s   u s e d   i n   t h i s   a r g u m e n t   p a r s e r . (default: False)")
    parser.add_argument("--input", type=str, default="input.txt", help="Input file path")
    args = parser.parse_args()

    Here is the text is wrapping correctly.

    usage: test.py [-h] [--verbose] [--input INPUT]
    
    A simple argument parser example.
    
    options:
      -h, --help     show this help message and exit
      --verbose      A l o n g d e s c r i p t i o n f o r t h e v e r b
                     o s e f l a g t o d e m o n s t r a t e t h e f o r
                     m a t t e r c l a s s u s e d i n t h i s a r g u m
                     e n t p a r s e r . (default: False)
      --input INPUT  Input file path
    

    #142040 is one way to fix it.

  7. picnixz commented on Nov 29, 2025

    @picnixz
    Member

    Maybe we could assume that (default: ...) is actually added to the description text before we format the entire thing? It looks like we are not wrapping the same text.

  8. yihong0618 commented on Nov 29, 2025

    @yihong0618
    Contributor

    Looking a bit more at the code, I believe it would make more sense to add a parameter to textwrap to ignore ANSI escape codes when wrapping/filling, then leverage it in argparse.

    With the recent addition of colors in the stdlib, I think it makes sense to have that option in textwrap directly. I would argue it should default to True, but I will leave that to the experts 😉.

    I opened #142040 as a proof-of-concept, if this is viable for the code owners, I can add the tests, docs, etc.

    EDIT: this is also an issue with multiple-code-points glyphs, textwrap is not handling them cleanly, so it can cause other wrapping issues:

    textwrap.wrap("Cafe\u0301 is good", width=4)
    ['Cafe', '́ is', 'good']
    textwrap.wrap("Cafe\u0301 is good", width=5)
    ['Café', 'is', 'good']
    AFAICT there is no clean way in stdlib to get the actual rendering width of such strings, like what is done by third party libs like wcwidth.

    wcwidth in terminal is hard.....
    pdm also found similar issue when color the arg when update to 3.14
    info: pdm-project/pdm#3682

  9. alexprengere commented on Dec 2, 2025

    @alexprengere
    ContributorAuthor

    For the record, as I explained in the linked PR, I believe the best solution to fix this would be to add a way to customize the length function of textwrap, like what was proposed in #28136.
    argparse could then use this to avoid counting the escape codes when wrapping.

  10. picnixz commented on Dec 2, 2025

    @picnixz
    Member

    I would be happy to have a custom length because I needed this in Sphinx. If I have more time this week-end I'll try to ressurect that other PR and check whether we can do something with this. This is a bit unfortunate that this will only be present in 3.15+ though, considering it's kinda annoying in Python 3.14. It's very user-facing in general now that all CLIs are colored (I don't know if they are colored by default but I think so) so we should also find a way to make it work in 3.14.

  11. savannahostrowski commented on Dec 2, 2025

    @savannahostrowski
    Member

    FWIW, text wrapping in argparse is quite fragile. There are a couple open issues related to text wrapping already open (albeit the solution would be different than what is required here). I do agree that it'd be nice to fix this in a more holistic way though.

  12. alexprengere commented on Dec 2, 2025

    @alexprengere
    ContributorAuthor

    If textwrap ever gets such a custom length parameter, that would make having a way to know multiple-code-points glyphs width even more appealing 😉, see #56777 for prior discussion.

  13. 10 remaining items

  14. added a commit that references this issue on Jul 15, 2026
  15. savannahostrowski commented on Jul 24, 2026

    @savannahostrowski
    Member

    The proposed textwrap change adds a public API, so it isn’t suitable for backporting. I’ve opened #154634 with a narrower, argparse-only bug fix that should be backportable. The broader textwrap API proposal should be considered separately.

  16. kdeldycke commented on Jul 24, 2026

    @kdeldycke

    The proposed textwrap change adds a public API, so it isn’t suitable for backporting. I’ve opened #154634 with a narrower, argparse-only bug fix that should be backportable. The broader textwrap API proposal should be considered separately.

    OK I will wait for feedback and eventually the merge of your #154634 PR before re-adapting my #152702 code to it.

  17. added a commit that references this issue on Jul 24, 2026
  18. added 3 commits that reference this issue on Jul 24, 2026
  19. added a commit that references this issue on Jul 24, 2026
  20. savannahostrowski commented on Jul 24, 2026

    @savannahostrowski
    Member

    I'm going to close this issue since it's specifically about a bug in argparse and the corresponding PRs have been merged. @kdeldycke Can you open a new issue to track the feature request for textwrap?


    Also removing the 3.14 label since AFAICT this only repros on 3.15+.

  21. moved this from Bugs to Doc issues in Argparse issueson Jul 24, 2026
  22. added a commit that references this issue on Jul 25, 2026
  23. kdeldycke commented on Jul 25, 2026

    @kdeldycke

    I'm going to close this issue since it's specifically about a bug in argparse and the corresponding PRs have been merged. @kdeldycke Can you open a new issue to track the feature request for textwrap?

    Just added an new issue at #154686 . This describe the situation and cross-link with my #152702 PR. The latter now removes your local patch and generalize it.

  24. added a commit that references this issue on Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.15bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions