python · pii redaction · openai · fastapi · tutorial

How to redact PII from LLM prompts in Python

Build a Python redact-and-restore flow with Sether. Check vault sharing, supported wrappers, streaming behavior and JSON-safe restoration.

Godfrey Lebo Founder, Sether · updated · 8 min read
In this article
Conceptual Sether illustration: Keep Python prompts deliberate. Sether™ / Field notes python Keep Python prompts deliberate. Select supported details to replace before the model.
Conceptual illustration with fictional data.
Direct answer

In Python, call `sether.redact_sync(prompt)` before a provider request and restore recognised reply tokens with the same retained vault. Stream APIs and wrappers cover supported integration paths. For JSON output, use JSON-aware restore on a complete document or parsed values; inspect outgoing fields and test missing mappings instead of assuming every sensitive detail was caught.

TL;DR
  • redact_sync and restore_sync provide a basic text round trip. Both directions need the matching retained vault.
  • 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 supported, with bounded buffering and transport-specific behavior to verify.
  • The Python 0.4.0 release adds JSON-safe restore and strict unresolved-token handling. Read the upgrade notes for changed structured-output behavior.

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.

SetherThe idea, illustrated Your map. Your side.
Original → replacement → restored reply.

Replace detected details before sending. Restore recognized replacements using the matching retained map.

Fictional example. Detection can miss values; review the edited text. Follow one prompt step by step

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

Python 3.9+
pip install sether

# or with the wrapper for your LLM client:
pip install "sether[openai]"
pip install "sether[anthropic]"
pip install "sether[httpx]"

Install sether from PyPI and review the requirements for the release you deploy. The Python 0.4.0 release notes cover the JSON-safe restore APIs and compatibility changes. Phone detection uses the phonenumbers package.

The redact-then-restore round trip

Two lines around your LLM call
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 back

Redaction stores token-to-original entries and restoration reads those entries. Reusing one instance is the simplest setup. Separate instances can restore through a shared configured vault, while a fresh default vault has no earlier mappings. Test the behavior when entries are missing, expired or evicted.

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 cover supported client request and reply shapes. The extras install the libraries those wrappers use. Keep integration fixtures for your selected SDK version and response modes; “drop-in” does not mean that every endpoint or future shape is automatically handled.

How do I cover a whole FastAPI or Flask app?

If a service forwards text from several routes, middleware can centralize part of the transformation. Verify which request and response fields it touches and keep its placement deliberate. Data redacted on inbound requests reaches your route handler already transformed; that is a different boundary from wrapping only the outbound provider call.

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 preserves surrounding text between chunks with bounded buffering, rather than scanning each chunk in isolation. Configure and test long values, final fragments and your actual event framing. The chunk-boundary post explains both the failure mode and the limits of bounded buffering.

What do tokens and the vault look like?

Tokens have a type prefix and fresh identifier per match. They reference vault entries rather than encoding originals. The model can still alter or omit them, and unmatched text or context can still identify someone, so validate the edited prompt and resulting reply.

The default vault lives in process memory. The library does not send that mapping to the model provider. If you implement a persistent or shared vault, its access controls, tenant separation and retention become part of your application’s design.

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.

test_redaction.py
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 == original

For a production acceptance fixture, capture a fictional outbound request, a reply that echoes its token, a reply with an unknown token, and an expired vault entry. If the result drives a tool, validate arguments and authorization before acting. A successful restore does not authorize sending an email or writing a record.

The full Python library remains free under MIT, including decoys and restore. Optional Pro in the official tools is a separate product tier; there is no Sether per-redaction quota in these SDK calls. See pricing for that distinction.

Limitations and maturity, honestly

Follow the release notes for the version you install rather than assuming perfect feature parity from a shared design. Python 0.4.0 and TypeScript 0.8.0 both add JSON-aware restoration, with some different streaming behavior. Review both articles if your system mixes the two languages.

  • 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 offers configurable detection and anonymization, including reversible operations such as decrypt. Sether focuses on a request-path redact/vault/restore flow. Compare the actual integration and labeled detection fixtures; neither project’s name establishes better accuracy for your data. See the comparison.

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?

They share a general design but have language- and version-specific behavior. The Python 0.4.0 release notes and Python docs describe supported APIs and current structured-output limits. Test the integration you intend to deploy.

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.

Godfrey Lebo

Written by Godfrey Lebo

Founder of Sether. Ships production software for a living and reads every comment below. More about the team.

Share: X LinkedIn Markdown

Comments

Emails and phone numbers in comments are hidden automatically. We take our own medicine.

No comments yet. Ask a question or share how you handle this. First comment gets our undivided attention.

Join the conversation

Questions, corrections and war stories are all welcome. Be kind, stay on topic.

By posting you agree to our terms. We store a one-way hash of your IP to fight spam, never the IP itself.