Detect and reversibly mask PII / secrets in text before sending it to an LLM.
Project description
redactor
Detect and reversibly mask PII / secrets in text before you send it to an LLM — then un-mask the model's reply.
Part of the ragkit suite. Install with
pip install ragkit-redactor, thenimport redactor.
redactor scans text for things you probably don't want leaving your machine —
emails, phone numbers, SSNs, credit cards, IP addresses, URLs, API keys — swaps
them for stable placeholders like [EMAIL_1], and gives you a mapping to put the
originals back afterward. Send the redacted text to a third-party AI API, get a
response that still references the placeholders, and restore it locally.
- Pure Python standard library. No dependencies.
- Python 3.8+.
- Reversible round-trip by design (
restore(redact(t).text) == t). - Luhn-validated credit-card detection to cut down false positives.
- Pluggable custom detectors with optional validators.
Install
pip install ragkit-redactor
Local development (from redactor/):
pip install -e .
Quick Start
import redactor
text = (
"Hi, this is Alice. Reach me at alice@example.com or +1 (555) 123-4567. "
"My card is 4111 1111 1111 1111 if you need to charge the deposit."
)
result = redactor.redact(text)
print(result.text)
# Hi, this is Alice. Reach me at [EMAIL_1] or [PHONE_1].
# My card is [CREDIT_CARD_1] if you need to charge the deposit.
print(result.mapping)
# {'[EMAIL_1]': 'alice@example.com',
# '[PHONE_1]': '+1 (555) 123-4567',
# '[CREDIT_CARD_1]': '4111 1111 1111 1111'}
# --- send result.text to your LLM of choice ---
llm_reply = "Sure — I'll confirm the deposit and email [EMAIL_1] the receipt."
# Un-mask the model's output locally:
print(result.restore(llm_reply))
# Sure — I'll confirm the deposit and email alice@example.com the receipt.
The redacted text is safe(r) to send to a third-party API; the sensitive values never leave your process.
API Reference
redactor.redact(text, **kwargs) -> RedactionResult
Convenience wrapper: builds a Redactor(**kwargs) and redacts text in one call.
redactor.luhn_check(number_str) -> bool
Runs the Luhn checksum over the digits in number_str (ignoring spaces/dashes).
Used internally to validate credit-card candidates; exposed for your own use.
class Redactor(detectors=None, placeholder_template="[{type}_{n}]", mask_char=None)
detectors— list of detector names to enable. Defaults to all built-ins. Pass[]to start empty and add your own.placeholder_template— format string with{type}and{n}fields. Default"[{type}_{n}]"produces[EMAIL_1],[PHONE_2], etc.mask_char— if set (e.g."*"), matches are replaced by that character repeated to the length of the match. This mode is one-way (see below).
add_detector(name, pattern, validator=None)
Register a custom detector. pattern is a regex string or compiled pattern.
validator(str) -> bool optionally filters raw matches (return False to reject).
Custom detectors take priority over built-ins for overlap resolution.
r = redactor.Redactor()
r.add_detector("EMP_ID", r"EMP-\d{4}", validator=lambda s: s != "EMP-0000")
detect(text) -> List[Match]
Returns all matches across enabled detectors, with overlaps resolved and sorted by start offset. Overlap resolution is deterministic: earlier start wins; for the same start the longer span wins; remaining ties go to detector priority (registration order, custom detectors first).
redact(text) -> RedactionResult
Detects, then substitutes placeholders (or mask characters). Identical values get the same placeholder (dedupe). Output is built by walking matches left-to-right, so offsets never corrupt.
restore(text, mapping=None) -> str
Replaces placeholders in text with their originals using mapping (required —
a bare Redactor keeps no state). Longer placeholders are substituted first so
[X_1] can't be clobbered by [X_11].
class Match
Dataclass: type (detector name), value (original text), start, end
(offsets in the source), placeholder (assigned during redact, else None).
class RedactionResult
.text— the redacted string..matches— list ofMatchwith placeholders assigned..mapping—{placeholder: original_value}(empty inmask_charmode)..restore(model_output)— un-mask placeholders in an LLM response using this result's mapping. This is the reversible round-trip.
Built-in Detectors
| Name | What it matches |
|---|---|
EMAIL |
Email addresses |
PHONE |
US-style and international-ish numbers (+country, separators, parentheses) |
SSN |
US Social Security numbers (###-##-####) |
CREDIT_CARD |
13–16 digit sequences (spaces/dashes allowed) that pass the Luhn check |
IP_ADDRESS |
IPv4 addresses |
IPV6 |
IPv6 addresses (basic) |
URL |
http/https URLs |
API_KEY |
sk-... tokens and long hex secrets (32+ chars) |
AWS_ACCESS_KEY |
AKIA + 16 uppercase alphanumerics |
False positives & Luhn validation
Regex PII detection is a balance between catching real data and not flagging every number in your prose. A few pragmatic choices:
- Credit cards are only reported when the digits pass
luhn_check, which discards the vast majority of random 13–16 digit strings. - API keys / secrets are kept deliberately narrow (
sk-tokens and long hex blobs) rather than "anything that looks random", to avoid swallowing ordinary words and IDs. - Overlaps (e.g. an IP embedded inside a URL) collapse to a single match, so you never get doubled or garbled output.
You will still see occasional misses and occasional false hits. Tune with the
detectors list and add_detector.
Reversible placeholders vs. mask_char
There are two modes:
- Placeholder mode (default, reversible). Values become
[TYPE_n]tokens and amappingis returned. You canrestore()both your text and the LLM's reply.restore(redact(t).text, mapping) == t. mask_charmode (one-way). Setmask_char="*"and each match becomes****of equal length. No mapping is produced and restoration is impossible — use this only when you want to scrub data for display/logging, not round-trip it.
r = redactor.Redactor(mask_char="*")
print(r.redact("SSN 123-45-6789").text) # SSN ***********
A note on compliance
This library is best-effort. Regex-based detection cannot catch every form of
sensitive data, and matching data is not the same as understanding it. redactor
is a practical safety net, not a guarantee of GDPR / HIPAA / PCI-DSS
compliance or any other regulatory requirement. Review the redacted output before
relying on it, and don't treat it as a substitute for proper data-handling
controls.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ragkit_redactor-0.1.1.tar.gz.
File metadata
- Download URL: ragkit_redactor-0.1.1.tar.gz
- Upload date:
- Size: 11.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6b82c014abc34d5691b390e1317e95661d127ebb35ddc697db85f83c7545041
|
|
| MD5 |
eb5fdc3f817d55b772fc85cbd240241e
|
|
| BLAKE2b-256 |
10750cc6d0ab000660ccd2f55e42bb83a63a708ee34ad92947ced9a71f2582ab
|
Provenance
The following attestation bundles were made for ragkit_redactor-0.1.1.tar.gz:
Publisher:
publish.yml on Meet2147/pythonLibraries
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ragkit_redactor-0.1.1.tar.gz -
Subject digest:
d6b82c014abc34d5691b390e1317e95661d127ebb35ddc697db85f83c7545041 - Sigstore transparency entry: 2245613212
- Sigstore integration time:
-
Permalink:
Meet2147/pythonLibraries@ee2115e797c97cad593ca75cb7317c09dad24b80 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Meet2147
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ee2115e797c97cad593ca75cb7317c09dad24b80 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file ragkit_redactor-0.1.1-py3-none-any.whl.
File metadata
- Download URL: ragkit_redactor-0.1.1-py3-none-any.whl
- Upload date:
- Size: 11.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d38054a3d5ac2240cc92a7fd9c6a105a8fcfe995b1a24060a6a294a4ba4d4e7
|
|
| MD5 |
23419f0999ff8c91175d945e6e2ce61a
|
|
| BLAKE2b-256 |
e804e15fbd2c275e260bc265c48e72c28f1a41ce00b5adbe30beb23dac9877dd
|
Provenance
The following attestation bundles were made for ragkit_redactor-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on Meet2147/pythonLibraries
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ragkit_redactor-0.1.1-py3-none-any.whl -
Subject digest:
1d38054a3d5ac2240cc92a7fd9c6a105a8fcfe995b1a24060a6a294a4ba4d4e7 - Sigstore transparency entry: 2245613779
- Sigstore integration time:
-
Permalink:
Meet2147/pythonLibraries@ee2115e797c97cad593ca75cb7317c09dad24b80 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Meet2147
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ee2115e797c97cad593ca75cb7317c09dad24b80 -
Trigger Event:
workflow_dispatch
-
Statement type: