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.
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
What you’ll learn · Before you start
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
On this page
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
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- 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.
- 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.
- 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'.
- 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.
- 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
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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
A unified diff turns two lists of lines into a patch
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
- "--- settings.ini (before)" names the old file.
- "+++ settings.ini (after)" names the new file.
- "@@ -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.
- A line that starts with a space, such as "host = 0.0.0.0", is context: it is the same in both files.
- A line that starts with a minus, such as "port = 8000", is only in the old file.
- 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
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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
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:
| Criterion | textwrap | difflib | string.Template |
|---|---|---|---|
| The job | Fit text to a width | Compare texts, find close words | Fill in values by name |
| You give it | A string and a width | Two strings or lists of lines | A template and a mapping |
| You get | Lines or one string | A diff, opcodes or matches | The filled-in string |
| Watch out for | Width is characters, not columns | Scores of long inputs | Missing values raise KeyError |
| When to choose | Console output, emails, tables | Did-you-mean, change logs, comparisons | Text 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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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.
-
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.
-
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?
-
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?
Key takeaways
textwrap.shorten(),fill(),wrap(),dedent()andindent()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, andunified_diff()prints a patch from two lists of lines.- The
stringconstants make quick character checks, andstring.Templatefills$nameplaceholders from a mapping, whilesafe_substitute()keeps the ones it cannot fill. string.Templateis the format for text other people edit, and t-strings are a different tool, written in code.
References
- textwrap: Text wrapping and filling (Python Software Foundation)
- difflib: Helpers for computing deltas (Python Software Foundation)
- string: Template strings (Python Software Foundation)
- string: Format String Syntax (Python Software Foundation)
- unicodedata: Unicode Database (Python Software Foundation)
- What's New In Python 3.14: difflib (Python Software Foundation)
- PEP 292: Simpler String Substitutions (Python Software Foundation)
- Unicode Standard Annex #11: East Asian Width (Unicode Consortium)
- Unicode Standard Annex #15: Unicode Normalization Forms (Unicode Consortium)
- wcwidth: measure the displayed width of Unicode strings in a terminal (Python Package Index)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress