Redact before it leaves
Why it matters Sometimes you do need a cloud AI, or a support ticket, or a log file shared with a colleague. Redaction means stripping out the personal details first, so what leaves your device is the useful part, not the private part. You will build a small redactor that you could really use.
The black marker
Before sharing a document, people used to go over names and numbers with a thick black marker. Redaction in code is the same idea: find the personal bits and swap them for a placeholder like [EMAIL], so the reader still knows something was there without learning what.
Redaction: replacing sensitive parts of a text with placeholders before the text is shared.
Personal data: anything that points to a real person, such as a name, email address, phone number or account number.
A new tool: regular expressions
To find "anything that looks like an email", we need a way to describe a shape of text, not one exact text. That is what a regular expression (regex for short) does. Python's built-in re module handles them.
Regular expression: a small pattern language for describing the shape of text. \d means "one digit", + means "one or more of the thing before", and [A-Za-z] means "one letter".
Here are the only pieces you need today:
| Piece | Means | Example match |
|---|---|---|
\d | one digit | 7 |
\s | one space (or tab, or new line) | |
[abc] | any one of the characters inside | b |
+ | one or more of the previous piece | \d+ matches 2026 |
{6,} | six or more of the previous piece | \d{6,} matches 1234567 |
? | the previous piece is optional | \+? matches a plus sign or nothing |
\b | a word boundary (start or end of a word) | \bAnn\b skips "Annual" |
Two functions do the work. re.sub(pattern, replacement, text) returns a new text with every match replaced. And instead of a fixed replacement you can pass a function: Python calls it for each match and uses whatever it returns. That lets you decide match by match.
import re
EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
print(EMAIL_RE.sub("[EMAIL]", "Write to maya@shopa.example today"))
# Write to [EMAIL] today
def shout(match):
return match.group(0).upper() # group(0) is the matched text
print(re.sub(r"\bcat\b", shout, "cat and category"))
# CAT and category
The r"..." in front of the pattern is a raw string: it tells Python to leave backslashes alone so the regex sees them. re.compile turns a pattern into a reusable object; you can also pass the pattern string straight to re.sub.
The tricky one: phone numbers
Phone numbers come in many shapes: +44 7700 900123, (555) 010-4477, 555.010.9988. A loose pattern catches them all, but it also catches dates like 2026-10-30. The trick is two steps: match loosely, then count the digits in a function. Most full phone numbers have 9 or more digits, while a date like this has 8, so in this lesson we use "9 or more digits" as our rule. If a match has too few digits, give it back unchanged. Real redactors need more care, because some countries use shorter numbers.
A common misconception
"Redaction makes text anonymous." It removes the obvious identifiers, but context can still give someone away: "the only left-handed nurse on ward 3" names nobody and yet points to one person. Redaction lowers the risk; it does not remove it. Over-redacting (blanking a long order number as if it were a phone) is the safe kind of mistake.
In other languages
JavaScript: the same regex works, and replace also accepts a function.
const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
const PHONE = /\+?\(?\d[\d\s().-]{6,}\d/g;
function redact(text) {
text = text.replace(EMAIL, "[EMAIL]");
return text.replace(PHONE, m => (m.match(/\d/g).length >= 9 ? "[PHONE]" : m));
}
Go: the regexp package has ReplaceAllStringFunc.
phone := regexp.MustCompile(`\+?\(?\d[\d\s().-]{6,}\d`)
out := phone.ReplaceAllStringFunc(text, func(m string) string {
n := 0
for _, c := range m { if c >= '0' && c <= '9' { n++ } }
if n >= 9 { return "[PHONE]" }
return m
})
Recap
- Redaction swaps personal details for placeholders before text leaves your device.
- Regular expressions describe the shape of text;
re.subreplaces every match. - Passing a function to
re.sublets you decide match by match, for example by counting digits. - Redaction reduces risk but does not make text anonymous; context can still identify people.
Write a function redact(text, names=()) that returns a new string where:
- every email address is replaced with
[EMAIL]; - every phone number is replaced with
[PHONE]. Treat as a phone number any run that starts with an optional+or(, then a digit, continues with digits, spaces, dots, dashes or brackets, ends with a digit, and contains at least 9 digits. Runs with fewer digits (dates like2026-10-30, times, prices) stay as they are; - every name in
namesis replaced with[NAME], ignoring upper or lower case, but only as a whole word (the nameAnnmust not change "Annual").
redact twice must give the same result as running it once. At the end, print redact("Call Maya on +44 7700 900123 or maya@shopa.example", ["Maya"]).
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.