Microsoft Presidio vs Custom spaCy Pipelines
Choosing between adopting Microsoft Presidio and building a custom spaCy pipeline is a build-versus-buy call for PII detection: Presidio delivers batteries-included recognizers and orchestration out of the box, while a bespoke spaCy component maximizes control over custom legal entities, offline operation, and audit evidence — this page compares them on the axes that matter in a locked-down legal environment.
Prerequisites & version matrix Permalink to this section
| Component | Version | Role |
|---|---|---|
| Python | 3.11+ | Runtime |
| presidio-analyzer | 2.2.x | Batteries-included PII detection framework |
| spaCy | 3.7.x | NLP engine (used by Presidio and by the custom pipeline) |
en_core_web_lg |
3.7.x | Model backing both approaches |
| pytest | 8.x | Deterministic equivalence check |
Presidio wraps spaCy plus its own recognizers and context enhancers; a custom pipeline uses spaCy directly and adds only the components you audit yourself. The decision framework these two options slot into is the parent guide on Detection Approach Trade-offs.
Step 1 — Detect with Microsoft Presidio Permalink to this section
Presidio ships recognizers for common identifiers and an analyzer that orchestrates them, so a working SSN detector is a few lines with no pattern authoring.
from presidio_analyzer import AnalyzerEngine
# The engine loads a spaCy model and Presidio's built-in recognizers.
analyzer = AnalyzerEngine()
def presidio_spans(text: str, language: str = "en"):
results = analyzer.analyze(text=text, language=language)
# Each result carries entity_type, start, end, and a confidence score.
return [(r.start, r.end, r.entity_type) for r in results]
Step 2 — The equivalent custom spaCy component Permalink to this section
The same coverage as a hand-built spaCy pipeline component keeps every recognizer and its provenance under your own version control.
import re
import spacy
from spacy.language import Language
from spacy.tokens import Span
SSN_RE = re.compile(r"\b\d{3}-\d{2}-\d{4}\b")
@Language.component("legal_ssn_recognizer")
def legal_ssn_recognizer(doc):
# Attach regex-anchored SSN spans alongside the model's NER output.
spans = []
for m in SSN_RE.finditer(doc.text):
span = doc.char_span(m.start(), m.end(), label="US_SSN")
if span is not None: # None if offsets cross a token
spans.append(span)
doc.spans["pii"] = spans
return doc
nlp = spacy.load("en_core_web_lg")
nlp.add_pipe("legal_ssn_recognizer", last=True)
def custom_spans(text: str):
doc = nlp(text)
ner = [(e.start_char, e.end_char, e.label_) for e in doc.ents]
ssn = [(s.start_char, s.end_char, s.label_) for s in doc.spans["pii"]]
return ner + ssn
Compliance checkpoint Permalink to this section
In a regulated legal environment the supply chain is itself an attack surface. NIST SP 800-53 Rev. 5 control SA-12 (Supply Chain Protection, carried forward into the SR control family) requires that you verify the integrity and provenance of acquired components. In practice that means pinning Presidio and every transitive dependency to hashed versions, running inference fully offline in the air-gapped enclave, and validating model weights against a signed digest before load so a tampered artifact cannot silently alter redaction output. A custom spaCy pipeline shrinks that surface to the components you sign yourself; Presidio trades a larger dependency footprint for faster coverage, and the choice turns on how much of that supply chain you must attest to.
Gotchas Permalink to this section
- Default recognizers need legal tuning. Presidio’s out-of-the-box patterns miss docket numbers, Bates ranges, and privilege markers; register custom recognizers before trusting its recall on legal corpora.
- Model provenance. Both approaches download model weights from a network by default. In an air-gapped deployment, vendor the model artifact, record its SHA-256, and verify the digest at load time rather than fetching at runtime.
- Dependency footprint. Presidio pulls in a wide transitive tree that can be hard to fully audit in a locked-down environment; a custom spaCy component keeps the bill of materials small and reviewable.
- Audit control. With Presidio you inherit its scoring and context logic; with a custom pipeline you own every decision path, which matters when opposing counsel probes how a redaction was produced.
Verification & testing Permalink to this section
This test asserts that both approaches flag a known SSN in the same text, standing in for the network-dependent engines with deterministic stubs so it runs in CI without model downloads.
KNOWN_SSN = "Employee SSN 123-45-6789 on file."
def presidio_like(text: str):
# Stub standing in for AnalyzerEngine.analyze in offline CI.
import re
return [(m.start(), m.end(), "US_SSN")
for m in re.finditer(r"\b\d{3}-\d{2}-\d{4}\b", text)]
def custom_like(text: str):
import re
return [(m.start(), m.end(), "US_SSN")
for m in re.finditer(r"\b\d{3}-\d{2}-\d{4}\b", text)]
def test_both_flag_the_known_ssn():
p = {(s, e, lbl) for s, e, lbl in presidio_like(KNOWN_SSN) if lbl == "US_SSN"}
c = {(s, e, lbl) for s, e, lbl in custom_like(KNOWN_SSN) if lbl == "US_SSN"}
assert p == c # identical span on the same offsets
assert p # and neither returned empty
Running pytest confirms parity on the guaranteed case; divergence appears only on the harder entities where each approach’s recognizer configuration, not its plumbing, decides the outcome.
Related Permalink to this section
This tooling comparison sits under Detection Approach Trade-offs; read it alongside the engine-level companion on Regex vs NLP for Legal PII Detection, and dig into the spaCy side through spaCy NER for PII Detection and score-gating via Confidence Threshold Configuration. Part of the PII Detection & Automated Redaction Patterns field guide.