Zero-config PII redaction for Python logging.
- Zero-config -- one call to
hushlog.patch()and you're done - Non-invasive -- wraps existing formatters, no logger rewrites needed
- Performant -- pre-compiled regex with heuristic early-exit checks
- Type-safe -- fully typed with PEP 561
py.typedmarker - Python 3.10+ -- supports Python 3.10 through 3.13
pip install hushlog
Or with uv:
uv add hushlog
import logging
import hushlog
# Configure logging FIRST, then patch
logging.basicConfig(level=logging.INFO)
hushlog.patch()
logger = logging.getLogger(__name__)
logger.info("User email: john@example.com")
# Output: User email: [EMAIL REDACTED]
logger.info("Card: 4111-1111-1111-1111")
# Output: Card: [CREDIT_CARD REDACTED]
logger.info("SSN: 123-45-6789")
# Output: SSN: [SSN REDACTED]
HushLog wraps your existing logging formatters with a RedactingFormatter that scans the final formatted string for PII patterns. It never replaces loggers or handlers -- your existing logger.info() calls remain unchanged. All regex patterns are pre-compiled at import time with lightweight heuristic pre-checks to minimize overhead on the hot logging path.
| Pattern | Example | Output | Notes |
|---|---|---|---|
john@example.com |
[EMAIL REDACTED] |
RFC 5322 subset, @ heuristic pre-check |
|
| Credit Card | 4111-1111-1111-1111 |
[CREDIT_CARD REDACTED] |
Luhn validated, supports spaces/dashes |
| SSN | 123-45-6789 |
[SSN REDACTED] |
Dashed format only, invalid ranges excluded |
| Phone | (555) 123-4567 |
[PHONE REDACTED] |
US NANP, multiple formats |
| JWT | eyJhbGci... |
[JWT REDACTED] |
3-5 segment base64url tokens |
| AWS Access Key | AKIAIOSFODNN7EXAMPLE |
[AWS_ACCESS_KEY REDACTED] |
AKIA/ASIA prefixed |
| AWS Secret Key | aws_secret_access_key=... |
[AWS_SECRET_KEY REDACTED] |
Context-dependent (requires label) |
| Stripe Key | sk_live_abc123... |
[STRIPE_KEY REDACTED] |
sk/pk/rk live/test keys |
| GitHub Token | ghp_xxxx... |
[GITHUB_TOKEN REDACTED] |
Classic + fine-grained (github_pat_) |
| GCP API Key | AIzaSyA... |
[GCP_KEY REDACTED] |
AIza-prefixed keys |
| Generic Secret | password=MyS3cret |
[SECRET REDACTED] |
Label-based (password, secret, api_key, etc.) |
| IPv4 | 192.168.1.1 |
[IPV4 REDACTED] |
Octet-validated, rejects version strings |
| IPv6 | 2001:db8::8a2e:370:7334 |
[IPV6 REDACTED] |
Full, compressed, and mixed forms |
Disable specific built-in patterns or add custom ones:
from hushlog import Config
hushlog.patch(Config(
disable_patterns=frozenset({"phone"}),
custom_patterns={"internal_id": r"ID-[A-Z]{3}-[0-9]{6}"},
))
Show partial values instead of full redaction:
hushlog.patch(Config(mask_style="partial"))
# john@example.com → j***@e***.com
# 4111111111111111 → ****-****-****-1111
# 078-05-1120 → ***-**-1120
# (555) 234-5678 → (***) ***-5678
Use a custom mask character:
hushlog.patch(Config(mask_style="partial", mask_character="#"))
# john@example.com → j###@e###.com
Note: Partial masking reveals partial information (first/last characters). In small organizations, this may be identifying. Use
mask_style="full"(default) for maximum privacy.
HushLog supports JSON log output with automatic PII redaction in all string values, including nested structures.
Use RedactingJsonFormatter as a drop-in JSON formatter for any handler:
import logging
from hushlog import Config, RedactingJsonFormatter
from hushlog._registry import PatternRegistry # internal API
registry = PatternRegistry.from_config(Config())
formatter = RedactingJsonFormatter(registry)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
logger = logging.getLogger(__name__)
logger.info("Contact user@example.com", extra={"ssn": "078-05-1120"})
# Output: {"message": "Contact [EMAIL REDACTED]", "ssn": "[SSN REDACTED]", ...}
Works with or without python-json-logger installed. Install the optional dependency for enhanced JSON serialization:
pip install hushlog[json]
For manual redaction of dict/list/string structures:
import hushlog
data = {"user": {"email": "alice@corp.io", "name": "Alice", "age": 30}}
clean = hushlog.redact_dict(data)
# {"user": {"email": "[EMAIL REDACTED]", "name": "Alice", "age": 30}}
Note:
redact_dict()creates a newPatternRegistryon every call. For repeated use, create a registry once viaPatternRegistry.from_config()and callregistry.redact_dict()directly.
Use structlog_processor() as a processor in your structlog pipeline:
import structlog
from hushlog import structlog_processor
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog_processor(),
structlog.dev.ConsoleRenderer(),
],
)
logger = structlog.get_logger()
logger.info("login", email="alice@corp.com")
# Output: email=[EMAIL REDACTED]
Install the optional dependency: pip install hushlog[structlog]
Wrap any loguru sink with PII redaction:
from loguru import logger
from hushlog import loguru_sink
logger.remove() # Remove default sink
logger.add(loguru_sink(print), format="{message}")
logger.info("User alice@corp.com logged in")
# Output: User [EMAIL REDACTED] logged in
Install the optional dependency: pip install hushlog[loguru]
Call unpatch() to remove HushLog's formatter wrappers and restore the original formatters. This is useful for testing or runtime toggling:
hushlog.unpatch()
Calling unpatch() without a prior patch() is safe (no-op). Calling patch() multiple times is also safe (idempotent).
- Only handlers present on the root logger at
patch()time are wrapped. Handlers added later will not be redacted. - Named loggers with
propagate=Falseand their own handlers bypass root-level redaction. - For structlog/loguru, use the dedicated integrations (
structlog_processor,loguru_sink) instead ofpatch(). - Phone detection is US NANP only.
Production hardening, docs site, and more. See the roadmap for details.
Contributions are welcome! See CONTRIBUTING.md for guidelines.
MIT -- see LICENSE for details.