Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 16 – Practical Python: text, CLIs, web, automation and security

Text tools: textwrap, difflib and string

Wrap and shorten text with textwrap, print diffs and suggest close matches with difflib, and fill in templates with string.Template in Python.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: textwrap fits text to a width counted in characters · difflib suggests the close matches that score 0.6 or more · A unified diff turns two lists of lines into a patch · 3 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Wrap, indent and shorten text with textwrap
  • Compare texts and suggest close matches with difflib
  • Use string constants and string.Template for simple substitution
  • Explain how difflib scores similarity and when its scores mislead
  • Check a template that users edit before your program uses it

Before you start

A command-line tool receives the command “staus” and should answer “did you mean status?”; a table with a 30-character column must show a 56-character lesson title; and a teammate wants to see only the lines that changed in a settings file. None of these jobs needs a pattern, and Python’s standard library has a module for each.

textwrap fits text to a width: it wraps paragraphs, shortens titles and fixes indentation. difflib compares texts: it suggests the closest known word and prints patch-style diffs. string holds ready-made character sets and string.Template, a fill-in-the-blanks format with $name placeholders that suits text other people write. All three work on ordinary str values and come with Python.

textwrap fits text to a width counted in characters

Titles shortened for a table, a note wrapped, a reply quoted Python · shorten_titles.py
import textwrap

lessons = [
    ("Introduction to data structures and algorithms in Python", 22),
    ("SQL joins", 14),
    ("Designing a rate limiter for a public API", 30),
]

# shorten() collapses spaces, then cuts at a word boundary and adds a placeholder.
print(f"{'Lesson':<30} {'Min':>3}")
for title, minutes in lessons:
    print(f"{textwrap.shorten(title, width=30, placeholder=' …'):<30} {minutes:>3}")

# fill() wraps a paragraph to a width; wrap() returns the lines as a list.
note = "Fees paid after the due date carry a late charge, which is added to the next month's invoice automatically."
print(textwrap.fill(note, width=38))
print(len(textwrap.wrap(note, width=38)), "lines")

# dedent() removes the indentation that every line shares; indent() adds a prefix.
reply = """
    Dear Asha,
      your receipt is attached.
    Thanks"""
print(textwrap.indent(textwrap.dedent(reply).strip(), "> "))

Output

Lesson                         Min
Introduction to data …          22
SQL joins                       14
Designing a rate limiter for …  30
Fees paid after the due date carry a
late charge, which is added to the
next month's invoice automatically.
3 lines
> Dear Asha,
>   your receipt is attached.
> Thanks

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shorten_titles.py

shorten() first collapses runs of spaces, then cuts at a word boundary and appends its placeholder, which counts towards the width, so every title fits the 30-character column. Step through the third title:

Interactive · stage by stage How shorten() fits a title into 30 characters Step through textwrap.shorten() on the third title of the table. Each stage either changes the text or decides whether it fits yet.
How shorten() fits a title into 30 characters, step 5 of 5. Showing Collapse the whitespace (41 characters), Does it fit the width? (yes: 28 + 2 = 30), Drop the last word ('… rate limiter for'), Add the placeholder (30 characters in all). In focus: Add the placeholder.check againCollapse the whitespace41 charactersDoes it fit the width?yes: 28 + 2 = 30Drop the last word'… rate limiter for'Add the placeholder30 characters in all

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The title as written. 'Designing a rate limiter for a public API' has 41 characters and single spaces between its words, so collapsing the whitespace changes nothing.
  2. Too long for the column. 41 characters do not fit a width of 30, so shorten() has to cut. The placeholder will take 2 of the 30 characters, which leaves 28 for the words.
  3. Words come off the end. shorten() drops whole words from the end until the rest and the placeholder fit. Without 'a public API', 28 characters are left: 'Designing a rate limiter for'.
  4. It fits now. 28 characters of words and the 2 of the placeholder make exactly 30, which the width allows, so no more words are dropped.
  5. The placeholder is added. shorten() returns 'Designing a rate limiter for …', 30 characters, which fills the column exactly. A title that fits as it is, such as 'SQL joins', comes back unchanged.

fill() wraps a paragraph to a width and returns one string; wrap() returns the lines as a list. dedent() removes the indentation that every line shares, which helps with triple-quoted text inside indented code, and indent() adds a prefix to every line that is not blank. The width counts characters, not screen columns, which matters for text in Hindi, Chinese or with emoji.

wrap() and fill() take more settings as keyword arguments. max_lines=2 stops after two lines and ends the last one with the placeholder, which makes a short preview of a long description. break_long_words=False keeps a long address or a URL whole instead of cutting it at the width, and initial_indent and subsequent_indent indent the first line and the rest differently, as in a numbered list.

difflib suggests the close matches that score 0.6 or more

Did you mean: suggestions for mistyped commands Python · suggest_commands.py
import difflib

COMMANDS = ["status", "stash", "start", "push", "pull", "checkout", "commit", "config"]

for typed in ["staus", "chekout", "comit", "pussh", "deploy"]:
    matches = difflib.get_close_matches(typed, COMMANDS)
    print(f"{typed:8} -> {matches}")

# The score behind each suggestion: 1.0 means identical, and 0.6 is the default cutoff.
for word in ["status", "stash", "start", "push"]:
    print(f"staus vs {word:6} {difflib.SequenceMatcher(None, 'staus', word).ratio():.3f}")

Output

staus    -> ['status', 'stash', 'start']
chekout  -> ['checkout']
comit    -> ['commit']
pussh    -> ['push', 'stash']
deploy   -> []
staus vs status 0.909
staus vs stash  0.800
staus vs start  0.600
staus vs push   0.444

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 suggest_commands.py

get_close_matches(word, possibilities) returns up to three possibilities, the most similar first, and leaves out any that score below 0.6. The scores come from SequenceMatcher.ratio(), which compares two sequences exactly as they are, case and spaces included, and returns a number from 0, nothing in common, to 1, identical. “deploy” resembled no command, so it got an empty list, which is the honest answer. quick_ratio() and real_quick_ratio() are cheaper estimates that can only overstate the score. The autojunk setting makes long sequences ignore their most common items.

Raise the cutoff

Press Edit, change the first call to difflib.get_close_matches(typed, COMMANDS, cutoff=0.85) and run it again: “staus” now gets only status, which scores 0.909, and “pussh” only push, while stash, at 0.800 and 0.600, drops out of both lists.

Checkpoint

Check: Fitting text with textwrap · Close matches and their scores

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    import textwrap
    print(textwrap.shorten("Pay   the   hostel   fees   by   Friday", width=20))
    Show the answer: Question 1 of 2

    Answer: Pay the hostel [...]

    shorten() collapses the whitespace, sees that the text is too long, drops words from the end until the words and the placeholder fit, and adds the placeholder: "Pay the hostel [...]" is exactly 20 characters.

  2. Question 2 of 2 With the COMMANDS list of the example, what does difflib.get_close_matches("stat", COMMANDS) return?

    Choose one answer.

    A hint

    The scores against stat are 0.889 for start, 0.800 for status and 0.667 for stash.

    Show the answer: Question 2 of 2

    Answer: ['start', 'status', 'stash']

    get_close_matches() keeps the possibilities that score 0.6 or more and returns up to three of them, the most similar first, so the order follows the scores, not the list.

A unified diff turns two lists of lines into a patch

A unified diff of two versions of a settings file Python · config_diff.py
import difflib

old = """[server]
host = 0.0.0.0
port = 8000
workers = 2

[cache]
ttl = 60
""".splitlines(keepends=True)

new = """[server]
host = 0.0.0.0
port = 8080
workers = 4

[cache]
ttl = 60
size = 512
""".splitlines(keepends=True)

diff = difflib.unified_diff(old, new, fromfile="settings.ini (before)", tofile="settings.ini (after)", n=1)
print("".join(diff), end="")

Output

--- settings.ini (before)
+++ settings.ini (after)
@@ -2,4 +2,4 @@
 host = 0.0.0.0
-port = 8000
-workers = 2
+port = 8080
+workers = 4
 
@@ -7 +7,2 @@
 ttl = 60
+size = 512

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 config_diff.py

A unified diff: two header lines name the files, @@ gives the line ranges, then lines start with a space, a minus or a plus.Reading a unified diff--- settings.ini (before)the old file+++ settings.ini (after)the new file@@ -2,4 +2,4 @@old lines 2-5, new lines 2-5 host = 0.0.0.0a space: in both files- port = 8000a minus: only in the old file+ port = 8080a plus: only in the new file

The parts of a unified diff

Text description of the diagram

The diagram is a column of six rows, each showing one kind of line from the diff that config_diff.py prints, with its meaning under it.

  1. "--- settings.ini (before)" names the old file.
  2. "+++ settings.ini (after)" names the new file.
  3. "@@ -2,4 +2,4 @@" starts a hunk: it covers 4 lines starting at line 2 of the old file and 4 lines starting at line 2 of the new file, that is lines 2 to 5 of each.
  4. A line that starts with a space, such as "host = 0.0.0.0", is context: it is the same in both files.
  5. A line that starts with a minus, such as "port = 8000", is only in the old file.
  6. A line that starts with a plus, such as "port = 8080", is only in the new file.

unified_diff() takes two lists of lines, with their line endings kept by splitlines(keepends=True), and yields the lines of a patch: the two file names, then A block of a unified diff that covers one region of change: an @@ line with the line ranges it covers in the old and the new file, then the lines of that region. that start with @@, and inside them lines marked with a space, a minus or a plus. n=1 keeps one line of context around each change; the default is three. The output lines end with a newline, so join them with "". When your lines have no endings, as with text.splitlines(), pass lineterm="" and join with "\n" instead; without it, joining with "" runs the changed lines together and joining with "\n" doubles the line breaks after the headers. The same module offers ndiff(), which marks every line, get_opcodes(), which describes the edits as data, and HtmlDiff, which builds a side-by-side HTML table and supports dark mode since Python 3.14.

string.Template fills $name placeholders and nothing more

A character check, then a template filled in three ways Python · template_basics.py
import string
from string import Template

# The constants are plain strings, handy for building simple checks.
print(string.ascii_lowercase, string.digits, string.punctuation)
ALLOWED = set(string.ascii_lowercase + string.digits + "_")
for name in ["asha_k", "Asha K", "ravi99"]:
    print(f"{name!r:9} allowed: {set(name) <= ALLOWED}")

# Template: $name or ${name} is replaced, and $$ is a literal dollar sign.
note = Template("Hi $name, order ${order}s ships $day. Save $$5 next time.")
print(note.substitute(name="Asha", order="A17", day="on Monday"))
print(note.safe_substitute(name="Ravi"))
try:
    note.substitute(name="Ravi")
except KeyError as missing:
    print("KeyError:", missing)
print(note.get_identifiers())

Output

abcdefghijklmnopqrstuvwxyz 0123456789 !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
'asha_k'  allowed: True
'Asha K'  allowed: False
'ravi99'  allowed: True
Hi Asha, order A17s ships on Monday. Save $5 next time.
Hi Ravi, order ${order}s ships $day. Save $5 next time.
KeyError: 'order'
['name', 'order', 'day']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 template_basics.py

The string constants are plain strings, so set(name) <= ALLOWED checks in one line that a user name uses only lower-case letters, digits and the underscore. Template replaces $name and ${name}; the braces are needed when letters follow, as in ${order}s, and $$ stands for a dollar sign. substitute() raises KeyError when a value is missing, while safe_substitute() leaves the placeholder as it was, which suits drafts. A placeholder name is made of ASCII letters, digits and underscores and does not start with a digit, so $5 is not a placeholder at all. get_identifiers(), added in Python 3.11, lists the placeholders a template uses.

Template only ever replaces names with values, which makes it a good format for text that staff or users edit, such as email wording. str.format() can do far more, reading attributes and items of the values it formats. Do not confuse string.Template with t-strings, the template string literals of Python 3.14, which the next lesson covers: a Template is filled from a dictionary at run time, while a t-string is written in your code.

Checkpoint

Check: Reading and printing a unified diff · string constants and string.Template

  1. Question 1 of 2 In a unified diff, what does a line that starts with a space mean?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It is context: the line is the same in both files

    Inside a hunk, every line starts with one mark: a space for a line in both files, a minus for a line only in the old one and a plus for a line only in the new one.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    from string import Template
    t = Template("Dear $name, ${n} items")
    print(t.safe_substitute(name="Asha"))
    Show the answer: Question 2 of 2

    Answer: Dear Asha, ${n} items

    substitute() needs a value for every placeholder, while safe_substitute() fills what it can and keeps the rest, which suits a draft that will be filled later.

Which text tool fits which job

Read the table by its last two rows, what to watch out for and when to choose each tool:

Three standard-library tools for text
Criterion textwrapdifflibstring.Template
The job Fit text to a widthCompare texts, find close wordsFill in values by name
You give it A string and a widthTwo strings or lists of linesA template and a mapping
You get Lines or one stringA diff, opcodes or matchesThe filled-in string
Watch out for Width is characters, not columnsScores of long inputsMissing values raise KeyError
When to choose Console output, emails, tablesDid-you-mean, change logs, comparisonsText that people edit

Name the job first

Width or layout: textwrap. Two versions of a text, or a word to correct: difflib. Values put into text that someone else writes: string.Template. A shape such as a code or a date: re.

Check yourself

Wrapping, close matches, diffs and templates are what this practice asks about, one question at a time, beginning with whichever of the four your answers show you know least.

Practice that adapts to you

Practise this lesson

8 questions about 10 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 What does this print?

    Read the code, then choose one answer.

    import textwrap
    print(textwrap.shorten("Designing a rate limiter for a public API", width=20))
    Show the answer: Question 1 of 8

    Answer: Designing a [...]

    shorten() cuts at a word boundary and adds the placeholder, " [...]" by default, and the placeholder counts towards the width of 20. "Designing a rate [...]" would be 22 characters, so only "Designing a" fits before it.

  2. Question 2 of 8 Which function of textwrap returns the wrapped lines as a list?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: wrap()

    wrap(text, width) gives a list of lines and fill(text, width) the same lines joined with "\n". Use wrap() when you need to count or process the lines.

  3. Question 3 of 8 What does this print?

    Read the code, then choose one answer.

    import difflib
    print(difflib.get_close_matches("pul", ["pull", "push", "put", "pill"]))
    Show the answer: Question 3 of 8

    Answer: ['pull', 'put']

    The scores are 0.857 for "pull", 0.667 for "put" and 0.571 for "push" and "pill". The default cutoff is 0.6, so only the first two stay, best first.

  4. Question 4 of 8 What does difflib.SequenceMatcher(None, "abc", "abc").ratio() return?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: 1.0

    SequenceMatcher.ratio() returns a float from 0 to 1. get_close_matches() keeps the possibilities that score at least its cutoff, 0.6 by default.

  5. Question 5 of 8 You need a patch-style diff of two versions of a file, with "---" and "+++" headers and "@@" hunks. Which function produces it?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: difflib.unified_diff()

    unified_diff() takes two lists of lines and yields the lines of a unified diff, the format that git diff and patch use. get_close_matches() suggests similar words and ratio() scores similarity.

  6. Question 6 of 8 Your lines come from text.splitlines(), without line endings. How do you print a readable unified diff of them?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: Pass lineterm="" to unified_diff() and join its lines with "\n"

    unified_diff() ends its own header lines with lineterm, "\n" by default, and copies the content lines as they are. Lines with endings: join with "". Lines without: pass lineterm="" and join with "\n".

  7. Question 7 of 8 What does this print?

    Read the code, then choose one answer.

    from string import Template
    print(Template("Dear $name, $$$amount due").safe_substitute(name="Ravi"))
    Show the answer: Question 7 of 8

    Answer: Dear Ravi, $$amount due

    $$ is a literal dollar sign, and safe_substitute() leaves an unknown placeholder such as $amount as it was instead of raising KeyError. The two together print "$$amount".

  8. Question 8 of 8 Shop staff edit the wording of order emails on an admin page, and the program fills in the customer's name and order number. Which format should the program use?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: string.Template, with placeholders such as $name and $order

    Text that someone else edits is filled at run time from a mapping, which is what string.Template does, and its placeholders can only be names. f-strings and t-strings belong to the code you write.

How difflib scores similarity, and where the scores mislead Pro: opens with a Pro, Premium or Ultimate pass

difflib’s scores decide which suggestions a user sees and how a diff reads, and they hide two traps. Here the score is worked out by hand, the length at which scores collapse is reproduced, and the cost of one lookup is measured.

In this part:

  • Working out the similarity score by hand, with numbers
  • The 200-character cliff: autojunk, reproduced
  • From opcodes to a summary of changes
  • What a lookup costs, measured on about 20,000 words
  • Exercise: suggestions and a change summary
    • Suggest commands and summarise a change with difflib

2 diagrams · 4 runnable examples (Python) · 1 exercise with sample tests · about 12 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Text that looks right on screen, and templates people can edit safely Pro: opens with a Pro, Premium or Ultimate pass

Text written by people breaks two assumptions: that one character takes one column, and that a template is only text. Here the first is measured on four kinds of text and the second is reproduced as the leak of a secret, with the check that stops it.

In this part:

  • Columns are not characters: Hindi, Chinese and emoji, measured
  • Equal on screen, different in Python: normalising before you compare
  • A format string that leaks a secret, reproduced
  • Checking an edited template before saving it

1 diagram · 2 runnable examples (Python) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What is the difference between textwrap.wrap(), textwrap.fill() and textwrap.shorten()?

    All three fit text to a width counted in characters. wrap(text, width) breaks a paragraph at spaces and returns a list of lines, none longer than the width, which suits code that prints or pads each line itself. fill(text, width) does the same and joins the lines with newlines into one string, ready to print. shorten(text, width) keeps a single line: it collapses runs of whitespace, cuts at a word boundary and adds a placeholder, " [...]" by default, which counts towards the width, so it suits table cells and titles. wrap() and fill() also accept max_lines with a placeholder for previews. None of them knows screen columns, so text with wide characters such as Chinese, or with combining marks such as Hindi vowel signs, can look ragged even when every line has the same length.

  2. Mid to senior

    A customer database holds 300,000 names with duplicates such as "Asha Kumari" and "ASHA KUMARI.". A colleague proposes comparing every pair with difflib.SequenceMatcher(None, a, b).ratio() > 0.9. Estimate the cost, say what would go wrong, and design a better approach.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Staff edit the wording of order-confirmation emails in an admin panel, and the code sends them with text.format(order=order, customer=customer). What is the risk, and how would you redesign how templates are stored, validated and rendered?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    Design a tool that shows engineers what changed between two versions of a 5,000-line configuration file before a deploy, and flags risky changes such as ports, timeouts and debug switches. Which difflib functions do you use, and what else does the tool need?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • textwrap.shorten(), fill(), wrap(), dedent() and indent() fit text to a width that is counted in characters, not screen columns.
  • difflib.get_close_matches() suggests up to three similar words that score at least 0.6, and unified_diff() prints a patch from two lists of lines.
  • The string constants make quick character checks, and string.Template fills $name placeholders from a mapping, while safe_substitute() keeps the ones it cannot fill.
  • string.Template is the format for text other people edit, and t-strings are a different tool, written in code.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.