Somebody needs a copy of production data to debug an issue, or a support ticket export has to go to a contractor, or your application logs are about to be shipped to a third-party service. Each time, emails, card numbers and phone numbers go along unless you strip them first. DataMask is a single Python file, standard library only, that masks that kind of PII in plain text, CSV and JSON. With a secret key it can replace values with stable tokens, so the masked data can still be joined and counted.
Everything below was run locally with the shipped datamask.py. The outputs are copied as-is.
What it detects
The built-in rules are regular expressions for:
| Type | Example input | Default output |
|---|---|---|
jane.doe@example.com |
j******e@example.com |
|
| Card (16 digits, with or without separators) | 4111 1111 1111 1111 |
****-****-****-1111 |
| US SSN | 123-45-6789 |
[SSN_REDACTED] |
| North American phone | 415-555-0132 |
(415) ***-0132 |
| IPv4 | 192.168.10.24 |
192.168.0.0 |
key=value secrets (api_key, secret, token) with a value of 16+ chars |
api_key=sk_test_51Hx… |
[GENERIC_SECRET_REDACTED] |
Masking free text (logs, tickets, notes)
printf 'Contact jane.doe@example.com or call +1 (415) 555-0132.\nCard 4111 1111 1111 1111, SSN 123-45-6789, IP 192.168.10.24\n' > in.txt
python3 datamask.py -i in.txt -k demo-key
Contact j******e@example.com or call +(415) ***-0132.
Card ****-****-****-1111, SSN [SSN_REDACTED], IP 192.168.0.0
The defaults keep enough to stay useful: the email domain, the last four digits of the card, the phone's area code and the /16 of the IP. To mask everything with one strategy, pass --strategy:
--strategy REDACT
Contact [EMAIL_REDACTED] or call +[PHONE_REDACTED].
Card [CREDIT_CARD_REDACTED], SSN [SSN_REDACTED], IP [IPV4_REDACTED]
--strategy TOKENIZE
Contact TOK_16978AC475DBF78A or call +TOK_407080C9ACCD2486.
Card TOK_7B8FCD949B6A4A3A, SSN TOK_39D12AE6849AA1EB, IP TOK_5052DA002CECEAC6
It also reads stdin and writes stdout, so it fits in a pipe:
tail -f app.log | python3 datamask.py -k "$DATAMASK_KEY" >> app.masked.log
Tokens you can still join on
TOKENIZE isn't random. It's an HMAC-SHA256 of the value under your key, truncated to 16 hex characters:
def tokenize(self, value: str) -> str:
token = hmac.new(self.secret_key, value.encode("utf-8"), hashlib.sha256).hexdigest()
return f"TOK_{token[:16].upper()}"
The same input with the same key always gives the same token, whether you pass the key with -k or through the DATAMASK_KEY environment variable:
echo "ada.park@example.com" | python3 datamask.py -k team-secret --strategy TOKENIZE
TOK_58D0726A2F40DC76
echo "ada.park@example.com" | DATAMASK_KEY=team-secret python3 datamask.py --strategy TOKENIZE
TOK_58D0726A2F40DC76
So an analyst can still count orders per customer, or join the masked users table to the masked orders table, without ever seeing an address. Without a key, DataMask generates a random one for each run, so tokens won't match across runs. Keep the key in your secrets manager, not next to the data.
CSV with per-column rules
For structured files you choose a strategy per column in a small JSON file:
{"email": "TOKENIZE", "name": "REDACT", "card": "PARTIAL", "phone": "PARTIAL"}
cat users.csv
id,name,email,phone,card,notes
1,Ada Park,ada.park@example.com,415-555-0101,4242 4242 4242 4242,VIP since 2021
2,Luis Ortega,luis@example.org,212-555-0199,5555-5555-5555-4444,called from 10.0.4.17
python3 datamask.py -f csv -i users.csv --rules rules.json -k team-secret
id,name,email,phone,card,notes
1,[CONFIDENTIAL],TOK_58D0726A2F40DC76,(415) ***-0101,****-****-****-4242,VIP since 2021
2,[CONFIDENTIAL],TOK_9E2DBA2E53D1D11F,(212) ***-0199,****-****-****-4444,called from 10.0.0.0
Columns without a rule still go through the text scanner, which is why the IP buried in notes came out as 10.0.0.0. The CSV is processed row by row with csv.DictReader, so large files don't need to fit in memory.
JSON, at any depth
echo '{"user":{"id":7,"email":"ada.park@example.com","note":"reach me at 415-555-0101"},"orders":[{"card":"4242424242424242"}]}' > ev.json
python3 datamask.py -f json -i ev.json -k team-secret --rules rules.json
{
"user": {
"id": 7,
"email": "TOK_58D0726A2F40DC76",
"note": "reach me at (415) ***-0101"
},
"orders": [
{
"card": "****-****-****-4242"
}
]
}
The rules match on key names at any depth, and every other string value is scanned. Numbers and booleans are left alone. Notice that the email token is identical to the one in the CSV: same key, same value, same token.
Using it from Python
from datamask import DataMaskEngine, MaskingStrategy
engine = DataMaskEngine(hmac_key="team-secret")
print(engine.mask_text("refund to jane.doe@example.com"))
# refund to j******e@example.com
clean = engine.mask_structure(payload, {"email": MaskingStrategy.TOKENIZE})
That makes it easy to wire into a logging.Filter or a serializer before data leaves the process.
How it works
Each rule is a compiled regex, a default strategy and an optional format-preserving replacer. mask_text applies them in order:
for rule in self.rules:
strategy = strategy_override or rule.default_strategy
if strategy == MaskingStrategy.PARTIAL and rule.custom_replacer:
sanitized = rule.pattern.sub(rule.custom_replacer, sanitized)
elif strategy == MaskingStrategy.TOKENIZE:
sanitized = rule.pattern.sub(lambda m: self.tokenize(m.group(0)), sanitized)
...
mask_structure walks dicts and lists recursively. Keys with a rule get that strategy; other strings go through mask_text.
Limitations
- It matches patterns, it doesn't understand language. Names and street addresses in free text aren't detected. "Ada Park" stays as it is unless it sits in a column or key you've given a rule. Use field rules for structured data.
- The phone and SSN rules are North American formats. Other countries' numbers and 15-digit Amex cards aren't covered by the default rules.
- The secret rule looks for
api_key=,secret:andtoken=style pairs andBearer <token>values (for exampleAuthorization: Bearer …headers), with tokens of 16+ characters. Other credential formats need a custom rule. -
HASHis a plain SHA-256 with no key. For low-entropy values such as phone numbers, useTOKENIZE(keyed) instead.
FAQ
Is this GDPR/HIPAA compliance? No tool makes you compliant by itself. DataMask helps with data minimisation and pseudonymisation; whether that's sufficient depends on your data and your process.
Does it need dependencies? No, it's standard library only.
Can I add my own patterns? Yes. The rules are a list of PIIRule(name, sensitivity, pattern, default_strategy, custom_replacer) objects in the source, so you can append your own (an internal customer-ID format, for example).
DataMask is a one-time purchase that includes the source and README: get DataMask ($19). More details are on the DataMask product page.
Top comments (0)