A Python package for zero-shot text anonymization using Transformer-based NER models.
Python
112
88 commits
updated Oct 10, 2026
Documentation · Tutorial · API reference · Contributing · Code of Conduct
Zink predicts entity spans using a GLiNER based model. Predictions can miss sensitive text, so review results before releasing research data. Numbered redaction keeps original values in a local mapping file and is pseudonymization rather than irreversible anonymization.
Zink combines user supplied entity labels with redaction, synthetic replacement, and persistent placeholder mapping in one Python API. It uses GLiNER for extraction rather than introducing a new recognition model.
| Tool | Main focus | Difference for a researcher |
|---|---|---|
| PyRedactKit | CLI redaction for built-in patterns and custom regular expressions | Zink can request semantic entity types at inference time; PyRedactKit is useful for known patterns in files. |
| Microsoft Presidio | Extensible PII detection and anonymization framework | Presidio has a broader set of recognizers and operators, including ML integrations. Zink provides a compact workflow around a default zero-shot model. |
| scrubadub | Text cleaning using configurable detectors | Zink exposes arbitrary label prompts to its entity model in the redaction call. |
| GLiNER | Zero-shot entity extraction | Zink builds on GLiNER and adds redaction, replacements, and mapping. |
See the full comparison. These are workflow differences; no head-to-head performance claim is made.
The proliferation of Large Language Models (LLMs) heightens challenges in protecting Personal Identifiable Information (PII), particularly Quasi-Identifiers (QIs), in unstructured text. QIs enable re-identification when combined and pose significant privacy risks, highlighted by their use in security verification. Current approaches face limitations: large LLMs offer flexibility for detecting diverse QIs but are often hindered by high computational costs, while traditional supervised NER models require domain-specific labeled data and fail to generalize to heterogeneous, unseen QI types. Furthermore, evaluating QI identification methods is hampered by the lack of diverse benchmarks. To address this need for evaluation resources, we present the Quasi-Identifier Benchmark (QIB), a new corpus with 1750 examples across 35 diverse QI categories (e.g., personal preferences, security answers) designed to assess model robustness against QI heterogeneity. To facilitate the application of flexible identification methods on such diverse data, we also introduce ZINK (Zero-shot INK), a Python package providing a unified framework for applying existing zero-shot NER models to QI identification and anonymization, simplifying model integration and offering configurable redaction and replacement.
Evaluation using ZINK on QIB shows strong performance, achieving an F4-score of 0.9206. This result outperforms both supervised models like BERT (0.6109) and paid large language models like GPT-4-Nano (0.9007), while remaining competitive with top-tier models like GPT-4 (0.9726). QIB and ZINK provide valuable resources enabling standardized evaluation and development of flexible, practical solutions for quasi-identifier anonymization in text.
Install the package using uv or pip.
CPU Support (Recommended for most users):
uv add "zink[cpu]"
# or
pip install "zink[cpu]"
GPU Support:
pip install "zink[gpu]"
First import: With a CPU or GPU backend installed, importing Zink downloads approximately 2 GB of model files from Hugging Face and loads the model. This can take several minutes, depending on your connection and hardware. Allow extra disk space for dependencies and the cache. Later imports reuse the cache; download and initialize the model before working offline. Installing bare
zinkemits a warning at import; inference requires the CPU or GPU extra.
Get started with ZINK in just a few lines of code. The redact function replaces identified entities with [LABEL]_REDACTED.
import zink as zn
text = "John works as a doctor and plays football after work and drives a toyota."
labels = ("person", "profession", "sport", "car")
result = zn.redact(text, labels)
print(result.anonymized_text)
Output:
person_REDACTED works as a profession_REDACTED and plays sport_REDACTED after work and drives a car_REDACTED.
Instead of simple redaction, you can replace entities with realistic synthetic data using the replace function. ZINK uses the Faker library to generate context-aware replacements.
import zink as zn
text = "John Doe dialled his mother at 992-234-3456 and then went out for a walk."
labels = ("person", "phone number", "relationship")
result = zn.replace(text, labels)
print(result.anonymized_text)
Possible Output:
Warren Buffet dialled his Uncle at 2347789287 and then went out for a walk.
You can provide your own dictionary of replacements for specific labels. This is useful for consistent mapping or using domain-specific datasets.
import zink as zn
text = "Melissa works at Google and drives a Tesla."
labels = ("person", "company", "car")
custom_replacements = {
"person": "Alice",
"company": "OpenAI",
"car": ("Honda", "Toyota")
}
result = zn.replace_with_my_data(text, labels, user_replacements=custom_replacements)
print(result.anonymized_text)
Possible Output:
Alice works at OpenAI and drives a Honda.
Protect sensitive data in your RAG pipelines or API calls using the @zink.shield decorator. It automatically anonymizes inputs before they reach the function and re-identifies the output, creating a secure "shield" around your logic.
import zink as zn
# This mock function simulates calling an external API (like OpenAI or Gemini)
@zn.shield(target_arg='prompt', labels=('person', 'company'))
def call_sensitive_api(prompt: str):
# The prompt received here is already anonymized.
# e.g., "Report for person_REDACTED from company_REDACTED."
return f"Analysis for {prompt} is complete."
# The original, sensitive text
sensitive_data = "Report for John Doe from Acme Inc."
# Call the function normally. The decorator handles anonymization and re-identification.
final_result = call_sensitive_api(prompt=sensitive_data)
print(final_result)
Output:
Analysis for John Doe from Acme Inc. is complete.
To ensure consistent redaction across multiple sessions or calls, you can use the numbered_entities=True flag. This will automatically use a persistent mapping file stored in your home directory (~/.zink/mapping.json).
import zink as zn
# First call: Generates IDs and saves them to default mapping file
text1 = "My name is Alice."
result1 = zn.redact(text1, labels=["person"], numbered_entities=True)
print(result1.anonymized_text)
# Output: person_1234_REDACTED ...
# Second call: Reuses the SAME ID for 'Alice'
text2 = "Alice is here again."
result2 = zn.redact(text2, labels=["person"], numbered_entities=True)
print(result2.anonymized_text)
# Output: person_1234_REDACTED ...
# Check where the mapping file is stored
print(zn.where_mapping_file())
# Clear the mapping file to start fresh
zn.refresh_mapping_file()
You can run zink easily using Docker. The image comes with the model pre-installed and exposes a REST API.
Run this command in the root of the repository:
docker build -t zink .
Start the API server on port 8000:
docker run --rm -p 8000:8000 zink
Tip: If port 8000 is already in use (e.g., you see a "Bind for 0.0.0.0:8000 failed" error), map it to a different port like 8001:
docker run --rm -p 8001:8000 zink
Once the container is running, you can send requests to the /redact endpoint.
Example Request:
curl -X POST "http://localhost:8000/redact" \
-H "Content-Type: application/json" \
-d '{"text": "John Doe lives in New York.", "labels": ["person", "location"]}'
(Note: If you used port 8001, replace localhost:8000 with localhost:8001)
Example Response:
{
"original_text": "John Doe lives in New York.",
"anonymized_text": "<PERSON> lives in <LOCATION>.",
"replacements": [
{
"label": "person",
"original": "John Doe",
"pseudonym": "<PERSON>",
"start": 0,
"end": 8,
"score": 0.99
},
{
"label": "location",
"original": "New York",
"pseudonym": "<LOCATION>",
"start": 18,
"end": 26,
"score": 0.99
}
]
}
To verify the installation, you can run the test suite inside the container:
docker run --rm zink pytest zink/tests
GLiNER is a Named Entity Recognition (NER) model capable of identifying any entity type using a bidirectional transformer encoder (BERT-like). It provides a practical alternative to traditional NER models, which are limited to predefined entities, and Large Language Models (LLMs) that, despite their flexibility, are costly and large for resource-constrained scenarios.
NuNerZero is a compact, zero-shot Named Entity Recognition model that leverages the robust GLiNER architecture for efficient token classification. It requires lower-cased labels and processes inputs as a concatenation of entity types and text, enabling it to detect arbitrarily long entities.
Zink leverages both the Faker library and the extensive training data from the GLiNER model to generate realistic, synthetic replacements for sensitive information.
A reproducible QIB comparison evaluates Zink and the GLiNER model used in Presidio's official integration example with the same semantic label prompts. It reports micro-averaged F4, precision and recall, CPU latency, throughput and process memory. Scores use exact target boundaries: unmatched predictions count as false positives and missed targets as false negatives. The methodology includes setup and reproduction commands.
Here is a comparison of ZINK against other models on Quasi Identifier Benchmark ([QIB])(https://huggingface.co/datasets/deepanwa/QIB)
These are the project's reported QIB results, not a head-to-head comparison with the redaction packages above. The new reproducible QIB comparison uses a separate exact-span scoring protocol and does not reproduce these historical numbers; verify their original conditions before using them as evidence in a submission.
| Model | Overall Recall | Overall Precision | Overall F4_SCORE | True Positives (TP) | False Negatives (FN) | Total Redaction Markers |
|---|---|---|---|---|---|---|
| gpt_41_nano | 0.8971 | 0.962 | 0.9007 | 1570 | 180 | 1632 |
| gpt_41 | 0.9726 | 0.9737 | 0.9726 | 1702 | 48 | 1748 |
| zink_single | 0.9 | 0.8858 | 0.8992 | 1575 | 175 | 1778 |
| zink_topic | 0.9126 | 0.6502 | 0.8914 | 1597 | 153 | 2456 |
| zink_human (run 1) | 0.9371 | 0.6597 | 0.9145 | 1640 | 110 | 2486 |
| zink_human (run 2) | 0.9446 | 0.6544 | 0.9206 | 1653 | 97 | 2526 |
| tars_topic | 0.5983 | 0.762 | 0.6059 | 1047 | 703 | 1374 |
| bert | 0.628 | 0.4255 | 0.6109 | 1099 | 651 | 2583 |
To install the development version from a clone of this repository (Python 3.11+):
python -m pip install -e '.[cpu]'
python -m pip install pytest sphinx sphinx-rtd-theme
See CONTRIBUTING.md for a virtual environment setup and documentation checks.
To run the tests, navigate to the project directory and execute:
pytest
Contributions are welcome! Please feel free to submit pull requests or open issues to suggest improvements or report bugs.
git checkout -b feature/your-featuregit commit -m 'Add your feature'git push origin feature/your-featureIf you are using this package for your work/research, use the below citation:
Wadhwa, D. (2025). ZINK: Zero-shot anonymization in unstructured text. (v0.2.1). Zenodo. https://doi.org/10.5281/zenodo.15035072
This project is licensed under the Apache 2.0 License.
92 followers · starred Aug 2025
189 followers · starred Nov 2025
32 followers · starred Jul 2025
59 followers · starred Dec 2025
A Python package for zero-shot text anonymization using Transformer-based NER models.
Python
112
88 commits
updated Oct 10, 2026
Documentation · Tutorial · API reference · Contributing · Code of Conduct
Zink predicts entity spans using a GLiNER based model. Predictions can miss sensitive text, so review results before releasing research data. Numbered redaction keeps original values in a local mapping file and is pseudonymization rather than irreversible anonymization.
Zink combines user supplied entity labels with redaction, synthetic replacement, and persistent placeholder mapping in one Python API. It uses GLiNER for extraction rather than introducing a new recognition model.
| Tool | Main focus | Difference for a researcher |
|---|---|---|
| PyRedactKit | CLI redaction for built-in patterns and custom regular expressions | Zink can request semantic entity types at inference time; PyRedactKit is useful for known patterns in files. |
| Microsoft Presidio | Extensible PII detection and anonymization framework | Presidio has a broader set of recognizers and operators, including ML integrations. Zink provides a compact workflow around a default zero-shot model. |
| scrubadub | Text cleaning using configurable detectors | Zink exposes arbitrary label prompts to its entity model in the redaction call. |
| GLiNER | Zero-shot entity extraction | Zink builds on GLiNER and adds redaction, replacements, and mapping. |
See the full comparison. These are workflow differences; no head-to-head performance claim is made.
The proliferation of Large Language Models (LLMs) heightens challenges in protecting Personal Identifiable Information (PII), particularly Quasi-Identifiers (QIs), in unstructured text. QIs enable re-identification when combined and pose significant privacy risks, highlighted by their use in security verification. Current approaches face limitations: large LLMs offer flexibility for detecting diverse QIs but are often hindered by high computational costs, while traditional supervised NER models require domain-specific labeled data and fail to generalize to heterogeneous, unseen QI types. Furthermore, evaluating QI identification methods is hampered by the lack of diverse benchmarks. To address this need for evaluation resources, we present the Quasi-Identifier Benchmark (QIB), a new corpus with 1750 examples across 35 diverse QI categories (e.g., personal preferences, security answers) designed to assess model robustness against QI heterogeneity. To facilitate the application of flexible identification methods on such diverse data, we also introduce ZINK (Zero-shot INK), a Python package providing a unified framework for applying existing zero-shot NER models to QI identification and anonymization, simplifying model integration and offering configurable redaction and replacement.
Evaluation using ZINK on QIB shows strong performance, achieving an F4-score of 0.9206. This result outperforms both supervised models like BERT (0.6109) and paid large language models like GPT-4-Nano (0.9007), while remaining competitive with top-tier models like GPT-4 (0.9726). QIB and ZINK provide valuable resources enabling standardized evaluation and development of flexible, practical solutions for quasi-identifier anonymization in text.
Install the package using uv or pip.
CPU Support (Recommended for most users):
uv add "zink[cpu]"
# or
pip install "zink[cpu]"
GPU Support:
pip install "zink[gpu]"
First import: With a CPU or GPU backend installed, importing Zink downloads approximately 2 GB of model files from Hugging Face and loads the model. This can take several minutes, depending on your connection and hardware. Allow extra disk space for dependencies and the cache. Later imports reuse the cache; download and initialize the model before working offline. Installing bare
zinkemits a warning at import; inference requires the CPU or GPU extra.
Get started with ZINK in just a few lines of code. The redact function replaces identified entities with [LABEL]_REDACTED.
import zink as zn
text = "John works as a doctor and plays football after work and drives a toyota."
labels = ("person", "profession", "sport", "car")
result = zn.redact(text, labels)
print(result.anonymized_text)
Output:
person_REDACTED works as a profession_REDACTED and plays sport_REDACTED after work and drives a car_REDACTED.
Instead of simple redaction, you can replace entities with realistic synthetic data using the replace function. ZINK uses the Faker library to generate context-aware replacements.
import zink as zn
text = "John Doe dialled his mother at 992-234-3456 and then went out for a walk."
labels = ("person", "phone number", "relationship")
result = zn.replace(text, labels)
print(result.anonymized_text)
Possible Output:
Warren Buffet dialled his Uncle at 2347789287 and then went out for a walk.
You can provide your own dictionary of replacements for specific labels. This is useful for consistent mapping or using domain-specific datasets.
import zink as zn
text = "Melissa works at Google and drives a Tesla."
labels = ("person", "company", "car")
custom_replacements = {
"person": "Alice",
"company": "OpenAI",
"car": ("Honda", "Toyota")
}
result = zn.replace_with_my_data(text, labels, user_replacements=custom_replacements)
print(result.anonymized_text)
Possible Output:
Alice works at OpenAI and drives a Honda.
Protect sensitive data in your RAG pipelines or API calls using the @zink.shield decorator. It automatically anonymizes inputs before they reach the function and re-identifies the output, creating a secure "shield" around your logic.
import zink as zn
# This mock function simulates calling an external API (like OpenAI or Gemini)
@zn.shield(target_arg='prompt', labels=('person', 'company'))
def call_sensitive_api(prompt: str):
# The prompt received here is already anonymized.
# e.g., "Report for person_REDACTED from company_REDACTED."
return f"Analysis for {prompt} is complete."
# The original, sensitive text
sensitive_data = "Report for John Doe from Acme Inc."
# Call the function normally. The decorator handles anonymization and re-identification.
final_result = call_sensitive_api(prompt=sensitive_data)
print(final_result)
Output:
Analysis for John Doe from Acme Inc. is complete.
To ensure consistent redaction across multiple sessions or calls, you can use the numbered_entities=True flag. This will automatically use a persistent mapping file stored in your home directory (~/.zink/mapping.json).
import zink as zn
# First call: Generates IDs and saves them to default mapping file
text1 = "My name is Alice."
result1 = zn.redact(text1, labels=["person"], numbered_entities=True)
print(result1.anonymized_text)
# Output: person_1234_REDACTED ...
# Second call: Reuses the SAME ID for 'Alice'
text2 = "Alice is here again."
result2 = zn.redact(text2, labels=["person"], numbered_entities=True)
print(result2.anonymized_text)
# Output: person_1234_REDACTED ...
# Check where the mapping file is stored
print(zn.where_mapping_file())
# Clear the mapping file to start fresh
zn.refresh_mapping_file()
You can run zink easily using Docker. The image comes with the model pre-installed and exposes a REST API.
Run this command in the root of the repository:
docker build -t zink .
Start the API server on port 8000:
docker run --rm -p 8000:8000 zink
Tip: If port 8000 is already in use (e.g., you see a "Bind for 0.0.0.0:8000 failed" error), map it to a different port like 8001:
docker run --rm -p 8001:8000 zink
Once the container is running, you can send requests to the /redact endpoint.
Example Request:
curl -X POST "http://localhost:8000/redact" \
-H "Content-Type: application/json" \
-d '{"text": "John Doe lives in New York.", "labels": ["person", "location"]}'
(Note: If you used port 8001, replace localhost:8000 with localhost:8001)
Example Response:
{
"original_text": "John Doe lives in New York.",
"anonymized_text": "<PERSON> lives in <LOCATION>.",
"replacements": [
{
"label": "person",
"original": "John Doe",
"pseudonym": "<PERSON>",
"start": 0,
"end": 8,
"score": 0.99
},
{
"label": "location",
"original": "New York",
"pseudonym": "<LOCATION>",
"start": 18,
"end": 26,
"score": 0.99
}
]
}
To verify the installation, you can run the test suite inside the container:
docker run --rm zink pytest zink/tests
GLiNER is a Named Entity Recognition (NER) model capable of identifying any entity type using a bidirectional transformer encoder (BERT-like). It provides a practical alternative to traditional NER models, which are limited to predefined entities, and Large Language Models (LLMs) that, despite their flexibility, are costly and large for resource-constrained scenarios.
NuNerZero is a compact, zero-shot Named Entity Recognition model that leverages the robust GLiNER architecture for efficient token classification. It requires lower-cased labels and processes inputs as a concatenation of entity types and text, enabling it to detect arbitrarily long entities.
Zink leverages both the Faker library and the extensive training data from the GLiNER model to generate realistic, synthetic replacements for sensitive information.
A reproducible QIB comparison evaluates Zink and the GLiNER model used in Presidio's official integration example with the same semantic label prompts. It reports micro-averaged F4, precision and recall, CPU latency, throughput and process memory. Scores use exact target boundaries: unmatched predictions count as false positives and missed targets as false negatives. The methodology includes setup and reproduction commands.
Here is a comparison of ZINK against other models on Quasi Identifier Benchmark ([QIB])(https://huggingface.co/datasets/deepanwa/QIB)
These are the project's reported QIB results, not a head-to-head comparison with the redaction packages above. The new reproducible QIB comparison uses a separate exact-span scoring protocol and does not reproduce these historical numbers; verify their original conditions before using them as evidence in a submission.
| Model | Overall Recall | Overall Precision | Overall F4_SCORE | True Positives (TP) | False Negatives (FN) | Total Redaction Markers |
|---|---|---|---|---|---|---|
| gpt_41_nano | 0.8971 | 0.962 | 0.9007 | 1570 | 180 | 1632 |
| gpt_41 | 0.9726 | 0.9737 | 0.9726 | 1702 | 48 | 1748 |
| zink_single | 0.9 | 0.8858 | 0.8992 | 1575 | 175 | 1778 |
| zink_topic | 0.9126 | 0.6502 | 0.8914 | 1597 | 153 | 2456 |
| zink_human (run 1) | 0.9371 | 0.6597 | 0.9145 | 1640 | 110 | 2486 |
| zink_human (run 2) | 0.9446 | 0.6544 | 0.9206 | 1653 | 97 | 2526 |
| tars_topic | 0.5983 | 0.762 | 0.6059 | 1047 | 703 | 1374 |
| bert | 0.628 | 0.4255 | 0.6109 | 1099 | 651 | 2583 |
To install the development version from a clone of this repository (Python 3.11+):
python -m pip install -e '.[cpu]'
python -m pip install pytest sphinx sphinx-rtd-theme
See CONTRIBUTING.md for a virtual environment setup and documentation checks.
To run the tests, navigate to the project directory and execute:
pytest
Contributions are welcome! Please feel free to submit pull requests or open issues to suggest improvements or report bugs.
git checkout -b feature/your-featuregit commit -m 'Add your feature'git push origin feature/your-featureIf you are using this package for your work/research, use the below citation:
Wadhwa, D. (2025). ZINK: Zero-shot anonymization in unstructured text. (v0.2.1). Zenodo. https://doi.org/10.5281/zenodo.15035072
This project is licensed under the Apache 2.0 License.
92 followers · starred Aug 2025
189 followers · starred Nov 2025
32 followers · starred Jul 2025
59 followers · starred Dec 2025