Fast local PII pseudonymization for text, JSON and LLM payloads.
Project description
Pseudonymize
Local-first semantic pseudonymization for text, JSON payloads, documents, and LLM applications.
Detect once, transform in different ways.
I am Paolo Mazza and my email is paolo@example.com.
↓
I am <PERSON_1> and my email is <EMAIL_1>.
Person detection requires an optional local NER or custom detection backend. The dependency-free core detects structured entities such as the email in this example. Pseudonymize processes data locally by default; remote processing is disabled unless explicitly configured in a future backend.
The 0.1.0a1 API is a prerelease and may change before 0.1.0.
Installation
pip install pseudonymize
The base package has no runtime dependencies and supports Python 3.11 or newer.
Thirty-second example
from pseudonymize import pseudonymize
safe_text = pseudonymize("Email paolo@example.com")
assert safe_text == "Email <EMAIL_1>"
Numbered semantic aliases are the default. Numbering starts from one for every convenience call.
Transformation modes
| Mode | Example | Identity behavior |
|---|---|---|
numbered |
<PERSON_1> |
Stable for each normalized entity inside one scope |
generic |
<PERSON> |
Does not distinguish entities of the same type |
deterministic |
<PERSON_K8M42PX7D3Q> |
Stable across calls for the same key and namespace |
redacted |
[REDACTED] |
Removes type and identity distinction |
from pseudonymize import Pseudonymizer, TransformationMode
engine = Pseudonymizer(mode=TransformationMode.GENERIC)
assert engine.process("paolo@example.com").text == "<EMAIL>"
deterministic = Pseudonymizer(
mode=TransformationMode.DETERMINISTIC,
key=b"a-32-byte-or-longer-secret-key...",
namespace="customer-42",
)
Typed redaction is available with redact(text, typed=True), producing values such as
[REDACTED_EMAIL].
Alias scope
engine.process(...), like the convenience function, uses a fresh alias scope for each call. Use
an explicit scope when multiple texts must share identity numbering:
from pseudonymize import Pseudonymizer
scope = Pseudonymizer().new_scope()
first = scope.process("paolo@example.com")
second = scope.process("maria@example.com and paolo@example.com")
assert first.text == "<EMAIL_1>"
assert second.text == "<EMAIL_2> and <EMAIL_1>"
process_batch and each process_data call also use one shared scope across their complete input.
Optional reversible mapping
Mappings are disabled by default and are available only for numbered and deterministic modes:
result = Pseudonymizer().process("paolo@example.com", include_mapping=True)
assert result.mapping == {"<EMAIL_1>": "paolo@example.com"}
assert result.restore("Reply to <EMAIL_1>.") == "Reply to paolo@example.com."
Mappings contain personal or sensitive data. They are excluded from repr(result), never logged or
persisted by the package, and must be protected separately by the application.
LLM payloads
from pseudonymize import Policy, Pseudonymizer
engine = Pseudonymizer(policy=Policy.llm())
payload = {
"messages": [
{"role": "user", "content": "Email paolo@example.com"},
{"role": "user", "content": "Use paolo@example.com again"},
],
"temperature": 0.2,
}
safe_payload = engine.process_data(payload)
Both occurrences become <EMAIL_1>. Container structure and non-string values remain unchanged,
and the input is not mutated.
Detection backends
The core includes validated rules for email addresses, phone numbers, IPv4 and IPv6 addresses, IBANs, payment cards, URL credentials, sensitive URL query values, and common credential formats.
PERSON, ORGANIZATION, and LOCATION are supported entity types but require an optional backend:
engine = Pseudonymizer(backends=[RulesBackend(), local_ner_backend])
No person detector based on capitalization heuristics is included. Exact normalized matches such
as Paolo Mazza and Paolo Mazza share an alias, but ambiguous references such as Paolo and
Mr. Mazza are not merged automatically.
Security and limitations
Pseudonymize performs pseudonymization, not guaranteed anonymization. Pseudonymized data can remain personal data. Detection has false positives and false negatives, and contextual information can remain identifying. Logs, traces, retrieved documents, tool inputs, and tool outputs need the same protection as prompts.
Deterministic mode uses HMAC-SHA256 over a versioned, domain-separated input. It requires a key of at least 32 bytes; the package never generates or persists one silently. Different tenants should use different keys or namespaces.
Migration from the initial prototype
- Numbered mode is now the default. Use
mode="deterministic"when providingkeyandnamespace. - Generic mode produces typed placeholders such as
<EMAIL>. redact(...)now produces[REDACTED]; usetyped=Truefor[REDACTED_EMAIL].- Existing
<PZ1:TYPE:IDENTIFIER>placeholders remain protected from reprocessing.
Roadmap and contributing
The full local-first document and backend plan is in VISION.md and
ROADMAP.md. See CONTRIBUTING.md before submitting changes.
Licence
Apache-2.0. Optional model licences are reviewed and documented separately.
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 pseudonymize-0.1.0a1.tar.gz.
File metadata
- Download URL: pseudonymize-0.1.0a1.tar.gz
- Upload date:
- Size: 122.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
73e26eb153f2d2f10717c9a85d0911b97648f2a2ae363a9ceb7944054d2765be
|
|
| MD5 |
292e607000b74f5b00abba0df8a8b574
|
|
| BLAKE2b-256 |
0d6e17077d6e22fec44eb665df691aed75308a462cf3200a3980c0ad38e5b935
|
Provenance
The following attestation bundles were made for pseudonymize-0.1.0a1.tar.gz:
Publisher:
release.yml on ma2za/pseudonymize
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pseudonymize-0.1.0a1.tar.gz -
Subject digest:
73e26eb153f2d2f10717c9a85d0911b97648f2a2ae363a9ceb7944054d2765be - Sigstore transparency entry: 2212379773
- Sigstore integration time:
-
Permalink:
ma2za/pseudonymize@9879058718b767770b72daeedc00db98c83ad92d -
Branch / Tag:
refs/tags/v0.1.0a1 - Owner: https://github.com/ma2za
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9879058718b767770b72daeedc00db98c83ad92d -
Trigger Event:
push
-
Statement type:
File details
Details for the file pseudonymize-0.1.0a1-py3-none-any.whl.
File metadata
- Download URL: pseudonymize-0.1.0a1-py3-none-any.whl
- Upload date:
- Size: 28.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 |
13823ef30bfb6e6998c5a73a238eacd66875d4a5f2ddb0d98319301af224838e
|
|
| MD5 |
3d57cee50523fad5fc3232ab0c59394a
|
|
| BLAKE2b-256 |
41e30d1998c9bfeeb5f68a9cbcc1ecba30a97c4717cea8e6ed6b54b2cfd448f8
|
Provenance
The following attestation bundles were made for pseudonymize-0.1.0a1-py3-none-any.whl:
Publisher:
release.yml on ma2za/pseudonymize
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pseudonymize-0.1.0a1-py3-none-any.whl -
Subject digest:
13823ef30bfb6e6998c5a73a238eacd66875d4a5f2ddb0d98319301af224838e - Sigstore transparency entry: 2212379789
- Sigstore integration time:
-
Permalink:
ma2za/pseudonymize@9879058718b767770b72daeedc00db98c83ad92d -
Branch / Tag:
refs/tags/v0.1.0a1 - Owner: https://github.com/ma2za
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9879058718b767770b72daeedc00db98c83ad92d -
Trigger Event:
push
-
Statement type: