Skip to main content

piimask

Reversible PII masking and unmasking for the LLM era. Hide personal data before it leaves your machine — send the safe version to a model, an API, or a log — then put the real values back when the answer comes home.

PyPI Python License: MIT


Why this exists

A few years ago, "sending your data to the cloud" meant a database you controlled. Today it means pasting a customer's email, a patient's phone number, or a colleague's home address into a prompt and shipping it off to a large language model you don't own, running somewhere you can't see, that may log or train on what it receives.

Most of the time we don't even notice we're doing it. A support ticket gets summarized. A sales call gets turned into follow-up notes. A spreadsheet gets "cleaned up." Each of those helpful little automations quietly carries real people's personal information across a boundary it was never meant to cross.

piimask is a small, dependency-free library that puts a checkpoint at that boundary. It finds the personal data in a piece of text, swaps it for stable placeholders, and remembers the mapping so you can reverse it later. The model sees <EMAIL_1> instead of jane@acme.com — and does its job just as well — while the real value never leaves your process.

It won't make you compliant with GDPR or HIPAA on its own, and it isn't a replacement for good security hygiene. But it makes the safe thing the easy thing, which is usually where privacy succeeds or fails.

The idea in ten seconds

from piimask import Anonymizer

anon = Anonymizer()

safe = anon.mask("Hi, I'm Jane — email jane@acme.com or call +1 415-555-0132.")
# "Hi, I'm Jane — email <EMAIL_1> or call <PHONE_1>."

# ...send `safe` to any LLM / API / log sink you don't fully trust...

answer = "I've emailed <EMAIL_1> and left a voicemail at <PHONE_1>."
print(anon.unmask(answer))
# "I've emailed jane@acme.com and left a voicemail at +1 415-555-0132."

Mask on the way out. Unmask on the way in. The model works with tokens; you work with reality.

Install

# with uv (recommended)
uv add piimask

# or with pip
pip install piimask

Optional extras:

uv add "piimask[faker]"   # realistic fake-data masking
uv add "piimask[crypto]"  # encrypt the vault at rest
uv add "piimask[all]"     # both

The core library has zero required dependencies — it's pure Python standard library, so it installs instantly and runs anywhere Python 3.9+ does.

A real workflow: protecting an LLM call

from piimask import Anonymizer
# from openai import OpenAI   # or anthropic, or anything else

anon = Anonymizer()
client = OpenAI()

user_message = "Summarize this: Jane Doe (jane@acme.com, +1 415-555-0132) " \
               "disputes charge on card 4111 1111 1111 1111."

# 1. Mask before the data leaves your machine.
safe_message = anon.mask(user_message)

# 2. The model only ever sees tokens.
reply = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": safe_message}],
).choices[0].message.content

# 3. Restore the real values in the model's answer.
print(anon.unmask(reply))

Because the same value always maps to the same token within a session, you can feed a whole multi-turn conversation through one Anonymizer and the model will reason about <EMAIL_1> consistently across every message.

What it detects

Out of the box, piimask recognizes the kinds of PII that leak most often into prompts and logs:

Entity Examples Notes
EMAIL jane.doe+tag@sub.acme.co.uk
PHONE +1 (415) 555-0132, 07700 900123 7–15 digits; ISO dates are excluded
CREDIT_CARD 4111 1111 1111 1111 validated with the Luhn checksum
SSN 123-45-6789 US format
IBAN GB82 WEST 1234 5698 7654 32 validated with the ISO 13616 mod-97 sum
IP_ADDRESS 192.168.1.20 strict 0–255 octets
IPV6 2001:db8::1
URL https://acme.com/x trailing punctuation is left alone

Need something else — an employee ID, a policy number, an internal hostname? Add a recognizer in one line (see below).

Masking strategies

The strategy decides how a detected value is replaced. Two are reversible; two are one-way, which is exactly what you want for logs you never need to rehydrate.

Strategy Reversible Example output Best for
placeholder <EMAIL_1> LLM round-trips (the default)
fake kevin83@hotmail.com prompts that behave better on realistic input
partial j***@***.com human-readable previews & support UIs
hash <EMAIL_c066add4a0> logs & analytics — correlate without exposing
Anonymizer(strategy="partial").mask("ssn 123-45-6789")
# "ssn ***-**-6789"

Anonymizer(strategy="hash", salt="pepper").mask("a@b.com and a@b.com")
# "<EMAIL_9f2c...> and <EMAIL_9f2c...>"   # same input → same token, never reversible

hash is deliberately consistent: the same input always produces the same token, so you can still count distinct users or join two logs together — you just can never get the email back.

The vault: where reversibility lives

Every reversible mask records a token → original entry in a vault. That's what unmask reads from.

anon = Anonymizer()
anon.mask("contact jane@acme.com")

anon.vault.to_json()      # persist the mapping (e.g. between requests)
len(anon.vault)           # how many values are stored
anon.reset()              # forget everything the moment you're done

⚠️ A vault contains the real PII in cleartext. Treat it like a password. Keep it in memory for the life of a request when you can, never commit one to source control (the shipped .gitignore already blocks *.vault), and if you must store it, encrypt it:

from piimask import Vault

key = Vault.generate_key()           # needs: pip install "piimask[crypto]"
blob = anon.vault.to_encrypted_json(key)   # ciphertext bytes, safe to store
restored = Vault.from_encrypted_json(blob, key)

Store the key somewhere separate from the blob — anyone holding both can recover the data.

Custom recognizers

Anything you can describe with a regex, you can mask. Give it a name, and (optionally) a validator to reject false positives.

from piimask import Anonymizer, Recognizer

anon = Anonymizer()
anon.add_recognizer(Recognizer("EMPLOYEE_ID", r"\bEMP-\d{5}\b", priority=5))

anon.mask("ticket from EMP-12345")
# "ticket from <EMPLOYEE_ID_1>"

Lower priority numbers win when matches overlap, so a specific pattern can take precedence over a general one.

Command line

piimask installs a small CLI for quick masking and pipelines:

echo "email me at a@b.com" | piimask
# email me at <EMAIL_1>

piimask --text "call 415-555-0100" --strategy partial
piimask --detect --text "ssn 123-45-6789"   # prints detections as JSON

Functional API

Prefer plain functions? Every capability has a one-shot form:

from piimask import mask, unmask, detect

masked, vault = mask("hi jane@acme.com")
original = unmask(masked, vault)
found = detect("card 4111 1111 1111 1111")   # -> [Detection(entity_type='CREDIT_CARD', ...)]

How accurate is detection?

Honestly: good, not perfect. piimask uses well-tuned regular expressions plus checksums (Luhn for cards, mod-97 for IBANs) to keep false positives low. That approach is fast, transparent, and dependency-free — but it recognizes patterns, not meaning. It will not catch a person's name written in prose, a mailing address, or a novel identifier it has never seen.

If you need semantic detection (names, locations, organizations), pair piimask with a named-entity-recognition model and register the results as custom detections — the vault and unmasking machinery work exactly the same. Treat this library as a strong, reliable first layer, not as a guarantee. Always keep a human in the loop for anything high-stakes.

FAQ

Does the masked data ever leave my machine? Only if you send it somewhere. piimask does no network I/O of any kind. The vault lives in memory unless you explicitly serialize it.

Will the placeholders confuse the LLM? Rarely. Models handle <EMAIL_1> tokens well and keep them consistent. If a particular model does better with natural-looking input, switch to the reversible fake strategy so it sees a plausible email that still maps back to the real one.

Is this enough for GDPR / HIPAA compliance? No single library makes you compliant. piimask is a practical control that reduces exposure; compliance is about your whole system, your contracts, and your processes. Use it as one layer of several.

Contributing

Issues and pull requests are welcome. To set up locally:

uv sync --all-extras
uv run pytest

License

MIT — see LICENSE. Built by Vipul Parmar.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

piimask-0.1.0.tar.gz (24.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

piimask-0.1.0-py3-none-any.whl (21.5 kB view details)

Uploaded Python 3

File details

Details for the file piimask-0.1.0.tar.gz.

File metadata

  • Download URL: piimask-0.1.0.tar.gz
  • Upload date:
  • Size: 24.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for piimask-0.1.0.tar.gz
Algorithm Hash digest
SHA256 615d2c06f66cc4d4bb067bd5006e12f6f59975ef484a70b4b28f8f11c4a6b254
MD5 d57c6c6cf4fb84636077bf618c950a48
BLAKE2b-256 5b1128a4adfbfb544be289a0b11eed3502a8eeb17b4878cf5952e1922889be30

See more details on using hashes here.

File details

Details for the file piimask-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: piimask-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 21.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for piimask-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5a498dc70eab8c4a854bf70e0747705367a2b0d17881961ee377d4b4e4e098ad
MD5 3806009d6dc8a2e7fc0950acec7ce2d3
BLAKE2b-256 4ea6119772cb4b30e13399ba016cc654388f80956b6c8f30f4737ca328df8dcd

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.0

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page