canpii
Canadian PII recognizers for Microsoft Presidio: provincial health numbers, passport, CRA Business Number, bank routing and driver's licences, with English and French context, LiteLLM / MCP-gateway exports and PIPEDA / PHIPA / Quebec Law 25 presets.
Why this exists
Presidio is the de-facto open-source PII detector (the presidio-analyzer package has
millions of monthly downloads), but for Canada it ships only two recognizers —
CA_SIN and CA_POSTAL_CODE — and both are disabled by default. Canadian
health numbers, driver's licences, passports, Business Numbers and bank details are
not covered, while closed products (Microsoft Purview's PIPEDA/PHIPA templates, AWS
Bedrock Guardrails' CA_HEALTH_NUMBER) do cover them.
New country recognizers are merged into Presidio slowly, and that is reasonable for a project of its size. This package lets you use Canadian recognizers today, written in Presidio's own style, and we upstream what Presidio accepts.
Quickstart
pip install canpii
python -m spacy download en_core_web_sm
python -m spacy download fr_core_news_sm
from canpii import create_analyzer, get_preset
analyzer = create_analyzer(languages=["en", "fr"]) # spaCy *_sm models, threshold 0.4
text = "Carte RAMQ ZZZX 8812 2512, numéro d'entreprise 123456782 RT 0001."
for r in analyzer.analyze(text, language="fr", entities=get_preset("quebec_law25").entity_list()):
print(r.entity_type, text[r.start : r.end], round(r.score, 2))
CA_BN 123456782 RT 0001 1.0
CA_HEALTH_NUMBER ZZZX 8812 2512 0.85
Already have an AnalyzerEngine? Add the recognizers to its registry:
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
from canpii import register_all
analyzer = AnalyzerEngine() # your existing engine (Presidio's default uses en_core_web_lg)
register_all(analyzer.registry, languages=("en",))
text = "Patient OHIP 1234-567-897 AB, BC PHN 9000 000 018."
results = analyzer.analyze(text, language="en", score_threshold=0.4)
print(AnonymizerEngine().anonymize(text=text, analyzer_results=results).text)
# Patient OHIP <CA_HEALTH_NUMBER>, BC PHN <CA_HEALTH_NUMBER>.
register_all() also enables Presidio's own CaSinRecognizer and
CaPostalCodeRecognizer. Use a score threshold of about 0.4: weak,
context-dependent patterns are scored well below it and only cross it when a
context word ("OHIP", "carte santé", "permis de conduire", …) is nearby.
No-code / YAML setup with Presidio's RecognizerRegistryProvider:
import canpii # registers the classes so `type: predefined` entries resolve
from presidio_analyzer.recognizer_registry import RecognizerRegistryProvider
registry = RecognizerRegistryProvider(
conf_file=canpii.recognizer_registry_yaml()
).create_recognizer_registry()
Supported entities
| Entity | Province | Format | Validation | Format source | Languages |
|---|---|---|---|---|---|
CA_HEALTH_NUMBER |
ON (OHIP) | 10 digits + optional 1–2 letter version code | structure; Luhn as bonus only (community-sourced) | official | en, fr |
CA_HEALTH_NUMBER |
QC (RAMQ / NAM) | 4 letters + 8 digits with embedded birth date | structure + birth-date check | official | en, fr |
CA_HEALTH_NUMBER |
BC (PHN) | 10 digits starting with 9 | MOD-11 checksum | official | en, fr |
CA_HEALTH_NUMBER |
AB (AHCIP) | 9 digits | context-only | official | en, fr |
CA_PASSPORT |
federal | 2 letters + 6 digits | context-only | secondary | en, fr |
CA_BN |
federal | 9 digits, or BN + RT/RP/RC/… + 4 digits | program id check; Luhn as bonus only (community-sourced) | official | en, fr |
CA_BANK_ACCOUNT |
federal | TTTTT-III transit, 0IIITTTTT routing, 7–12 digit account |
context-only | official / secondary | en, fr |
CA_DRIVER_LICENSE |
ON | 1 letter + 14 digits (A1234-56789-01234) |
structure; birth date as bonus only | secondary | en, fr |
CA_DRIVER_LICENSE |
QC | 1 letter + 12 digits | context-only | secondary | en, fr |
CA_DRIVER_LICENSE |
BC | 7 digits | context-only (very weak) | secondary | en, fr |
CA_SIN, CA_POSTAL_CODE |
federal | — | upstream Presidio recognizers, enabled by register_all() |
— | en, fr |
Every rule, with its source URL and confidence level, is in
docs/entities.md. Rules that are only secondary or
community-sourced never reject a match on their own; they raise the score when
they pass (pass strict=True to also reject on failure). Rules from official
sources reject matches that fail them.
Other provinces and territories are tracked in docs/roadmap.md.
Gateways: LiteLLM, ContextForge and others
# LiteLLM presidio_ad_hoc_recognizers JSON (one copy per language)
canpii export --format litellm --preset pipeda -l en,fr -o canpii_ad_hoc.json
# LiteLLM config.yaml guardrail snippet (pii_entities_config for the preset, MASK)
canpii export --format litellm-config --preset pipeda
# IBM ContextForge PIIFilterPlugin custom_patterns (distinctive formats only)
canpii export --format contextforge --preset phipa
# Gateway-neutral regex + entity + score + context JSON
canpii export --format generic
# Presidio RecognizerRegistryProvider YAML (keeps full validation)
canpii export --format presidio-yaml --preset quebec_law25
Regex-only exports lose validation. LiteLLM's ad-hoc recognizers and gateway
custom patterns are plain regular expressions: BC's MOD-11 checksum, the RAMQ
birth-date check, the Luhn bonuses and the negative-context filter do not travel with
them. Expect more false positives than with the Python package. The ContextForge
export keeps only patterns with a base score ≥ 0.3, because gateways without context
support would otherwise redact every 7- to 12-digit number. All exported regexes avoid
look-arounds and back-references so they also compile in Rust's regex crate.
For LiteLLM, the Presidio analyzer server must have a French spaCy model loaded to use
presidio_language: fr. If you can run your own analyzer image, pip install canpii into it and load the presidio-yaml export instead: you keep every
check.
Policy presets
| Preset | Law | Highlights |
|---|---|---|
pipeda |
PIPEDA (federal private sector) | identity, government and financial numbers, contact details |
phipa |
Ontario PHIPA (health) | health numbers, names, contact details, dates and locations |
quebec_law25 |
Quebec P-39.1 as amended by Law 25 | as pipeda + dates; French first |
from canpii import get_preset
get_preset("phipa").entity_list()
Presets are a starting point, not legal advice — see docs/presets.md.
Benchmark
benchmarks/run_benchmark.py generates a labeled bilingual synthetic corpus (1,740
sentences: 740 positives with and without context words, 1,000 negatives including
phone numbers, dates, order IDs, amounts, near-miss identifiers and "hard negatives"
that put a context word next to an unrelated number) and reports precision/recall at
threshold 0.4.
Positives with context words (seed 2026, presidio-analyzer 2.2.366, spaCy *_sm):
| Entity | Gold | P (substring) | R (substring) | P (whole-word) | R (whole-word) |
|---|---|---|---|---|---|
| CA_HEALTH_NUMBER | 160 | 0.870 | 1.000 | 0.870 | 1.000 |
| CA_PASSPORT | 40 | 1.000 | 1.000 | 1.000 | 1.000 |
| CA_BN | 40 | 1.000 | 1.000 | 1.000 | 1.000 |
| CA_BANK_ACCOUNT | 100 | 0.714 | 1.000 | 0.714 | 1.000 |
| CA_DRIVER_LICENSE | 120 | 0.909 | 1.000 | 0.909 | 1.000 |
| CA_SIN (upstream) | 20 | 0.526 | 1.000 | 1.000 | 1.000 |
All positives, including sentences without context words:
| Entity | Gold | P (substring) | R (substring) | P (whole-word) | R (whole-word) |
|---|---|---|---|---|---|
| CA_HEALTH_NUMBER | 240 | 0.896 | 0.863 | 0.896 | 0.863 |
| CA_PASSPORT | 60 | 1.000 | 0.667 | 1.000 | 0.667 |
| CA_BN | 60 | 1.000 | 0.933 | 1.000 | 0.933 |
| CA_BANK_ACCOUNT | 160 | 0.714 | 0.625 | 0.714 | 0.625 |
| CA_DRIVER_LICENSE | 180 | 0.921 | 0.778 | 0.921 | 0.778 |
| CA_SIN (upstream) | 40 | 0.679 | 0.950 | 1.000 | 0.950 |
How to read this:
- This is a regression benchmark on template sentences we wrote, not a real-world accuracy estimate. Scores and negative-context words were tuned on part of the negatives; the remaining false positives come from an "untuned" set ("Patient chart 1234567890", "Bank statement page …", "Driver ID …") where a context word sits next to an unrelated number — something no pattern recognizer can resolve.
- Context-only identifiers (Alberta PHN, BC licence, bank account, passport) are intentionally missed without context words; that is what lowers recall in the second table.
- "substring" vs "whole-word" is Presidio's context matching mode.
create_analyzer()defaults to whole-word: in substring mode, upstreamCA_SIN's context word "sin" matches inside "business", so every Business Number next to the word "business" is also reported as a SIN.
Reproduce with uv run python benchmarks/run_benchmark.py --show-errors.
Relationship to upstream Presidio
- This is an independent community package, not affiliated with Microsoft or the
Presidio maintainers. It builds on Presidio's public APIs (
PatternRecognizer,RecognizerRegistry,RecognizerRegistryProvider) and follows its contribution conventions: one recognizer class per file,COUNTRY_CODE = "ca", per-language instances,invalidate_resultfor checks, source URLs in docstrings. - Credit to the Presidio maintainers and to the upstream Canadian contributors:
CA_SIN, andCA_POSTAL_CODE(presidio#2204, by GitHub user watsos). We use those recognizers as they are rather than reimplementing them. - Business Number: upstream issue presidio#2206 proposes a
CA_BNrecognizer and PR presidio#2207 by GitHub user Joey239716 implements it (open since 2026-07-29). OurCA_BNrecognizer is an independent implementation written from the CRA documentation and deliberately uses the same entity name, so switching is seamless. Once #2207 (or an equivalent) is merged, we will defer to upstream: deprecate ours for one minor release, then delegate. - Upstreaming plan: recognizers whose formats are backed by official sources (BC PHN,
RAMQ first) will be proposed to Presidio as individual PRs, in Presidio's code style,
once they have had real-world use here. See
docs/roadmap.md.
Disclaimer
- Not legal advice. Presets and documentation describe engineering starting points. Compliance with PIPEDA, PHIPA, Law 25 or any other law is your responsibility.
- Detection is probabilistic. Pattern recognizers miss identifiers and flag unrelated numbers. Do not rely on this package alone to protect sensitive data.
- Synthetic data only. All identifiers in this repository, its tests and its
documentation are generated or obviously fake. Randomly generated values can still
coincide with issued identifiers; never use them as real identifiers. Never commit,
paste or upload real identifiers — not in issues, PRs, fixtures, screenshots or
logs. See
SECURITY.md. - Government programs and agencies are named only to describe the identifiers they issue; no endorsement is implied.
Contributing
Adding a province or identifier is a well-defined task — see
CONTRIBUTING.md and the drafted good-first-issues in
docs/issues/. Commits must be signed off (DCO).
License
Apache-2.0. Presidio is MIT-licensed; see NOTICE.
Metadata
Release files for canpii 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 | Provenance | Actions |
|---|---|---|---|---|
| canpii-0.1.0.tar.gz | 58.8 kB | With provenance | Details for canpii-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| canpii-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for canpii-0.1.0-py3-none-any.whl |
Total release size: 107.5 kB
Release files / canpii-0.1.0.tar.gz
| Download URL | canpii-0.1.0.tar.gz |
|---|---|
| Size | 58.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
87c7419191a89ab9bd8537832499a379680e93fc68ab517f70c8cb7c134d9a08
|
|
BLAKE2b-256 checksum How to use checksums |
ddbd25f74489f3dc4fe02d666883762af0ee64e9b68ba9e5c877e2470651109f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 8, 2026.
Transparency logRelease files / canpii-0.1.0-py3-none-any.whl
| Download URL | canpii-0.1.0-py3-none-any.whl |
|---|---|
| Size | 48.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b47d51b321ca4cb74f5670f9b6414903d7633cc1e1ec10c094956fca73716278
|
|
BLAKE2b-256 checksum How to use checksums |
5fe7f5ac2613cad5844d2b5d4d70ad1b69fbc8ba6e2d2efdda6029488c0f1740
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 8, 2026.
Transparency logSecurity for canpii 0.1.0
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
Both files in this release have been uploaded with provenance metadata.
Attestation for canpii-0.1.0.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 Nov 8, 2026.
Transparency logA 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.