Skip to main content

domherre

PyPI Python

Redacts Swedish personal names, personnummer and e-mail addresses from string columns in a polars DataFrame.

  • Personnummer — regex (date-shaped) + Luhn checksum.
  • Names — KB/bert-base-swedish-cased-ner, PRS entities only.
  • E-mail — regex, shape-based.

Install

pip install domherre

What you get

import polars as pl
import domherre as dh

df = pl.DataFrame({
    "id": [1, 2],
    "note": [
        "Engelbert Karlsson, personnummer 850101-0006, ringde idag.",
        "Ingen känslig info här.",
    ],
})

dh.redact(df, "note")
# note -> note_redacted
# "[NAMN], personnummer [PERSONNUMMER], ringde idag."

Redacted columns are renamed with a suffix (default _redacted) so a redacted column can never be mistaken for the source. The original column is replaced, not kept. Unlisted columns are untouched. Raises if a target name already exists.

Several columns at once:

dh.redact(df, ["note", "kommentar"])

Single string:

dh.redact_text("Anna Andersson, 850101-0006")

pandas input works and comes back as pandas. A one-time PandasPerformanceWarning fires per process; silence it with DOMHERRE_QUIET=1 if you have already made your peace with pandas.

Read before you trust us

The default model is trained on SUC 3.0 — formal written Swedish. On informal text (chat logs, free-text notes, heavy abbreviation) it will miss names, silently and without error. Personnummer and e-mail are regex and deterministic; names are not.

Sample the output before calling this compliance-grade, and use scan to assert on what you are shipping rather than assuming.

Counting what was found

redact returns just the frame. redact_with_report also returns counts, and scan counts without modifying anything:

out, report = dh.redact_with_report(df, "note")
report.rows, report.rows_with_pii
report.names, report.personnummer, report.email
report.extra            # {"[TELEFON]": 3} for extra_patterns

# guard a pipeline that must never carry PII
if dh.scan(df, ["note", "kommentar"]).rows_with_pii:
    raise ValueError("PII reached a clean layer")

Ship these counts to your metrics. Without them you have no way to notice the model quietly stopped matching anything after a version bump.

Config

config = dh.Config(
    name_tag="[NAMN]",
    personnummer_tag="[PERSONNUMMER]",
    email_tag="[EMAIL]",
    suffix="_redacted",

    backend="kb-bert",       # or "gliner", "presidio", "none", or your own
    model=None,              # backend default, or a local path: "/models/ner"
    local_only=False,        # True = never hit the network, fail loudly if not cached
    num_threads=2,           # pin to your k8s cpu limit
    batch_size=32,
    min_score=0.0,           # raise to cut false-positive names, at the cost of misses
    labels=("person",),      # gliner only
    language="sv",
    backend_options={},      # passed through to the backend

    redact_names=True,
    redact_personnummer=True,
    redact_email=True,
    validate_personnummer=True,   # False = redact on shape alone, catches typo'd numbers
    enumerate_names=False,        # [NAMN_1], [NAMN_2] instead of flat [NAMN]
    extra_patterns={},            # {"[TELEFON]": r"\b0\d{1,3}[- ]?\d{5,8}\b"}
)

dh.redact(df, "note", config)

extra_patterns is applied alongside the built-ins, and wins on an identical span — so {"[EPOST]": r"\S+@\S+\.\w+"} replaces the built-in [EMAIL] tag rather than fighting it. On partial overlap the longest span wins.

enumerate_names

Distinguishes people within a single string, so downstream analysis can still tell that two mentions refer to the same person:

"[NAMN_1] mailade [NAMN_2] om [NAMN_1]s ärende"

Numbering is per string, not per DataFrame. [NAMN_1] in row 1 and row 2 are not necessarily the same person — that would need identity resolution, which this does not do.

Backend models

Name detection is pluggable. Personnummer and e-mail detection are always regex and are not affected by the backend choice. kb-bert is the default; it is the strongest Swedish-specific name model we have measured.

Backend Install Notes
kb-bert (default) included KB/bert-base-swedish-cased-ner. Swedish-only, trained on formal text.
gliner domherre[gliner] Zero-shot, multilingual, CPU-optimised. Change labels to detect anything.
presidio domherre[presidio] Full PII framework. Bring your own configured AnalyzerEngine.
none included Skips name detection. Regex only.
dh.redact(df, "note", dh.Config(backend="gliner"))

kb-bert needs the transformers extra:

pip install domherre[transformers]

gliner

config = dh.Config(
    backend="gliner",
    model="urchade/gliner_multi_pii-v1",   # default
    labels=("person", "full name"),
    min_score=0.5,
    backend_options={"map_location": "cpu", "quantize": True},
)

Custom backends

Anything with load() and person_spans(texts) -> list[list[tuple[int, int]]] works. A plain callable works too:

def detector(texts: list[str]) -> list[list[tuple[int, int]]]:
    return [[(m.start(), m.end()) for m in pattern.finditer(t)] for t in texts]

dh.redact(df, "note", dh.Config(backend=detector))

Register by name to make it selectable like a builtin:

dh.register_backend("mine", MyBackend)
dh.available_backends()   # ['gliner', 'kb-bert', 'mine', 'none', 'presidio']

Backends are cached per (name, config), so a Deployment loads each model once.

Service deployment

Load the model at startup so the first request doesn't pay for it:

dh.load(backend="kb-bert", num_threads=2, local_only=True)

Bake the weights into the image and set local_only=True. Otherwise every cold pod pulls ~440MB from HuggingFace, and a network blip becomes a runtime failure in the middle of a batch instead of a clear error at boot.

RUN python -c "from transformers import pipeline; \
    pipeline('token-classification', model='KB/bert-base-swedish-cased-ner')"
resources:
  requests: { cpu: "1", memory: "2Gi" }
  limits:   { cpu: "2", memory: "3Gi" }

Set num_threads to match the CPU limit. Torch otherwise reads the node's core count, not the cgroup limit, and thrashes threads on a large node.

Personnummer formats detected

YYMMDD-XXXX, YYMMDD+XXXX, YYMMDDXXXX, YYYYMMDD-XXXX, YYYYMMDDXXXX, plus samordningsnummer (day + 60). All must pass the Luhn check unless validate_personnummer=False.

Not detected: reservnummer/interimsnummer (region-specific, no fixed national format), and personnummer with a typo'd control digit — set validate_personnummer=False to catch those, at the cost of redacting unrelated 10 and 12-digit numbers.

License

MIT

Metadata

Release files for domherre 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for domherre 0.1.2
File Size Uploaded Provenance
domherre-0.1.2.tar.gz 16.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for domherre 0.1.2
File Interpreter ABI Platform
domherre-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 30.1 kB

Release files / domherre-0.1.2.tar.gz

Download URL domherre-0.1.2.tar.gz
Size 16.6 kB
Tags Source
SHA-256 checksum
How to use checksums
eda51684c11c7f008028029bfc7e6b72359e354dfd7f3b7465c52072d7c00681
BLAKE2b-256 checksum
How to use checksums
715284d8c29838ffdbece6f6e44242e49bb5d3899ad7532bf3134a2de26fd7f9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release files / domherre-0.1.2-py3-none-any.whl

Download URL domherre-0.1.2-py3-none-any.whl
Size 13.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d36bf902e578e326bd816e94359ea785b3ec02337145be9f351fe76472492378
BLAKE2b-256 checksum
How to use checksums
ee9da7188d6328881e71a0cbc3873121869bb2905f6b5f1a0e7322b982775418
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Security for domherre 0.1.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for domherre-0.1.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page