FastPII Docs
Guides

Custom Detectors

Build custom detectors by subclassing the Detector base class.

Custom Detectors

You can extend FastPII by creating custom detectors. A detector implements the Detector base class with detect() and validate() methods.

Detector base class

from fastpii.detectors.base import Detector
from fastpii.models import Finding

class Detector(ABC):
    name: str
    region: str
    description: str

    @abstractmethod
    def detect(self, text: str) -> list[Finding]:
        ...

    @abstractmethod
    def validate(self, value: str) -> bool:
        ...

Creating a custom detector

import re
from fastpii.detectors.base import Detector
from fastpii.models import Finding

class EmployeeIdDetector(Detector):
    def __init__(self):
        super().__init__(
            name="employee_id",
            region="us",
            description="Employee ID in format EMP-XXXXX",
        )

    def detect(self, text: str) -> list[Finding]:
        pattern = re.compile(r"\bEMP-(\d{5})\b")
        findings = []
        for match in pattern.finditer(text):
            findings.append(Finding(
                type=self.name,
                value=match.group(),
                start=match.start(),
                end=match.end(),
                confidence=0.85,
                region=self.region,
                metadata={"employee_number": match.group(1)},
            ))
        return findings

    def validate(self, value: str) -> bool:
        return bool(re.fullmatch(r"EMP-\d{5}", value))

Registering a custom detector

from fastpii import FastPII, DEFAULT_PRIORITY
from fastpii.countries.cz import CzechPack

engine = FastPII(priority=DEFAULT_PRIORITY)
engine.register(CzechPack())

custom_detector = EmployeeIdDetector()
engine.register_detector(custom_detector)

result = engine.detect("Contact EMP-12345 for details")
print(result.findings[0].value)
# EMP-12345

Custom metadata extraction

Override _extract_metadata() to provide additional metadata during validation:

class EmployeeIdDetector(Detector):
    def __init__(self):
        super().__init__(
            name="employee_id",
            region="us",
            description="Employee ID in format EMP-XXXXX",
        )

    def detect(self, text: str) -> list[Finding]:
        pattern = re.compile(r"\bEMP-(\d{5})\b")
        findings = []
        for match in pattern.finditer(text):
            findings.append(Finding(
                type=self.name,
                value=match.group(),
                start=match.start(),
                end=match.end(),
                confidence=0.85,
                region=self.region,
                metadata={"employee_number": match.group(1)},
            ))
        return findings

    def validate(self, value: str) -> bool:
        return bool(re.fullmatch(r"EMP-\d{5}", value))

    def _extract_metadata(self, value: str) -> dict:
        match = re.fullmatch(r"EMP-(\d{5})", value)
        if match:
            return {"employee_number": match.group(1), "checksum_valid": True}
        return {"checksum_valid": False}

Priority for custom detectors

Add your custom detector type to the priority dict when creating the engine:

priority = {
    **DEFAULT_PRIORITY,
    "employee_id": 75,
}

engine = FastPII(priority=priority)

Higher priority values win overlap conflicts against lower-priority detectors.

On this page