Two versions of the same decision — does this word still fit? One of them is off by one on every line.
line, word, width = 'abc', 'def', 6
print(len(line) + len(word) <= width) # -> True, and wrong
print(len(line + ' ' + word)) # -> 7, which is over
# Joining costs a separator as well as the letters
print(len(line) + 1 + len(word) <= width) # -> False, correct
# ...but only when there is already something on the line
print(len('') + 1 + len(word) <= width) # -> True here, wrong in general
- Measure the string you are about to build
The reliable version of this test is len(line + ' ' + word) <= width — it asks about the exact thing that will exist. Adding the pieces up separately is faster and invites exactly one mistake, the missing separator, which shows up as lines one character too long. When an arithmetic check and a real construction disagree, trust the construction.
- The empty line is a different case
The first word on a line joins nothing, so it costs no space — and a single word longer than the whole width still has to go somewhere. Treating 'the line is empty' as its own branch handles both at once: the first word always goes on, no matter its length. Without that branch, an over-long word either loops forever or vanishes.
- Greedy is not optimal, and that is fine
Taking as much as fits on each line can leave a ragged right edge that a smarter algorithm would even out — TeX famously optimises the paragraph as a whole and gets visibly better results. Greedy wins anyway for terminals and plain text because it is one pass, needs no lookahead, and produces the same answer whether you have the whole paragraph or are streaming it.
Walk the words one at a time, keeping the line you are building in a variable. For each word, ask whether it still fits — and remember that the last line never gets a chance to fail the test, so it needs handling after the loop.
text.split() with no argument splits on any run of whitespace and drops the empties, which is exactly what the contract asks for. Keep a `current` string; when the next word does not fit, append current to the results and start a new one from that word.
Three branches per word: if current is empty, current = word (this is what lets an over-long word through); elif len(current) + 1 + len(word) <= width, current += ' ' + word; else append current and start again. After the loop, append current if it is not empty.
def wrap(text, width): if width < ____: raise ____(f’width must be at least 1, got {width}') lines = [] current = '' for word in text.____(): if not current: current = ____ elif len(current) + ____ + len(word) <= width: current += ' ' + word else: lines.append(____) current = word if ____: lines.append(current) return lines
def wrap(text, width):
if width < 1:
raise ValueError(f"width must be at least 1, got {width}")
lines = []
current = ""
for word in text.split():
if not current:
# The first word always goes on, even if it overflows the line —
# there is nowhere else for it to go.
current = word
elif len(current) + 1 + len(word) <= width:
current += " " + word
else:
lines.append(current)
current = word
if current:
lines.append(current)
return lines
Nothing inside the loop ever appends the final line — it is only ever flushed by a word that does not fit, and the last one has no successor. Forgetting the trailing `if current` silently drops the end of every paragraph, and short test inputs where the last line happens to be empty will not catch it. Any loop that accumulates into a buffer has this same flush-at-the-end obligation.
The no-argument form splits on runs of any whitespace and discards empty pieces; passing ' ' splits on each single space and keeps the empties, so 'a b' becomes ['a', '', 'b'] and newlines stay attached to words. The difference is invisible on tidy input and shows up the moment real text arrives.
Order the branches the other way — fit test first, empty check second — and a word longer than width never fits, so it never gets placed and the line never advances. Putting the unconditional branch first turns the impossible case into a defined one. When an algorithm might not make progress, find the branch that always does and make sure it can be reached.
Every version below is correct and verified against the same cases by the build. They are here because knowing which one to reach for is the part that takes judgement, and the trade-off is stated rather than left for you to guess.
import textwrap
def wrap(text, width):
if width < 1:
raise ValueError(f'width must be at least 1, got {width}')
return textwrap.wrap(
' '.join(text.split()),
width,
break_long_words=False,
break_on_hyphens=False,
)
What to write once you understand the loop. The ' '.join(text.split()) is not decoration — textwrap replaces each whitespace character with a space but does not collapse runs, so without it 'a b' keeps its three spaces. The two break_ flags are the contract’s decisions made explicit; leave them out and the defaults will split long words and hyphenated ones.
For plain text in real code, use textwrap — it handles initial and subsequent indents, tab expansion, hyphen breaking, whitespace policy and the long-word choice, and textwrap.shorten and textwrap.dedent solve the two neighbouring problems. This is worth writing by hand to understand the loop, and worth reaching for when you are wrapping something that is not characters — laying out fixed-width cells, or packing items whose 'width' is a measured pixel width from a font. The moment the text has proportional glyphs, markup or East Asian double-width characters, len() stops meaning width and the whole approach needs replacing.
- Add an indent argument applied to every line after the first, and work out what it does to the fit test.
- Make over-long words break instead of overflow, with a hyphen, and decide what happens when width is 1.
- Return the ragged space left on each line, then use it to compare your greedy result against putting one fewer word on the first line.