GDPR Compliance and Data Protection
On this page
Updated November 3, 2026. The list of items detected by the
piicategory now aligns with Mistral’s definition (health data falls under different rules, and thehealthcategory targets requests for medical advice). The compliant logger previously miscalculated its retention end date, causing purges to delete everything; it also used a hardcoded default salting key for pseudonymization and the deprecateddatetime.utcnow(). Pseudonymization now uses HMAC-SHA-256 with a mandatory key. Sources: Moderation & Guardrailing · GDPR, Recital 26 and Articles 4 and 22 · Python hmac.
AI and Personal Data: A Strict Framework
There is a paradox at the heart of this lesson, and seeing it clearly avoids many design errors: your moderation system is itself a personal data processing operation. To protect your users, you analyze their text, generate scores, and retain logs—all operations that the GDPR regulates exactly like any other processing activity.
Compliance, therefore, does not mean adding a protection layer on top of moderation. It means designing the moderation system itself to process the minimum amount of data, for the shortest time possible, while maintaining a record of its decisions. This is the thread running through the following sections.
The GDPR (General Data Protection Regulation) applies fully to AI applications processing data of European residents. Content moderation itself generates data that falls within the scope of the GDPR. You must understand your obligations to avoid penalties reaching up to 4% of global annual turnover.
The PII Category in Mistral Moderation
Mistral’s classifier pii category detects requests for or disclosures of personal information. One architectural point must be made immediately: this classifier is a safety net, not a guarantee. It catches varied phrasings that a regex would miss (“my number is…”), but like any model, it has false negatives. Conversely, a regex reliably catches structured formats—IBANs, social security numbers, credit cards—but nothing else. Best practice combines both: regex for known formats, classifier for the rest. Detected types include:
- Full names
- Addresses
- Phone numbers
- Social security numbers
- Financial account details
Mistral’s definition focuses on the request, sharing, or attempt to obtain this identifying information.
from mistralai.client import Mistral
import os
import re
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
def detect_pii(text: str) -> dict:
"""Detects personal data in text."""
# Layer 1: Mistral Classifier
response = client.classifiers.moderate(
model="mistral-moderation-2603",
inputs=[text]
)
pii_score = (response.results[0].category_scores or {}).get("pii", 0)
# Layer 2: Regex for common patterns
pii_patterns = {
"email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
"telephone_fr": r"(?:0|\+33)[1-9](?:\d{2}){4}",
"secu_sociale": r"\d{13,15}",
"carte_bancaire": r"\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}",
"iban": r"FR\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{3}"
}
detected_pii = {}
for name, pattern in pii_patterns.items():
matches = re.findall(pattern, text)
if matches:
detected_pii[name] = len(matches)
return {
"score_classifieur": round(pii_score, 3),
"pii_regex": detected_pii,
"contient_pii": pii_score > 0.2 or len(detected_pii) > 0
}
Anonymization Before Processing
Upstream anonymization is the most cost-effective measure in this entire lesson because it changes the nature of the problem: data that never leaves your infrastructure raises no transfer issues. Replacing names, emails, and numbers with tokens ([PERSON_1], [EMAIL_1]) before calling the model almost always preserves meaning for analysis—and if you keep the mapping table locally, you can reinject the real values into the response. As long as this table exists, this is legally pseudonymization, not anonymization: the data remains personal, but the data sent to the model no longer allows identification of the person on its own.
def anonymize_text(text: str) -> tuple:
"""Anonymizes text and keeps a restoration mapping."""
mapping = {}
anonymized_text = text
# Anonymize emails
emails = re.findall(
r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
text
)
for i, email in enumerate(set(emails)):
placeholder = f"[EMAIL_{i+1}]"
mapping[placeholder] = email
anonymized_text = anonymized_text.replace(email, placeholder)
# Anonymize phones
phones = re.findall(
r"(?:0|\+33)[1-9](?:[\s.-]?\d{2}){4}",
text
)
for i, tel in enumerate(set(phones)):
placeholder = f"[TEL_{i+1}]"
mapping[placeholder] = tel
anonymized_text = anonymized_text.replace(tel, placeholder)
# Anonymize names (if detected by upstream NER)
# Add your own NER logic here
return anonymized_text, mapping
def restore_text(anonymized_text: str, mapping: dict) -> str:
"""Restores anonymized data after processing."""
restored_text = anonymized_text
for placeholder, value in mapping.items():
restored_text = restored_text.replace(placeholder, value)
return restored_text
# Usage in pipeline
original_text = "Contactez [email protected] au 06 12 34 56 78"
anonymized_text, mapping = anonymize_text(original_text)
# → "Contactez [EMAIL_1] au [TEL_1]"
# Send anonymized text to model
response = client.chat.complete(
model="mistral-large-latest",
messages=[{"role": "user", "content": anonymized_text}]
)
GDPR-Compliant Moderation Logs
Logs are the classic blind spot: we think “personal data” regarding user messages, but rarely regarding the technical traces that copy them. Yet a moderation log retaining the analyzed text is a personal database, with all the ensuing obligations. The rule of thumb: log text only for blocked content—you will need it in case of dispute—and log only scores for accepted content.
import hashlib
import hmac
from datetime import datetime, timedelta, timezone
class LogModerationRGPD:
"""GDPR-compliant moderation logger."""
def __init__(self, retention_jours: int = 90):
self.retention_jours = retention_jours
# Mandatory secret key, kept out of code (vault or environment variable)
self.key = os.environ["PII_HMAC_KEY"].encode()
def _pseudonymiser(self, user_id: str) -> str:
"""Pseudonymizes a user identifier (HMAC-SHA-256)."""
return hmac.new(self.key, user_id.encode(), hashlib.sha256).hexdigest()[:16]
def log_moderation(self, user_id: str, scores: dict, decision: str,
text: str | None = None) -> dict:
"""Records a moderation decision."""
maintenant = datetime.now(timezone.utc)
entry = {
"timestamp": maintenant.isoformat(),
"user_hash": self._pseudonymiser(user_id),
"scores": scores,
"decision": decision,
# Do NOT store full text by default
"texte_longueur": len(text) if text else 0,
"conserver_jusqu_au": (maintenant + timedelta(days=self.retention_jours)).isoformat(),
}
# Store text excerpt only if blocked (for audit)
if decision == "blocked" and text:
entry["texte_tronque"] = text[:200]
return entry
def purger_anciens_logs(self, logs: list[dict]) -> list[dict]:
"""Keeps logs whose retention end date has not passed."""
maintenant = datetime.now(timezone.utc)
return [
log for log in logs
if datetime.fromisoformat(log["conserver_jusqu_au"]) > maintenant
]
Two precautions in this code bear mentioning. The pseudonymization key has no default value: a key written in the code, known to everyone, would allow recalculating the hash of any identifier. Furthermore, pseudonymized data remains personal data under the GDPR (Recital 26): this log remains subject to retention periods and data subject rights.
Audit Trail for Compliance
The audit trail answers a specific question a user or authority may ask you: “Why was this content blocked?” Without a trace, your only honest answer is “I don’t know anymore”—untenable given that the GDPR requires, for fully automated decisions under Article 22, informing the person about the underlying logic (Articles 13 to 15), and the AI Act adds its own transparency obligations. The audit trail retains the decision, the scores that motivated it, and the version of the policy applied—without necessarily retaining the text itself.
class AuditTrail:
"""Audit trail for moderation decisions."""
def __init__(self, storage_path: str):
self.storage_path = storage_path
def record_decision(self, data: dict) -> str:
"""Records a decision with a unique identifier."""
import uuid
audit_id = str(uuid.uuid4())
entry = {
"audit_id": audit_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"type_decision": data.get("decision"),
"modele_moderation": "mistral-moderation-2603",
"scores": data.get("scores"),
"seuils_appliques": data.get("seuils"),
"politique_version": data.get("politique_version"),
"action_prise": data.get("action"),
"base_legale": "interet_legitime",
"responsable": "systeme_automatise"
}
# Write to audit file
import json
with open(self.storage_path, "a") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
return audit_id
GDPR Checklist for Your AI Application
Here are the points to verify to ensure your moderation system is compliant:
Legal Basis
- Documented legitimate interest for content moderation
- User notification about automated processing
- Ability to contest an automated decision
Data Minimization
- Store only strictly necessary data
- Anonymize or pseudonymize user identifiers
- Truncate text in moderation logs
- Do not retain accepted texts (scores only)
Retention Period
- Define a retention period for each data type
- Moderation logs: 90 days maximum (unless legal obligation requires otherwise)
- Audit trail: per sectoral regulation
- Scheduled automatic purge
User Rights
- Right of access to moderation decisions concerning them
- Right to rectification if the decision is contested
- Right to explanation of the automated decision
Key Takeaways
- The GDPR applies fully to your AI moderation systems
- Use Mistral’s
piicategory AND regex to detect personal data - Anonymize texts before sending them to the model when possible
- Pseudonymize user identifiers in logs
- Implement an audit trail for decision traceability
- Define a retention period and automatically purge logs