Skip to content

gh-160633: Break the last chunk of a broken long word in textwrap truncation - #160634

Open
qiyuhuating wants to merge 1 commit into
python:mainfrom
qiyuhuating:textwrap-broken-long-word-truncation
Open

qiyuhuating wants to merge 1 commit into
python:mainfrom
qiyuhuating:textwrap-broken-long-word-truncation

Conversation

@qiyuhuating

Copy link
Copy Markdown

shorten() and wrap(max_lines=...) currently discard the whole remaining piece of a broken long word on the final line instead of breaking it further to make room for the placeholder, so the output can carry no text at all:

>>> import textwrap
>>> textwrap.shorten('x' * 30, width=10)   # before
'[...]'
>>> textwrap.wrap('x' * 30, width=10, max_lines=2)   # before
['xxxxxxxxxx', '[...]']

The truncation loop in TextWrapper._wrap_chunks() drops chunks whole until the placeholder fits; for a line whose only chunk is a full-width piece produced by _handle_long_word(), nothing ever fits and the bare placeholder is emitted. With break_long_words=True (the default) the wrapper already breaks this word on earlier lines, so breaking the piece further on the last line is consistent with the documented behavior of both break_long_words and shorten() ("enough words are dropped from the end so that the remaining words plus the placeholder fit within width").

>>> textwrap.shorten('x' * 30, width=10)   # after
'xxxx [...]'
>>> textwrap.wrap('x' * 30, width=10, max_lines=2)   # after
['xxxxxxxxxx', 'xxxx [...]']

Scope

The new branch only fires when all of the following hold, so every other documented truncation behavior is unchanged:

  • max_lines truncation is happening (the else branch);
  • break_long_words is true (the default);
  • the pending line consists of exactly one non-whitespace chunk of exactly width characters (a piece of a broken long word, or a word exactly as wide as the line);
  • the placeholder leaves room for at least one character (otherwise the bare-placeholder line is kept, as before).

In particular: whole-word drops from the end (['Hello', '[...]'] cases), the placeholder backtracking onto the previous line, the eager ValueError("placeholder too large for max width") check, and break_long_words=False all behave byte-identically to before. The existing test test_first_word_too_long_but_placeholder_fits (which asserts the bare-placeholder behavior for a word shorter than the width) still passes unchanged.

After the cut, cur_len + len(placeholder) == width, so the next loop iteration necessarily appends the placeholder and breaks out: no new looping risk (cf. gh-158979).

Notes

Test evidence

Lib/test/test_textwrap.py: 70 tests, all passing with this change. Against unpatched main exactly 3 fail — the updated test_max_lines_long expectation and the two new tests — confirming the change is limited to the intended behavior delta. New tests:

  • LongWordTestCase.test_max_lines_long_word_broken (wrap with max_lines, including the too-narrow and break_long_words=False fallbacks),
  • ShortenTestCase.test_long_word_broken_to_fit_placeholder.

@python-cla-bot

python-cla-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #35387635 | 📁 Comparing 0c0ba7f against main (b26fc41)

  🔍 Preview build  

2 files changed
± library/textwrap.html
± whatsnew/changelog.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant