Syllabus Lesson 89 of 156 · Private AI
Private AI

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:

PieceMeansExample match
\done digit7
\sone space (or tab, or new line)
[abc]any one of the characters insideb
+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
\ba 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.sub replaces every match.
  • Passing a function to re.sub lets you decide match by match, for example by counting digits.
  • Redaction reduces risk but does not make text anonymous; context can still identify people.
Your turn

Write a function redact(text, names=()) that returns a new string where:

  1. every email address is replaced with [EMAIL];
  2. 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 like 2026-10-30, times, prices) stay as they are;
  3. every name in names is replaced with [NAME], ignoring upper or lower case, but only as a whole word (the name Ann must not change "Annual").
Replace emails first, then phones, then names. Everything else in the text must stay exactly the same. Running 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"]).

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output