Text Toolkit
Regular Expressions in Practice
Chapter 10 ยท Capstone: A Text Toolkit
The earlier chapters each taught one idea. In real programs the ideas arrive together, so this last chapter builds six small helpers that you could drop into any project. Each one is short, has a clear name, and uses patterns compiled once at module level (Chapter 6), with the habits from Chapter 9 built in.
Design rules for a toolkit
- Compile once. Patterns that never change live in module-level constants with an upper-case name.
- One job per function. A function takes text and returns a result. It does not print.
- Name your groups.
m["y"]explains itself;m[3]does not. - Escape outside text. Anything that did not come from you goes through
re.escape. - Fail loudly when it matters. A parser should raise an error on a bad line, not skip it silently.
Helper 1: squeeze whitespace
The simplest and most useful helper. It collapses every run of spaces, tabs and newlines into one space and trims the ends (Chapter 4).
Helper 2: make a slug
A slug is the tidy, lower-case form of a title used in a web address. Instead of describing what to remove, describe what to keep: runs of letters and digits, then join them (Chapter 1).
Helper 3: find dates in two formats
This one combines alternation, named groups, finditer and a little ordinary Python (Chapters 2, 3, 6 and 7). A single pattern recognises both 2026-11-02 and 5 Dec 2026. Because the two alternatives need different group names, the code checks which one matched. The pattern only finds the shape; the function then checks the month name against a dictionary, which is a job for code, not for a pattern.
The made-up month 3 Foo 2020 has the right shape, so the pattern matches it, but the dictionary lookup rejects it. Splitting the work this way keeps the pattern readable.
Helper 4: read key=value pairs
Values may be bare words or quoted text containing spaces. The alternation tries the quoted form first. The helper checks is not None rather than truthiness, so an empty quoted value is kept as an empty string instead of being lost.
Helper 5: censor whole words safely
The list of words comes from outside, so each is escaped. Longer words go first so that a short word cannot win inside a longer one (Chapter 9). The usual \b boundary breaks for a word that ends in a symbol, such as c++, because there is no word boundary between + and a space. Lookaround (Chapter 5) says what is wanted instead: not preceded and not followed by a letter, digit or underscore.
Notice that concatenate is left alone, because cat inside it has letters on both sides. The empty-list check matters too: an empty alternation would build the pattern (?:), which matches everywhere.
Helper 6: mask long numbers
A replacement function (Chapter 4) can compute its output from the match. Here every run of six or more digits keeps only its last four digits; shorter numbers are untouched.
Using the helpers together
Because each helper takes text and returns text (or a simple value), they chain naturally:
| Helper | Main tools from the course | Chapter |
|---|---|---|
squeeze | re.sub, \s+ | 4 |
slugify | findall, character classes | 1 |
find_dates | alternation, named groups, finditer | 2, 3, 6, 7 |
parse_pairs | groups, alternation, finditer | 2, 6 |
censor | re.escape, lookaround, replacement function, flags | 3, 4, 5, 9 |
mask_digits | counted quantifier, replacement function | 1, 4 |
Where to go next
- Add a test for every helper, including awkward inputs: empty text, a long near-miss, text full of special characters.
- Use
re.VERBOSEand comments as soon as a pattern passes one line (Chapter 3). - When the input has nested structure, reach for a real parser (Chapter 9).
- Read the standard library documentation for the
remodule. You now know enough to follow every section of it.
Challenges
Write redact_emails(text) that replaces every email address with [email] and returns both the new text and the number of replacements. Test it on Write to ann@example.com or bob.smith+x@mail.co.uk today. and on Nothing to hide here, only a@ and @b.
Write top_words(text, n=3) that returns the n most common words, ignoring case, with words such as didn't kept whole. Test it on The cat and the hat. The CAT didn't sit; the hat didn't mind. with the default and with n=5.
Write parse_config(text) for a small settings format: [section] headers, key = value lines, blank lines and # comments (also allowed after a value). Return a dictionary of dictionaries. Use a VERBOSE pattern with named groups, and raise ValueError with the line number for an unreadable line or a key that appears before any section.
Quick Reference
- Compile once: module-level upper-case constants for fixed patterns
- One job per function: text in, result out, no printing
- Named groups:
m["name"]overm[2] - Outside text: always
re.escape, and guard against an empty list - Boundaries for symbols:
(?<!\w)and(?!\w)when\bwill not do - Pattern plus code: let the pattern find the shape and ordinary code check the meaning
- Parsers: raise an error with the line number rather than skipping bad input