How to redact PII from LLM prompts in Python
To redact PII from LLM prompts in Python, pip install sether, create one Sether instance, call sether.redact_sync(prompt) before the API call, and sether.restore_sync(reply) after. Values become tokens like <EMAIL_uuid>, mapped in a local vault, so the provider never sees real data and the user never notices.
- redact_sync and restore_sync give you the round trip in two lines; the same instance must do both.
- Extras add drop-in wrappers: pip install "sether[openai]" for wrap_openai, plus wrap_anthropic and wrap_httpx.
- SetherASGIMiddleware and SetherWSGIMiddleware cover FastAPI and Flask with no extra dependency.
- Sync and async streaming are both supported, chunk-boundary safe.
- The package is v0.1.1, a young port of the TypeScript original. Same design, honest maturity gap.
Why does my Python app leak PII to LLM providers?
Because that is the default. Whatever string you pass to the OpenAI or Anthropic client goes to their servers verbatim, and user input carries names, emails, and pasted card numbers more often than anyone expects.
The fix is a redaction step in the request path: swap personal values for tokens before the call, swap them back in the reply. This post shows how in Python; the background and the tool landscape are in the complete guide.
Install
pip install sether
# or with the wrapper for your LLM client:
pip install "sether[openai]"
pip install "sether[anthropic]"
pip install "sether[httpx]"The base package is sether on PyPI, currently v0.1.1, supporting Python 3.9+. Phone detection uses the phonenumbers package, the Python counterpart of the libphonenumber-js dependency in the TypeScript build.
The redact-then-restore round trip
from sether import Sether
sether = Sether()
user_message = "Hi, I'm Sarah, my email is sarah@acme.com"
safe = sether.redact_sync(user_message)
# -> "Hi, I'm Sarah, my email is <EMAIL_uuid>"
reply = call_openai(safe) # your existing call
restored = sether.restore_sync(reply) # real values backThe rule that trips people up: the same instance must handle both directions. Redacting stores the token-to-value mapping in a vault on the instance, and restoring reads it back. A fresh instance has an empty vault and will leave tokens in the output.
Why the design works this way, and what the vault stores, is covered in Reversible PII redaction: how redact-then-restore works.
Which integration path should I use?
| Your setup | Use | Install |
|---|---|---|
| Direct OpenAI SDK calls | wrap_openai | pip install "sether[openai]" |
| Direct Anthropic SDK calls | wrap_anthropic | pip install "sether[anthropic]" |
| Any HTTP client via httpx | wrap_httpx | pip install "sether[httpx]" |
| FastAPI (or any ASGI app) | SetherASGIMiddleware | pip install sether (built in) |
| Flask (or any WSGI app) | SetherWSGIMiddleware | pip install sether (built in) |
| Full control, custom pipeline | redact_sync / restore_sync | pip install sether |
The wrappers redact on the way in and restore on the way out, so existing call sites do not change. The extras only pull in the client library they wrap; the base install stays small.
How do I cover a whole FastAPI or Flask app?
If your service proxies user text to an LLM from several routes, wrapping each call site gets tedious. SetherASGIMiddleware sits in your ASGI middleware stack instead, so request bodies are redacted before your route handlers see them and responses are restored on the way back out.
Place it like any other ASGI middleware in FastAPI, and scope it to the routes that forward text to a provider rather than your whole API. SetherWSGIMiddleware does the same job for Flask and other WSGI apps. Both ship in the base package with no extra dependency.
Does it handle streaming responses?
Yes, both sync and async streaming. This matters because streaming is where naive redaction silently fails: a value split across two chunks passes a per-chunk regex on both sides.
Sether holds back a small window of bytes until it can prove no match spans the chunk boundary. The failure mode, and how we caught it in testing before release, is written up in the chunk-boundary bug post.
What do tokens and the vault look like?
Tokens are typed: <EMAIL_uuid>, <PHONE_uuid>, and so on, with a fresh identifier per value. The type prefix keeps the model reasoning normally; the identifier is meaningless to anyone without your vault.
The vault is in-memory by default and stays inside your process. It never leaves your infrastructure, so the mapping between tokens and real values exists only where you run the code.
What gets detected?
- Basic pack (default): email, phone (via phonenumbers), credit card with Luhn validation, SSN, IPv4, IPv6, IBAN with mod-97 validation.
- Secrets pack (opt-in): AWS access keys, OpenAI keys, Anthropic keys, GitHub PATs, Slack tokens, Stripe keys, JWTs, and high-entropy strings (Shannon entropy of 3.5+ bits per character).
- Identity pack (opt-in): label-anchored name, date of birth, passport, and address, with labels recognised across Latin, CJK, Cyrillic, and Arabic scripts.
How do I test it?
Assert the round-trip property: restoring a redacted string returns the original exactly. Also assert the redacted form no longer contains the raw value.
from sether import Sether
def test_round_trip():
sether = Sether()
original = "Reach me at sarah@acme.com or +14155552671"
redacted = sether.redact_sync(original)
assert "sarah@acme.com" not in redacted
assert "+14155552671" not in redacted
restored = sether.restore_sync(redacted)
assert restored == originalLimitations and maturity, honestly
The Python package is a port of the TypeScript original, which is further along. Same design, same token format, same detector philosophy, but at v0.1.1 it is the younger sibling, and some features land in the TypeScript package first.
- Free-text names ("tell Chidi I sent it") are out of scope for pattern and label detection. That class needs NER; see Regex vs NER for PII detection.
- The default vault is in-memory, so redact and restore must happen in the same process unless you plug in a shared store.
- Redaction is a technical measure that helps with data minimisation. It is not a compliance certificate on its own.
Docs live at /docs/python, the source is at github.com/raeven-co/sether, and you can try detection in the browser sandbox first.
Frequently asked questions
- How is this different from Microsoft Presidio?
- Presidio is a batteries-included NER-based framework, excellent for batch pipelines and data teams. Sether is a lightweight request-path library: validated patterns, a restore round-trip for LLM replies, and streaming safety. Presidio has no built-in restore; Sether has no built-in NER in the core package.
- Does it work with async FastAPI handlers?
- Yes. Async streaming is supported alongside the sync API, and SetherASGIMiddleware is native ASGI, so it sits in an async FastAPI stack normally.
- Is the Python package as complete as the TypeScript one?
- Not yet. It is v0.1.1 and shares the design of the TypeScript original, but the TS package is older and gets some features first. The core round trip, wrappers, middlewares, and streaming are in place.
- Does redacting prompts make me GDPR-compliant?
- No single tool does. Redaction is a technical measure that helps, especially with data minimisation (Article 5) and technical measures (Article 32). You still need processor agreements and a lawful basis for the processing.
Sether is free and MIT-licensed: npm i @raeven-co/sether · pip install sether. Or paste some text into the live sandbox and watch the redaction happen.
Keep reading
- PII redaction for LLM applications: the complete guide (2026)How to keep names, emails, cards, and secrets out of OpenAI, Anthropic, and other LLM providers: detection methods, reversible tokenization, streaming pitfalls, and tool comparison.
- How to redact PII before sending prompts to OpenAI (TypeScript)A hands-on TypeScript tutorial: detect and redact emails, phones, cards, and API keys before they reach OpenAI, then restore them in the reply. Streaming included.
- Reversible PII redaction: how redact-then-restore worksWhy one-way masking breaks chat UX, how stable tokens and a local vault make redaction reversible, and when reversibility is the wrong choice.
- Regex vs NER for PII detection: when patterns aren't enoughWhere validated patterns win, where label anchoring helps, and where only NER catches PII. Trade-offs, local ONNX inference, and a practical recommendation ladder.