MaskPipe
MaskPipe is a spaCy-native toolkit for detecting, refining, resolving, and redacting PII.
Use it when you want one of these workflows:
- detect PII with built-in and custom rules, then redact it
- take entities from another NER system, run overlap resolution, then redact them
- combine both approaches in one spaCy pipeline
Where maskpipe fits
Maskpipe is a refinement and masking layer. It does not replace your NER model — it makes the output of any NER model production-ready: scoring, context boosting, conflict resolution, validation, and redaction, all in a composable spaCy pipeline.
Bring your own detection source:
| Source | Use case |
|---|---|
| GLiNER | Zero-shot multilingual PII detection across 100+ languages; define entity types on the fly without retraining — covers 40+ PII categories including SSN, passport, IBAN, credit card |
| GLiNER2 | Multi-task PII detection with text classification and structured extraction in one model |
| HuggingFace NER | Fine-tuned transformer models (BERT, DeBERTa) for high-accuracy PII on standard categories; strongest choice when you have labeled data or need domain-specific models (legal, code, multilingual) |
| OpenMed | Clinical NER and HIPAA-aware de-identification for healthcare; detects clinical entities (diseases, medications, anatomy) alongside patient PII in 13 languages |
| Built-in rules | Rule-based detection for 13+ countries without an external model; high-precision validators for country-specific IDs (SSN, passport, national ID, VAT) |
Each source produces character-offset spans with a label and score. DocBuilder normalizes these into spaCy docs. The maskpipe pipeline then applies context-aware scoring, resolves overlapping spans, validates matches, and writes doc._.masked.
What MaskPipe Does
MaskPipe gives you four composable pipeline components:
recognizer: finds spans from token patterns, phrase patterns, and custom matcherscontext_enhancer: boosts scores or relabels spans from nearby contextconflict_resolver: resolves overlap and filters low-confidence spansanonymizer: writes masked output todoc._.masked
The original doc.text is never modified.
Installation
pip install maskpipe
python -m spacy download nl_core_news_sm
Requirements:
- Python 3.11-3.14
- spaCy 3.8+
Optional dependencies for examples and integrations:
pip install faker gliner transformers
Quick Start: Built-in Detection + Masking
This is the default workflow if you want MaskPipe to detect PII itself.
import spacy
from maskpipe import PipelineBuilder
from maskpipe import entities
from maskpipe.entities import nl
nlp = spacy.load("nl_core_news_sm", disable=["ner"])
builder = PipelineBuilder(nlp)
builder.add_entities([
nl.BSN.replace(redactor="[BSN]"),
entities.PHONE_NUMBER.replace(redactor="[PHONE_NUMBER]"),
entities.EMAIL.replace(redactor="[EMAIL]"),
])
nlp = builder.build()
doc = nlp("Mijn BSN is 692015644, bel me op 0612345678 of mail naar info@example.com")
print(doc.text)
print(doc._.masked)
for ent in doc.ents:
print(ent.text, ent.label_, ent._.score, ent._.replacement)
# Mijn BSN is 692015644, bel me op 0612345678 of mail naar info@example.com
# Mijn BSN is [BSN], bel me op [PHONE_NUMBER] of mail naar [EMAIL]
# 692015644 BSN 0.85 [BSN]
# 0612345678 PHONE_NUMBER 0.75 [PHONE_NUMBER]
# info@example.com EMAIL 1.0 [EMAIL]
Output model:
doc.text: original textdoc._.masked: masked textdoc.ents: resolved spans after conflict resolutionspan._.replacement: replacement chosen by the anonymizer
If no redactor is registered for a label, MaskPipe uses [LABEL].
Quick Start: External NER + Masking
This is the right setup if another model already produced entity offsets.
import spacy
from transformers import pipeline
from maskpipe import PipelineBuilder, DocBuilder, HF_NER_MAPPER
# Load your NER model
ner = pipeline("ner", model="dslim/bert-base-NER", aggregation_strategy="simple")
# Set up MaskPipe to only resolve overlaps and mask (no local detection).
nlp = spacy.load("nl_core_news_sm", disable=["ner"])
builder = PipelineBuilder(nlp, disable=["recognizer", "context_enhancer"])
nlp = builder.build()
text = "Alice works at Google. Contact her at alice@example.com or 555-1234."
results = ner(text)
# results is a list of dicts like:
# [{"word": "Alice", "start": 0, "end": 5, "entity_group": "B-PER", "score": 0.98}, ...]
doc = DocBuilder(nlp, text).with_entities(results, entity_mapper=HF_NER_MAPPER).build()
doc = nlp(doc)
print(doc._.masked)
# [PERSON] works at [ORG]. Contact her at [EMAIL] or [PHONE_NUMBER].
Why this works:
HF_NER_MAPPERnormalizes HuggingFace NER output to the canonical entity format.with_entities()converts character offsets to spaCy spans with scores.conflict_resolverdeduplicates overlapping spans and writes clean results todoc.ents.anonymizerreadsdoc.entsand generatesdoc._.masked.
Built-in Entities
Generic entities in maskpipe.entities:
| Entity | Description |
|---|---|
CREDIT_CARD |
Credit card numbers (Luhn-validated) |
CRYPTO |
Bitcoin and Ethereum wallet addresses |
DATE |
Date expressions |
EMAIL |
Email addresses |
IBAN |
International bank account numbers |
IPV4 / IPV6 |
IP addresses |
MAC_ADDRESS |
MAC / hardware addresses |
NUMBER |
Generic numeric values |
PHONE_NUMBER |
Phone numbers (international) |
URL |
Web URLs |
Country-specific entities:
| Package | Entities |
|---|---|
maskpipe.entities.australia |
AU_ABN, AU_ACN, AU_MEDICARE, AU_TFN |
maskpipe.entities.finland |
FI_PERSONAL_IDENTITY_CODE |
maskpipe.entities.india |
IN_AADHAAR, IN_GSTIN, IN_PAN, IN_PASSPORT, IN_VEHICLE_REGISTRATION, IN_VOTER |
maskpipe.entities.italy |
IT_DRIVER_LICENSE, IT_FISCAL_CODE, IT_IDENTITY_CARD, IT_PASSPORT, IT_VAT_CODE |
maskpipe.entities.korea |
KR_BRN, KR_DRIVER_LICENSE, KR_FRN, KR_PASSPORT, KR_RRN |
maskpipe.entities.nigeria |
NG_NIN, NG_VEHICLE_REGISTRATION |
maskpipe.entities.nl |
NL_BSN |
maskpipe.entities.poland |
PL_PESEL |
maskpipe.entities.singapore |
SG_FIN, SG_UEN |
maskpipe.entities.spain |
ES_NIE, ES_NIF |
maskpipe.entities.thai |
TH_TNIN |
maskpipe.entities.uk |
UK_NHS, UK_NINO, UK_PASSPORT, UK_POSTCODE, UK_VEHICLE_REGISTRATION |
maskpipe.entities.us |
ABA_ROUTING, BANK_ACCOUNT, MEDICAL_LICENSE, US_DRIVER_LICENSE, US_ITIN, US_MBI, US_NPI, US_PASSPORT, US_SSN |
Entity objects are immutable configs. Use .replace(...) to override one field without rebuilding the whole entity:
from maskpipe import entities
masked_email = entities.EMAIL.replace(redactor="[EMAIL]")
Creating Custom Entities
from maskpipe.entities import Entity
EMPLOYEE_ID = Entity(
label="EMPLOYEE_ID",
patterns=[
{"pattern": [{"TEXT": {"REGEX": r"EMP-\\d{5}"}}], "score": 0.9, "id": "employee-id"},
],
context_patterns=[
{"pattern": [{"LOWER": "employee"}]},
{"context_label": "STAFF_ID", "pattern": [{"LOWER": "staff"}, {"LOWER": "id"}]},
],
validator=lambda span: span.text.startswith("EMP-"),
redactor=lambda text: "EMP-XXXXX",
)
Supported redactors:
- fixed string:
"[MASK]" - zero-argument callable:
lambda: "generated-value" - one-argument callable:
lambda text: text[:1] + "*" * (len(text) - 1)
DocBuilder
DocBuilder converts character offsets from external NER systems into spaCy spans with scores.
Basic Usage
from maskpipe import DocBuilder
# Create a doc and add entities
doc = DocBuilder(nlp, text).with_entities(
entities=[
{"start": 0, "end": 5, "label": "PERSON", "score": 0.95},
{"start": 30, "end": 45, "label": "EMAIL", "score": 0.99},
]
).build()
doc = nlp(doc)
print(doc._.masked)
Entity Format
with_entities() expects a list of dicts with at least:
start: character offset (int)end: character offset (int)label: entity type (str)score: confidence [0.0, 1.0] (float, optional)
entities = [
{"start": 0, "end": 5, "label": "PERSON", "score": 0.95},
{"start": 30, "end": 45, "label": "EMAIL", "score": 0.99},
]
doc = DocBuilder(nlp, text).with_entities(entities).build()
Entity Mappers
Use entity_mapper to normalize different NER output formats. MaskPipe provides pre-configured mappers:
| Mapper | Use For | Key Fields |
|---|---|---|
GLINER_MAPPER |
GLiNER (x-large) | start, end, label, score |
GLINER2_MAPPER |
GLiNER2 (nested format) | nested {label: {start, end, confidence}} |
HF_NER_MAPPER |
HuggingFace NER | entity_group (or entity), start, end, score |
OPENMED_MAPPER |
OpenMed (clinical NER / HIPAA) | start, end, label, confidence |
Example with GLiNER:
from maskpipe import GLINER_MAPPER, DocBuilder
from gliner import GLiNER
model = GLiNER.from_pretrained("knowledgator/gliner-x-large")
text = "Patient John Doe, email: john@example.com"
predictions = model.predict_entities(text, labels=["person", "email"], threshold=0.5)
doc = DocBuilder(nlp, text).with_entities(predictions, entity_mapper=GLINER_MAPPER).build()
doc = nlp(doc)
print(doc._.masked)
# [PERSON], email: [EMAIL]
Example with HuggingFace NER:
from maskpipe import HF_NER_MAPPER, DocBuilder
from transformers import pipeline
ner = pipeline("ner", model="dslim/bert-base-NER")
text = "Contact: alice@example.com"
results = ner(text)
doc = DocBuilder(nlp, text).with_entities(results, entity_mapper=HF_NER_MAPPER).build()
doc = nlp(doc)
print(doc._.masked)
# Contact: [EMAIL]
Custom Mappers
Create custom mappers for other NER systems:
from maskpipe import EntityMapper
# For any system with {start, end, label, score}
custom_mapper = EntityMapper(label="type", score="confidence")
# For systems with conditional label fields
fallback_mapper = EntityMapper(
label="entity_type",
label_fallback="category", # use if entity_type not found
score="conf"
)
Batch Processing
Use build_batch() to process multiple texts at once:
docs = list(DocBuilder.build_batch(
nlp=nlp,
texts=["text1", "text2", "text3"],
entities_list=[
[{"start": 0, "end": 5, "label": "PERSON", "score": 0.9}],
[{"start": 10, "end": 20, "label": "EMAIL", "score": 0.95}],
[], # no entities
],
))
for doc in nlp.pipe(docs):
print(doc._.masked)
Context Words
Add context to help the context_enhancer component make relabeling decisions:
doc = DocBuilder(nlp, text).with_context_words(["email", "contact"]).build()
Customizing Components
PipelineBuilder
PipelineBuilder adds the default component chain in this order:
recognizercontext_enhancerconflict_resolveranonymizer
You can disable components you do not need:
from maskpipe import PipelineBuilder
builder = PipelineBuilder(
nlp,
label_mapping={"persoon": "PERSON"},
disable=["context_enhancer"],
)
Context Enhancement
Add context patterns directly to the component:
context_enhancer = nlp.get_pipe("context_enhancer")
context_enhancer.add_patterns([
{
"label": "EMAIL",
"pattern": [{"LOWER": {"IN": ["email", "mail", "e-mail"]}}],
}
])
Important:
- context patterns match by label
- score changes come from component config such as
confidence_boost context_labelcan relabel a matched spandoc._.context_wordslets you add extra context terms not present in the text
Anonymizer
anonymizer = nlp.get_pipe("anonymizer")
anonymizer.add_redactors({
"EMAIL": "[REDACTED]",
"ID": lambda: "ID-000001",
"PERSON": lambda text: text[0] + "." * (len(text) - 1),
})
The anonymizer:
- leaves
doc.textunchanged - stores masked output in
doc._.masked - stores the chosen replacement in
span._.replacement
spaCy Extensions Added by MaskPipe
Document extensions:
doc._.maskeddoc._.context_words
Span extensions:
span._.scorespan._.contextspan._.replacement
Minimal API Reference
PipelineBuilder(nlp, label_mapping=None, disable=None)
DocBuilder(
nlp,
text,
label_mapping=None,
spans_key="sc",
annotate_ents=False,
default_score=0.6,
alignment_mode="strict",
)
Entity(
label,
patterns=None,
custom_matcher=None,
validator=None,
context_patterns=None,
redactor=None,
)
Development
uv sync --dev
uv run pytest -q
To regenerate entity files from Presidio recognizers:
uv sync --group codegen
python scripts/gen_entity.py --update-all
License
MIT. See LICENSE.
Release files for maskpipe 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| maskpipe-0.1.0.tar.gz | 205.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| maskpipe-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 284.5 kB
Release files / maskpipe-0.1.0.tar.gz
| Download URL | maskpipe-0.1.0.tar.gz |
|---|---|
| Size | 205.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
00f4f2debca9564acf14f492afffd624a54d94d3c0365972d0afa9d30d22a60e
|
|
BLAKE2b-256 checksum How to use checksums |
99038dc8a48456ed348bbf1f50ed8831592c93111971920a8ab0afb43c64d441
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / maskpipe-0.1.0-py3-none-any.whl
| Download URL | maskpipe-0.1.0-py3-none-any.whl |
|---|---|
| Size | 79.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dacab6b77f4111c832c9d77e8e09741c63db357ffa413035191e4e06544f78c6
|
|
BLAKE2b-256 checksum How to use checksums |
18a6618727dfaa6eff9159c2968590047eeafda1f90176ed6ab0e6d3e0a4cf6d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|