/SDK reference

Python SDK /

PII redaction for Python. Sync and async support, country inference, ~9.5 ms per page — or ~4.8 ms with the optional RE2 prefilter.

Installation

Terminal
$ pip install euredact

The core install has no dependencies. The optional euredact[fast] extra adds an RE2 scan prefilter: one DFA pass per 1 KB window reports which patterns can match there, so the rest are skipped. Measured at ~2.0x on a 2,000-character page. Output is unchanged by construction — the prefilter only decides which patterns are worth running, and each survivor is then run over the whole text.

If you installed euredact[fast] at 0.3.1, upgrade. In that release the windowed scan could miss SECRET patterns wider than the window — a PEM private key passed through unmasked. The pure-Python default and the Node.js package were never affected. Fixed in 0.3.2.

Upgrading from 0.6.0. Four changes in 0.7.0 can affect your code:

  • Every credential is SECRET; CREDENTIAL is now a legacy alias that resolves to it. The Irish PPSN, the Austrian SVNr and the UK National Insurance number are SSN, not NATIONAL_ID.
  • A span the cloud model found reports confidence None instead of "high"; filtering on "high" keeps checksum-backed rules detections only.
  • A daily-quota 429 raises QuotaExceededError at once instead of after every retry.
  • A cloud base URL that is not https is refused, except for localhost.

Which option do I need?

Thirteen options is a lot to read through when you know what you want to do. Start here, then follow the link for the detail.

I want to…Use
redact a document with no further setupredact(text)
restrict scope to the countries I operate incountries=["NL", "BE"]
keep detection wide but resolve ambiguitycountry_hint=["DE"]
send a prompt to an LLM and restore the replytokenize=True, then restore()
keep relationships visible across a whole sessionreferential_integrity=True
never redact my own company name or addressesallowlist=[...]
never redact anything at my own domainallowlist_domains=["acme.be"]
audit what the allowlist kept in the documentresult.exempted
catch person names, employers, job titles, diagnosesmode="cloud"
include dates of birthdetect_dates=True
redact a document too large for one callcontext= + chunk_offset=
detect an identifier the engine does not knowadd_custom_pattern()
isolate tenants from each otherone EuRedact() instance each
free PII held in memoryclear()

redact()

Main entry point. Detects and redacts PII from a text string. Returns a RedactResult with the cleaned text and a list of detections.

Signature
def redact(
    text: str,
    *,
    countries: list[str] | None = None,
    mode: str = "rules",
    referential_integrity: bool = False,
    detect_dates: bool = False,
    cache: bool = True,
) -> RedactResult
ParameterTypeDefaultDescription
textstr—Input text to scan.
countrieslist[str] | NoneNoneISO country codes the document is declared to belong to. Since 0.3.2 this scores detection rather than gating it: every pattern runs whatever you declare, and a match attributed outside the set is flagged out_of_scope rather than dropped. Declaring it is still worth 99.8% recall against 99.7% blind.
country_hintlist[str] | NoneNoneA prior that resolves ambiguity without narrowing scope or flagging anything out of scope — use it when you know the likely origin but do not want out_of_scope semantics.
contextDocumentContext | NoneNoneShares country evidence across the chunks of one document, so a chunk carrying no country signal is still scored with what the rest of the document established. Pair with chunk_offset. Caching is disabled while a context is in use, since the result no longer depends on the text alone.
chunk_offsetint0Character offset of this chunk within the whole document, so evidence spans recorded on a context point into the full text.
modestr"rules""rules" runs locally. "cloud" sends the document to the euRedact service, which adds the model-only types — person names, organisations, job titles, diagnoses. See the cloud tier section below.
referential_integrityboolFalseReplace PII with consistent labels (NAME_1, BANK_ACCOUNT_1) instead of generic [TYPE] labels. Labels persist on the instance across calls, so two documents sharing a value get the same label. Cannot be combined with tokenize.
tokenizeboolFalseReplace each value with a reversible token (EMAIL_K7Q2) and return the token-to-value mapping in result.tokens, for restore(). Tokens are unique to the call, so nothing is retained. Cannot be combined with referential_integrity.
allowlistlist[str] | NoneNoneValues never to redact — your own organisation's name, its own addresses. Matched whole and case-insensitively, and merged with the instance's list. Structured identifiers also match across spacing and hyphenation. Applies to cloud-tier types too.
allowlist_domainslist[str] | NoneNoneDomains whose addresses are never redacted, e.g. ["acme.be"]. EMAIL and URL only, subdomains covered, matched on a label boundary — so acme.be does not exempt evilacme.be.
detect_datesboolFalseInclude DOB/date-of-death detection. Off by default because it requires context.
cacheboolTrueCache results for identical inputs. Set False for one-off calls on sensitive text, or when timing the engine.
corefboolFalseReserved for pronoun/coreference resolution and not yet implemented. Accepted and ignored in rules mode; raises in cloud mode. Listed because it is accepted, not because it does anything.
coref_modelstr"default"Reserved, with coref. Not yet implemented.

Example

example.py
import euredact

result = euredact.redact(
    "Mijn BSN is 111222333 en IBAN NL91ABNA0417164300.",
    countries=["NL"],
)

print(result.redacted_text)
# "Mijn BSN is [NATIONAL_ID] en IBAN [BANK_ACCOUNT]."

print(result.detections)

countries takes a list. Since 0.3.3 a bare string raises TypeError, on every entry point, in both SDKs. Before that, countries="NL" was iterated character by character into the codes "N" and "L"; neither resolves, so the call declared nothing and every detection came back flagged out_of_scope while the redacted text still looked correct. A pipeline filtering on that field — which these docs tell you to do — kept none of them. A wrong country code still only warns; a wrong type has no correct reading to fall back on.

redact_batch()

Batch redaction. More efficient than calling redact() in a loop because it loads configs once.

Signature
def redact_batch(
    texts: list[str],
    **kwargs,
) -> list[RedactResult]
ParameterTypeDefaultDescription
textslist[str]—List of input texts to redact.
**kwargs—Same keyword arguments as redact() (countries, mode, referential_integrity, detect_dates, cache).

Example

batch_example.py
import euredact

texts = [
    "BSN 111222333",
    "IBAN NL91ABNA0417164300",
]

results = euredact.redact_batch(texts, countries=["NL"])
for r in results:
    print(r.redacted_text)

aredact()

Async version of redact(). Offloads CPU work to a thread pool. Same keyword arguments and return type.

Signature
async def aredact(
    text: str,
    **kwargs,
) -> RedactResult

Example

async_example.py
import asyncio
import euredact

async def main():
    result = await euredact.aredact("BSN 111222333")
    print(result.redacted_text)

asyncio.run(main())

aredact_batch()

Async batch redaction with controlled concurrency.

Signature
async def aredact_batch(
    texts: list[str],
    *,
    max_concurrency: int = 4,
    **kwargs,
) -> list[RedactResult]
ParameterTypeDefaultDescription
textslist[str]—List of input texts to redact.
max_concurrencyint4Maximum number of concurrent tasks.
**kwargs—Same keyword arguments as redact().

redact_iter()

Lazy iterator for large datasets. Yields results one at a time without loading everything into memory.

Signature
def redact_iter(
    texts: Iterable[str],
    **kwargs,
) -> Iterator[RedactResult]

Example

iter_example.py
import euredact

texts = ["BSN 111222333", "IBAN DE89370400440532013000"]

for result in euredact.redact_iter(texts):
    print(result.redacted_text)

add_custom_pattern()

Register a custom regex pattern. Matches are reported with the given name as the entity type.

Signature
def add_custom_pattern(
    name: str,
    pattern: str,
) -> None
ParameterTypeDefaultDescription
namestr—Entity type name for matches (e.g., "EMPLOYEE_ID").
patternstr—Regular expression pattern to match.

restore()

Put the original values back into text that derives from a tokenize=Trueresult — typically an LLM’s reply to the tokenized prompt.

Signature
euredact.restore(text: str, tokens: Mapping[str, str]) -> str

tokens is result.tokens. Every occurrence is replaced, including a token a model has glued to other characters (EMAIL_P4RTs) — leaving a token behind is the worse failure. See reversible tokenization.

available_countries()

Returns a sorted list of supported ISO country codes.

example.py
import euredact

print(euredact.available_countries())  # ["AT", "BE", "BG", ...]

EuRedact class

For isolated instances with separate caches and custom patterns. Useful when different parts of your application need different configurations.

ParameterTypeDefaultDescription
max_input_lengthint10_485_760Longest document redact() accepts, in characters (~10 MB). Above it, ValueError — split the input or raise the ceiling.
allowlistlist[str] | NoneNoneValues never to redact, for every call on this instance. Merged with the per-call allowlist.
allowlist_domainslist[str] | NoneNoneDomains never to redact, for every call on this instance. Merged with the per-call value.

State held on the instance — the result cache, referential-integrity labels and custom patterns — is per instance, which is what makes one instance per tenant the right default. clear() releases the cache and the label mapping.

instance_example.py
from euredact import EuRedact

instance = EuRedact()
instance.add_custom_pattern("CASE_REF", r"CASE-\d{8}")

result = instance.redact(
    "See CASE-20260401",
    countries=["NL", "BE"],
)

print(result.redacted_text)
# "See [CASE_REF]"

The EuRedact instance exposes the same methods: redact(), redact_batch(), aredact(), aredact_batch(), redact_iter(), and add_custom_pattern().

Return Types

RedactResult

A dataclass returned by all redaction functions.

ParameterTypeDefaultDescription
redacted_textstr—The text with PII replaced by labels like [NATIONAL_ID], [BANK_ACCOUNT], etc.
detectionslist[Detection]—List of detected PII spans.
sourcestr"rules"Detection source used.
degradedboolFalseWhether results may be incomplete due to an internal issue.
inferred_countriestuple[tuple[str, float], ...]—New in 0.3.2. The countries the engine concluded the document belongs to, each with a score in 0-1, most likely first. Populated whether or not you passed countries.
evidencetuple[CountryEvidence, ...]—New in 0.3.2. The individual signals behind inferred_countries, so an attribution can be audited rather than taken on faith.
tokensdict[str, str]—Token to original value. Populated only with tokenize=True, and the only copy that exists — pass it to restore(), and drop it when you no longer need the values back.
exemptedlist[Exemption]—Every span the allowlist kept, with the rule that matched and whether it was a value or a domain rule. An exemption leaves a direct identifier in a document that otherwise claims to be redacted, so it is reported rather than silent.
detection_modestr"inferred"New in 0.3.2. "declared" when you passed countries, "inferred" otherwise (including when you passed only country_hint).

Detection

A frozen dataclass (immutable and hashable) representing a single PII detection.

ParameterTypeDefaultDescription
entity_typeEntityType | str—The type of PII detected (e.g., NATIONAL_ID, BANK_ACCOUNT).
startint—Start character offset in the original text.
endint—End character offset in the original text.
textstr—The matched PII text.
sourceDetectionSource—Detection source ("rules" or "cloud").
countrystr | None—ISO country code the detection is associated with.
confidencestr"high"The verdict on this identifier. "high" means a checksum or an unambiguous format confirmed it; "low" means the span was recognised and not confirmed — typically a labelled identifier whose check digit failed, which is masked anyway rather than printed in full. Filter on this, not on country_confidence, when you need confirmed identifiers only.
country_confidencefloat0.0New in 0.3.2. How strongly the document supports the country on this detection, 0-1. Separate from confidence, which is about the match itself: a pattern can fire unambiguously while its country attribution stays a guess.
out_of_scopeboolFalseNew in 0.3.2. True when the detection was attributed to a country outside the countries you declared. It is still redacted and still returned — filter on this field if you only want in-scope entities.

CountryEvidence

One signal supporting a country attribution. New in 0.3.2.

ParameterTypeDefaultDescription
countrystr—The ISO country code this signal points at.
sourcestr—What produced the signal, e.g. "iban_prefix", "phone_prefix", "tld".
log_oddsfloat—How much this signal moves the score. Signals accumulate, so several weak ones can outweigh a single strong one.
spantuple[int, int]—Where in the text the signal was found. Offsets are absolute when chunk_offset is passed.

DocumentContext

New in 0.3.2. Country inference reads signals out of the text, so a document processed in chunks loses accuracy at exactly the chunks that carry no signal of their own — a page of bare reference numbers between two pages full of Dutch addresses. A DocumentContext carries the evidence across chunks so the whole document is scored as one.

Chunked document
from euredact import DocumentContext, redact

ctx = DocumentContext()
offset = 0
for chunk in chunks:
    result = redact(chunk, context=ctx, chunk_offset=offset)
    offset += len(chunk)
    print(result.redacted_text)

# Every signal the document produced, spans pointing into the full text.
print(ctx.evidence())

Pass chunk_offset or the evidence spans will all point into the start of the text. Caching is disabled while a context is in use, since the result depends on the document rather than on the chunk alone.

Reuse a context only across chunks of the same document. A context shared between unrelated documents mixes their countries, which cannot cause a miss — a context influences scoring only, never which spans are found — but can attribute a value to the wrong national scheme.

Reversible tokenization

tokenize=True is for text that has to come back — a prompt you send to a model, whose reply has to read as if the values were never gone. Each value is replaced by a token that names its type and nothing else, and the result carries the mapping that turns tokens back into values.

round_trip.py
import euredact

result = euredact.redact(prompt, countries=["BE"], tokenize=True)
print(result.redacted_text)
# "Write an email to Joren at EMAIL_K7Q2 or call PHONE_P4RT about the invoice."

reply = call_your_llm(result.redacted_text)   # sees tokens, never the values
print(euredact.restore(reply, result.tokens))
# the reply, with the real address and number back in it

Within one call the same value gets the same token, so a prompt that names someone twice still reads as one person. Across calls it gets a different one: nothing is retained on the instance, result.tokens is the only copy, and two tokenized documents never reveal that they share a value. That is the opposite retention model from referential_integrity, which deliberately persists labels across calls — which is why the two cannot be combined. Reach for tokenization when you need a round trip, and for referential integrity when you need consistency across a session.

A token is TYPE_ plus four characters from an alphabet with no vowels and no 0/1/I/O, so it cannot spell anything or be misread. Tokens are kept clear of any token-shaped string already in the document, so a model’s reply can itself be redacted without restore()putting the wrong value back. Works in cloud mode too, where the person’s name is tokenized as well (PERSON_NAME_W3NB).

Batch and iterator variants tokenize each text on its own, so one mapping per text — not one across the batch.

Allowlist

Values a caller declares are not PII to them— their own organisation’s name, their own addresses. Set it per call, on the instance, or both; the two merge. It applies to cloud-tier types as well, including ORGANISATION_NAME and PERSON_NAME returned by the model.

allowlist.py
import euredact

sdk = euredact.EuRedact(allowlist=["ACME NV", "info@acme.be"])

print(sdk.redact(text, countries=["NL"]).redacted_text)
# "ACME NV: mail info@acme.be or [EMAIL] about IBAN [BANK_ACCOUNT]."

Structured identifiers match however they are spaced

A company allowlisting its own IBAN as NL91ABNA0417164300 is exempted when the document writes NL91 ABNA 0417 1643 00 or the hyphenated form — which is how an IBAN is normally printed. Applies to IBAN, BIC, card, phone, VAT, national and tax IDs, passport, licence, permit, health, chamber-of-commerce, IMEI and VIN. Free-text types stay literal, deliberately: jan.devries@acme.be and jandevries@acme.be are different mailboxes at most providers, so folding them would exempt something the caller never listed. It widens the spelling, never the scope — a different account is still redacted.

Whole domains, without listing every mailbox

domains.py
sdk = euredact.EuRedact(allowlist_domains=["acme.be"])
print(sdk.redact("Mail jan@acme.be or piet@acme.be", countries=["NL"]).redacted_text)
# "Mail jan@acme.be or piet@acme.be"

EMAIL and URL only, subdomains covered (mail.acme.be). The match is on a label boundary, so acme.be does not exempt evilacme.be — that is the security property of the feature, not an implementation detail.

Every exemption is reported

result.exemptedlists every span the allowlist kept, with the rule that matched and whether it was a value or a domain rule. A document that deliberately keeps a direct identifier can therefore be audited, rather than a reader having to take the caller’s configuration on trust.

audit.py
for e in result.exempted:
    print(e.entity_type, e.text, e.rule, e.rule_kind)
# EntityType.EMAIL jan@acme.be acme.be domain

There are deliberately no wildcards. The allowlist is the only option that turns redaction off, so an over-broad entry fails toward under-redaction and does so silently: *@* would disable email redaction entirely, and * Maesa whole family’s names once the cloud tier is on. A domain rule cannot be widened that way — it structurally cannot match a person’s name or an IBAN.

Cloud tier

The cloud tier is in closed alpha. The options below are documented because the SDK accepts them; access is by invitation while the service is in closed testing.

mode="cloud" sends the document to the euRedact service, which adds the types no pattern can match — person names, organisations, job titles, diagnoses. Credentials come from configure() or from the environment.

ParameterTypeDefaultDescription
api_keystr—Your service key. Also read from EUREDACT_API_KEY.
base_urlstr—Service endpoint. Also read from EUREDACT_BASE_URL.
timeout_sfloat—Per-request timeout.
poll_timeout_sfloat—How long to wait for a queued document to finish.
max_retriesint—Retries on a transport failure.
headersdict[str, str]—Extra headers sent with every request.

Confirmed detections only

Redaction masks both confirmed and unconfirmed spans, which is the safe default. A pipeline that acts on detections — routing a document, opening a case, filing a breach report — usually wants only the ones a checksum stands behind:

strict.py
strict = [
    d for d in result.detections
    if d.confidence == "high" and not d.out_of_scope
]

country_confidenceis not this field. It answers “how strongly does this documentlook Dutch”, and stays high for a labelled identifier whose checksum failed — the document still looks Dutch, the number is still wrong. Filtering on it to find confirmed identifiers passes values a validator rejected.

Custom Patterns

Register custom regex patterns to detect domain-specific identifiers. Custom patterns are always active regardless of the countries parameter.

custom_patterns.py
import euredact

euredact.add_custom_pattern("EMPLOYEE_ID", r"EMP-\d{6}")

result = euredact.redact("Contact EMP-123456 for details")
print(result.redacted_text)
# "Contact [EMPLOYEE_ID] for details"

Priority order:validated patterns > custom patterns > regex-only patterns.

For isolated pattern registrations, use separate EuRedact instances.

Secret Detection

euRedact automatically detects secrets and API keys using two strategies.

Known-prefix detection

Matches tokens with recognized prefixes from common services:

AWS
AKIA...
GitHub
ghp_, gho_, ghs_, github_pat_
Stripe
sk_live_, pk_live_
OpenAI / Anthropic
sk-, sk-ant-
Slack
xoxb-, xoxp-
JWT
eyJ...
SendGrid
SG.

Entropy-based detection

Flags 32+ character high-entropy strings found near context keywords such as key, token, secret, password, and their translations in 12 EU languages.

Performance

~9.5 ms
Per page (2,000 chars)
~4.8 ms with the [fast] extra
~665
Records per second
300-character record
~50 KB
Memory per country
pyahocorasick
Optional accelerator

0.3.6 is about 13% slower than 0.3.2 per page, and about 14% slower per record. The extra work is rule and vocabulary growth from 0.3.5 and 0.3.6 — support-desk terms in nine languages, wider currency lists, more month spellings. The record figure moves more than the page figure because 0.3.5 began treating a value that fills an entire field of a delimited row as context, and a 300-character record is exactly that shape.

If you are coming from 0.3.3, you will find this faster. 0.3.3 fixed a real defect — Python’s \bis Unicode-aware where JavaScript’s is ASCII-only, so a national ID written against a non-ASCII letter (ЕГН7523169263, PESELŁ44051401359) was redacted by the Node SDK and silently missed by Python — but it paid for the fix by rewriting every \bin all 303 patterns into a three-branch union, which cost roughly 4×. 0.3.4 chose the boundary per occurrence instead and gave the time back without giving back the recall.

For throughput, install euredact[fast]: the RE2 prefilter roughly halves per-page latency. Node is unaffected by any of this at ~1.6 ms per page — its \b always behaved this way.

Supported Countries

31 European countries supported out of the box.

ATBEBGCHCYCZDEDKEEELESFIFRHRHUIEISITLTLULVMTNLNOPLPTROSESISKUK

Entity Types

These are the 29 types this package emits, across all supported countries, backed by 346 pattern definitions and 44 checksum validators. IBAN is still accepted as a legacy alias on input, but detections are emitted as BANK_ACCOUNT.

27 is the Rules Engine layer. The forthcoming AI layer adds 13 more — names, addresses, medical information and the other categories that have no fixed format for a pattern to match, plus the GDPR Article 9 special-category types. What euRedact detects lists all 40 with the layer responsible for each, so a count of 40 elsewhere on this site is not a different number from this one.

[BANK_ACCOUNT][BIC][CREDIT_CARD][PHONE][EMAIL][DOB][DATE_OF_DEATH][NATIONAL_ID][SSN][TAX_ID][PASSPORT][DRIVERS_LICENSE][LICENSE_PLATE][VIN][VAT][POSTAL_CODE][IP_ADDRESS][IPV6_ADDRESS][MAC_ADDRESS][HEALTH_INSURANCE][HEALTHCARE_PROVIDER][CHAMBER_OF_COMMERCE][IMEI][GPS_COORDINATES][UUID][SOCIAL_HANDLE][SECRET][RESIDENCE_PERMIT][INTERNAL_ID]

View source on GitHub

Browse the code, report issues, or contribute.

GitHubarrow_outward