pyguides

Text Wrapping and Formatting with textwrap

Text wrapping in Python: when to reach for textwrap

You generated a 200-character help string and the terminal is 80 columns. You logged a sentence that wraps mid-word in a JSON viewer. You want a paragraph to look the same in a CLI as in a markdown file. These are the jobs Python’s textwrap module was built for, and the text wrapping it produces is what makes CLI output and help text look intentional.

The module exposes six public names: wrap, fill, shorten, dedent, indent, and the TextWrapper class. They all operate on str (never bytes) and assume a single paragraph of text. When you need text wrapping to a specific column width, the fastest fix is one line:

import textwrap

text = "Text wrapping in Python is easy with the textwrap module."
print(textwrap.fill(text, width=30))
# output: Text wrapping in Python is
#         easy with the textwrap module.

fill returns a single string with newlines between the wrapped lines. That covers most one-off cases. If you want the wrapped output as a list of lines instead, call wrap and use the list directly.

Wrap and fill: the workhorses

wrap and fill run the same algorithm with a different return type. wrap returns a list[str]; fill joins that list with "\n" and returns one string. The module-level wrap and fill create a fresh TextWrapper instance on every call, which is fine for one paragraph but wasteful in a tight loop. If you are wrapping thousands of paragraphs in a request handler or a doc generator, build one wrapper and reuse it (covered in the TextWrapper section below).

import textwrap

text = "This is a simple example showing how textwrap works in Python."

print(textwrap.wrap(text, width=20))
# output: ['This is a simple', 'example showing how', 'textwrap works in', 'Python.']

print(textwrap.fill(text, width=20))
# output: This is a simple
#         example showing how
#         textwrap works in
#         Python.

initial_indent and subsequent_indent are useful for bullets and quoted blocks. The first argument prefixes the first wrapped line; the second prefixes every line after it. The two can be different strings, which is how you make hanging indents and bulleted lists without writing your own loop.

import textwrap

text = "A long explanation that needs to be quoted so the reader knows it came from somewhere important."
print(textwrap.fill(text, width=50, initial_indent="> ", subsequent_indent="> "))
# output: > A long explanation that needs to be quoted so
#         > the reader knows it came from somewhere
#         > important.

The width argument is the soft cap, not a hard one. If a single word is longer than width, it still gets emitted on its own line; setting break_long_words=False keeps it on the same line and lets the wrapper break the line right after. The same trade-off applies to break_on_hyphens for hyphenated words: leave it True (the default) to break at the hyphen, set it to False to keep the word whole.

Shorten: truncate text for previews

shorten (added in Python 3.4) collapses all whitespace first, then truncates so the result fits width characters including the placeholder. It is the right tool for RSS excerpts, log line previews, and list-view cards where you need a single-line summary that always lands within a fixed character budget.

import textwrap

print(repr(textwrap.shorten("Hello  world!\n  Nice day.", width=15)))
# output: 'Hello [...]'

print(repr(textwrap.shorten("Hello world!", width=12)))
# output: 'Hello world!'

print(repr(textwrap.shorten("Hello world!", width=11)))
# output: 'Hello [...]'

print(repr(textwrap.shorten("Hello world", width=10, placeholder="...")))
# output: 'Hello...'

Two things to remember. The width budget includes the placeholder length, so a width of 10 with a 3-character placeholder leaves only 7 characters for the real text. And shorten does not preserve the original whitespace; it collapses to single spaces before measuring. If the original paragraph had meaningful indentation, that information is lost in the preview.

shorten also accepts a smaller set of keyword arguments than wrap and fill. Passing tabsize= or expand_tabs= raises TypeError, because whitespace collapsing makes those options meaningless. The only kwargs shorten takes are fix_sentence_endings, break_long_words, break_on_hyphens, and placeholder.

Dedent and indent: control leading whitespace

dedent strips the longest common leading whitespace from every non-blank line. Its canonical use is triple-quoted strings nested inside a function, where you want the source indentation to look natural without leaking into the runtime string value.

import textwrap

def make_report():
    body = textwrap.dedent("""\
        Summary:
            All tests passed.
        Next steps:
            Review and deploy.
    """)
    return body

print(make_report())
# output: Summary:
#         All tests passed.
# Next steps:
#         Review and deploy.

The backslash after the opening """ keeps the first newline from creating a blank first line. Without it, your dedented string starts with an extra \n and the common-prefix calculation still works, but the output reads as having a leading blank line, and the print call wastes a screen line.

Tabs and spaces are not equal to dedent. If the first non-blank line uses two spaces and the second uses one tab, there is no common prefix and the string is returned unchanged. This catches people when they paste code from a source file that mixes tab-indented and space-indented lines, or when they mix tabs from a Makefile with spaces from Python. Pick one style and stick to it.

indent does the opposite. It prefixes every non-blank line with a string. The default predicate skips whitespace-only lines, which is what you usually want:

import textwrap

print(repr(textwrap.indent("hello\n\n \nworld", "> ")))
# output: '> hello\n\n \n> world'

Pass lambda line: True to prefix every line including the blanks, which is useful for block-quote style prefixes that should appear even on visually empty lines. Pass a more selective predicate (such as lambda line: line.startswith(" ")) to mark only already-indented lines, which fits diff-style annotations and reStructuredText-style block quotes.

The TextWrapper class: reuse for performance

For hot paths, build one TextWrapper and call wrap or fill on it many times. The module-level helpers create a fresh wrapper on every call, so reusing one instance avoids that setup cost. The savings are small per call but add up in any loop that wraps hundreds of paragraphs, such as rendering documentation, building README tables, or generating log summaries.

import textwrap

wrapper = textwrap.TextWrapper(
    width=40,
    initial_indent="",
    subsequent_indent="    ",
    break_long_words=False,
    break_on_hyphens=False,
)

print(wrapper.fill("A short paragraph that fits on one line."))
# output: A short paragraph that fits on one line.

print(wrapper.fill("A longer paragraph that will wrap onto multiple lines with a hanging indent of four spaces."))
# output: A longer paragraph that will wrap onto
#             multiple lines with a hanging indent
#             of four spaces.

Every constructor argument is also a public instance attribute, so you can adjust behavior between calls. wrapper.width = 60 and wrapper.subsequent_indent = "" will mutate the existing wrapper rather than creating a new one. This is the easiest way to format different paragraph types (bullets, block quotes, code comments) with one shared formatter, since each call to fill can take a different text argument while keeping the same configuration.

Common gotchas

A few things that trip people up the first time:

  • wrap treats its input as one paragraph. To wrap multiple paragraphs, split on "\n\n" and wrap each one separately; the function collapses internal newlines into spaces, so the blank line is the only safe separator. Single \n characters become spaces, which is almost never what you want for paragraph breaks.
  • Tabs and spaces do not match inside dedent. Use a consistent indentation style in the source string and watch out for copy-paste from editors that auto-convert. A file that looks aligned in the editor can have hidden tabs.
  • textwrap only accepts str. Decode bytes first: textwrap.wrap(payload.decode("utf-8")). Passing a bytes object raises a TypeError from inside the wrapper’s str.translate call, and the error message is unhelpful because it points at the internal call site, not your code.
  • break_long_words=False and break_on_hyphens=False together make words truly inseparable. With only one of them set to False, the other still breaks the word at an arbitrary position, which looks ugly in monospace output and breaks URLs and identifiers.
  • shorten collapses all whitespace before measuring, and the placeholder counts toward the width budget. A 10-character width with a 3-character placeholder leaves 7 characters for the actual text, and you can end up with just a placeholder when the input is short.
  • fix_sentence_endings is off by default, English-only, and imperfect. Leave it off unless you have a specific reason to use it. It uses a small regex to detect sentence endings and double-space after them, which is a US-English typographic convention that does not generalize to other languages or to code snippets.

Practical recipes

A few patterns that come up often:

import shutil
import textwrap

# Wrap to the actual terminal width, falling back to 80 columns.
width = shutil.get_terminal_size((80, 24)).columns
print(textwrap.fill(long_paragraph, width=width))

# CLI help-text formatter with hanging indents.
help_wrapper = textwrap.TextWrapper(
    width=78,
    initial_indent="",
    subsequent_indent="    ",
)
formatted = help_wrapper.fill(epilog_text)

The terminal-width trick is worth keeping in your back pocket. Hardcoding 80 columns works on a developer machine and breaks on a 120-column IDE or a 40-column SSH session; reading the actual terminal width is one extra import and zero new failure modes. The help-text formatter above is the same recipe argparse uses internally for its epilog= and description= blocks, and reusing it means your custom help output looks the same as the framework’s. If your terminal reports a width smaller than, say, 40, you probably want to clamp the value before wrapping so the words do not wrap one per line and produce an unreadable wall of single-word lines.

For a centered banner, combine indent with str.center. The indent call ensures the prefix only attaches to non-blank lines, so a multiline string centers each line independently rather than shifting the whole block right by the prefix width.

import textwrap

text = "Short announcement goes here."
banner = "# " + text.center(40) + " #"
print(banner)
# output: #      Short announcement goes here.       #

The # and # bookends are static, so the visible width is 40 + 4 characters total. If you need the banner to fit a terminal, swap the literal 40 for shutil.get_terminal_size((80, 24)).columns - 4 and the same recipe adapts to whatever column width the user is running in. Center alignment is also helpful for diff markers in code review tools, where you want the + and - characters to line up regardless of the line content length.

Conclusion

textwrap solves the small but annoying problem of getting text wrapping to look right at a fixed width. Reach for fill when you need a quick wrapped string, TextWrapper when you are wrapping many times in a loop, shorten when you need a truncated preview, and dedent or indent when you need to shape leading whitespace. For the full method-level reference, see the textwrap module reference. When you want to control the line separator yourself instead of using fill, see str.join(). For multi-paragraph input, the str.splitlines() method is the easiest way to break the text into wrap-safe chunks before handing each chunk to wrap or fill.

See also