04 Oct PII Guardrails: Detecting and Masking PII in LLM Apps
People often type personal details into chatbots without thinking. A customer may paste their email address, phone number, or even a card number into a support chat. An AI model may also repeat such details in its reply. This is a serious privacy risk, and it is one of the most common reasons to add guardrails.
In this chapter, you will learn what PII is, how to detect it with regular expressions, how to confirm card numbers with a simple check, and how to mask PII in both the user’s message and the AI’s reply.
What is PII?
PII stands for Personally Identifiable Information. It is any information that can identify a real person. Common examples are:
- Email addresses
- Phone numbers
- Government ID numbers, such as a social security number in the United States
- Credit or debit card numbers
- Home addresses
- Full names combined with other details
Why Should We Mask PII?
- Third-party providers: When you use an external AI service, the user’s message travels to that provider. Fewer personal details sent means less risk.
- Logs: Applications often store conversations for debugging. Stored personal data can be leaked or misused.
- Replies: The model may repeat private data to the wrong person.
- Rules and laws: Many places have privacy rules, such as GDPR in Europe. Check the rules that apply to your website and your visitors.
Detect, Then Mask
PII protection has two steps. First, detect where the PII is. Second, decide what to do with it. The most common choices are:
- Replace with a label: For example, change anna@example.com to [EMAIL HIDDEN]. This is simple and clear.
- Partially mask: Show only a small part, such as the last four digits of a card.
- Block: Reject the whole message and ask the user to remove the private data.
Example 1: Detecting PII
We store one pattern for each kind of PII in a dictionary. For card numbers, a pattern alone is not enough, because many long numbers, such as order numbers, look like card numbers. So we add a second check called the Luhn check. Real card numbers are built so that they pass this simple calculation, while most random numbers do not.
The card number below, 4111 1111 1111 1111, is a well-known fake test number. Never use real card numbers in tests.
import re
PII_PATTERNS = {
"CARD": r"\b\d(?:[ -]?\d){12,15}\b",
"SSN": r"\b\d{3}-\d{2}-\d{4}\b",
"EMAIL": r"[\w\.-]+@[\w\.-]+\.\w+",
"PHONE": r"\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b",
}
def passes_luhn(number_text):
digits = [int(ch) for ch in number_text if ch.isdigit()]
total = 0
parity = len(digits) % 2
for index, digit in enumerate(digits):
if index % 2 == parity:
digit = digit * 2
if digit > 9:
digit = digit - 9
total = total + digit
return total % 10 == 0
def find_pii(text):
findings = []
for label, pattern in PII_PATTERNS.items():
for match in re.finditer(pattern, text):
value = match.group()
if label == "CARD" and not passes_luhn(value):
continue
findings.append((label, value))
return findings
text1 = "Email me at anna@example.com, call 555-123-4567, SSN 123-45-6789, card 4111 1111 1111 1111."
for label, value in find_pii(text1):
print(label, "|", value)
print("---")
text2 = "Order number 1234567890123 has shipped."
print(find_pii(text2))
Output
CARD | 4111 1111 1111 1111 SSN | 123-45-6789 EMAIL | anna@example.com PHONE | 555-123-4567 --- []
Understanding the Code
- PII_PATTERNS maps a label to a regex pattern. The card pattern means: a digit, followed by 12 to 15 more digits, where each digit may have a space or dash before it. That covers card numbers with 13 to 16 digits. The SSN pattern matches the shape of three digits, two digits, and four digits separated by dashes.
- passes_luhn performs the Luhn check. It collects the digits, doubles every second digit starting from the right, subtracts 9 from any doubled digit above 9, adds everything up, and checks that the total is divisible by 10.
- find_pii loops over the patterns and uses re.finditer to get every match. For card matches, it skips any value that fails the Luhn check. It returns a list of pairs, each with a label and the value found.
- The first text contains four kinds of PII, and all are found.
- The second text has a 13-digit order number. It looks like a card number to the regex, but it fails the Luhn check, so it is correctly ignored.
Example 2: Masking PII in Messages and Replies
Now we add masking. We also write a function that partially masks a card number, so you can see the second masking style. Finally, we protect both sides of the chat: the user’s message is masked before it goes to the model, and the model’s reply is masked before it goes to the user.
import re
PII_PATTERNS = {
"CARD": r"\b\d(?:[ -]?\d){12,15}\b",
"SSN": r"\b\d{3}-\d{2}-\d{4}\b",
"EMAIL": r"[\w\.-]+@[\w\.-]+\.\w+",
"PHONE": r"\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b",
}
def passes_luhn(number_text):
digits = [int(ch) for ch in number_text if ch.isdigit()]
total = 0
parity = len(digits) % 2
for index, digit in enumerate(digits):
if index % 2 == parity:
digit = digit * 2
if digit > 9:
digit = digit - 9
total = total + digit
return total % 10 == 0
def mask_pii(text):
for label, pattern in PII_PATTERNS.items():
def replace(match, label=label):
value = match.group()
if label == "CARD" and not passes_luhn(value):
return value
return "[" + label + " HIDDEN]"
text = re.sub(pattern, replace, text)
return text
def partial_mask_card(card_text):
digits = [ch for ch in card_text if ch.isdigit()]
return "**** **** **** " + "".join(digits[-4:])
def fake_llm(prompt):
return "Thanks! I have noted your request: " + prompt + " Contact support@shop.com for updates."
def safe_chat(user_message):
safe_prompt = mask_pii(user_message)
reply = fake_llm(safe_prompt)
return mask_pii(reply)
print(partial_mask_card("4111 1111 1111 1111"))
print("---")
print(safe_chat("My card is 4111 1111 1111 1111 and my email is anna@example.com. Please refund me."))
Output
**** **** **** 1111 --- Thanks! I have noted your request: My card is [CARD HIDDEN] and my email is [EMAIL HIDDEN]. Please refund me. Contact [EMAIL HIDDEN] for updates.
Understanding the Code
- mask_pii goes through each pattern and uses re.sub to replace the matches. Instead of a fixed replacement, we give re.sub a small function named replace. It is called for every match and decides what to return.
- Inside replace, a card-like number that fails the Luhn check is returned unchanged. Everything else becomes a label such as [EMAIL HIDDEN].
- The part label=label in the function definition remembers the current label for that round of the loop. This avoids a common Python mistake with functions created inside loops.
- partial_mask_card collects the digits of a card number and keeps only the last four. This style is common on receipts, where people can recognize their card without the full number being shown.
- safe_chat masks the message first, then calls the model, then masks the reply. The card number and the user’s email never reach the model. The support email that the model added on its own is hidden before the user sees it.
What Regex Cannot Find
Pattern-based detection works well for data with a fixed shape, but it has limits.
- Names and addresses: A name like “Anna Smith” has no fixed pattern. Regex cannot tell it apart from ordinary words.
- Country differences: Phone numbers, ID numbers, and postal codes have different formats around the world. Our patterns fit only some of them, so you should adapt them for your audience.
- Disguised data: A user can write an email as “anna at example dot com” or add spaces between digits.
For stronger detection, there are tools that use trained language models to find names, places, and other entities. Libraries such as Microsoft Presidio are built for this. You can add them later when your application needs them. Regex remains a fast, free first layer.
Good Habits for PII Guardrails
- Mask PII before sending a message to an outside AI service.
- Mask PII again in the reply before showing it.
- Never write raw PII into your logs.
- Use fake data in your tests, such as the test card number in this chapter.
- Tell your users clearly how their data is handled.
Key Takeaways
- PII is information that can identify a person, such as emails, phone numbers, ID numbers, and card numbers.
- Detect PII with regex patterns, and use extra checks such as the Luhn check to avoid false alarms.
- Mask PII by replacing it with a label, partially masking it, or blocking the message.
- Protect both directions: mask the input before the model and the output before the user.
- Regex cannot catch everything, so consider stronger tools for names and addresses.
What is Next?
In the next chapter, you will learn how to block toxic and harmful content with moderation techniques.
If you liked the tutorial, spread the word and share the link and our website, Studyopedia, with others.
For Videos, Join Our YouTube Channel:Â Join Now
Read More:
- Generative AI Tutorial
- AI Ethics
- Machine Learning Tutorial
- Deep Learning Tutorial
- Ollama Tutorial
- Retrieval Augmented Generation (RAG) Tutorial
- ChatGPT Tutorial
- Microsoft Copilot Tutorial
No Comments