nh3
Rust-powered HTML sanitizer for Python that strips XSS while preserving safe markup, roughly 20x faster than bleach.
Repository Health
Technical Analysis
nh3 is a Python binding to Ammonia, a whitelist-based HTML sanitizer written in Rust. It parses an HTML fragment, walks the resulting tree, and rewrites it against an explicit allowlist of tags, attributes, URL schemes, and CSS classes, dropping or escaping anything outside that allowlist. Because the sanitization pass runs as compiled Rust code behind a thin PyO3 binding rather than pure Python, nh3 sanitizes typical HTML fragments an order of magnitude faster than pure-Python sanitizers like bleach, without giving up configurability.
The library exposes a small functional surface: a clean() function for one-off sanitization calls, a reusable Cleaner class for repeated calls with the same configuration, clean_text()/escape() for HTML-escaping plain strings, and is_html() to detect whether a string contains HTML syntax at all. Configuration covers allowed tags and per-tag attributes, tags whose content should be dropped entirely, per-attribute value allowlists, CSS class allowlists, style property filtering, relative-URL rewriting (including a custom callback), rel attribute injection on links, and id attribute namespacing to avoid collisions when sanitized fragments are embedded in a larger page.
nh3 is commonly used anywhere untrusted or semi-trusted HTML needs to be rendered safely — comment systems, markdown-to-HTML pipelines, CMS content fields, and chat/forum message bodies — as a drop-in, faster alternative to bleach for exactly this job.
What You Get
clean()function - one-call HTML sanitization with a large surface of keyword options for tags, attributes, URL schemes, classes, and style propertiesCleanerclass - a reusable sanitizer object for services that repeatedly sanitize with the same configuration, avoiding rebuilding the allowlist on every callclean_text()/escape()- strict HTML-escaping of plain strings, stricter than Python’s stdlibhtml.escape()is_html()- quick detection of whether a string contains HTML syntax- Relative URL handling -
url_relativesupports pass-through, deny, rewrite-against-a-base-URL, rewrite-into-a-root-path, or a custom Python callback - Attribute-level control -
attribute_filtercallback, per-tagtag_attribute_valuesvalue allowlists, andset_tag_attribute_valuesfor unconditionally injected attributes
Common Use Cases
- Sanitizing user comments - forums, blogs, and comment widgets clean submitted HTML before storing or rendering it, stripping scripts and event handlers while keeping basic formatting tags
- Markdown-to-HTML pipelines - after rendering user-authored Markdown to HTML, nh3 sanitizes the output as a second line of defense against injected raw HTML
- Replacing bleach for performance - services that hit sanitization as a hot path swap in nh3 for a faster drop-in with a similar configuration surface
- CMS and chat message bodies - platforms accepting rich-text or HTML-formatted messages from users sanitize them before display to prevent stored XSS
Under The Hood
Architecture
nh3’s entire implementation lives in a single Rust source file (src/lib.rs) exposed to Python through a PyO3 extension module. Python-facing calls (clean(), Cleaner.clean(), clean_text(), escape(), is_html()) funnel into a shared Config struct that captures every keyword option, which a private build_ammonia_from_config() translates into an ammonia::Builder. The reusable Cleaner class wraps this builder in a self-referencing struct (via the ouroboros crate) so the builder can borrow from its own config across repeated clean() calls without re-parsing options each time, while the plain clean() function builds a throwaway Cleaner per call. Validation that spans multiple options (e.g. rejecting a tag listed in both tags and clean_content_tags, or an attribute whitelisted in both attributes and tag_attribute_values) happens eagerly at construction time in py_new(), so misconfiguration raises a clear ValueError up front rather than failing silently mid-sanitization. Sanitization itself is delegated entirely to the Rust ammonia crate; nh3’s own code is a configuration and marshalling layer between Python’s calling convention and Ammonia’s builder API.
Tech Stack
The implementation is Rust (edition 2024) bound to Python via pyo3 0.29 with the abi3-py38 feature, producing a single wheel binary compatible across CPython 3.8+ (and, per the #[pymodule(gil_used = false)] attribute, buildable for free-threaded Python). HTML sanitization is delegated to the ammonia crate (^4.1.4), and the self-referencing Cleaner struct uses ouroboros (^0.18) to hold a builder that borrows from its own config field. The Python package is built and published with maturin, and the pyproject.toml declares maturin as the sole build backend with dynamic version/urls sourced from Cargo.toml.
Code Quality
Tests live in tests/test_nh3.py (direct unit tests against every keyword option: tag/attribute allowlisting, attribute_filter, url_relative modes, class/style filtering, error cases like the rel-attribute conflict) plus tests/test_doctests.py, and pytest.ini_options in pyproject.toml also runs the .. code-block:: pycon examples embedded directly in the Rust doc comments and in docs/index.rst as doctests (--doctest-glob=*.rst), so the documented examples are continuously verified against the actual binding. Error handling is explicit and typed: invalid configuration raises PyValueError/PyTypeError with specific messages rather than panicking, and callback failures (attribute_filter, url_relative custom callables) are caught and routed through sys.unraisablehook instead of crashing the sanitization call. CI (.github/workflows/CI.yml) builds and tests wheels across macOS, Windows (x64/x86), and presumably Linux/musl targets via maturin-action, including free-threaded Python builds, before installing the built wheel and running pytest — giving reasonable confidence the binding behaves consistently across platforms.
API Design
The public surface is deliberately small and mirrors bleach’s shape (clean(), allowlist-oriented keyword arguments, ALLOWED_TAGS/ALLOWED_ATTRIBUTES module constants) to ease migration, while adding an escape hatch for repeated use via the Cleaner class. Every keyword argument is documented with a runnable doctest embedded in the Rust source, so the generated Sphinx docs and the type stub (nh3.pyi) stay directly traceable to working examples. A few option pairs are deliberately restricted rather than left to silently misbehave — the library raises instead of accepting configurations that would produce misleading behavior (e.g. an attribute whitelisted in both attributes and tag_attribute_values, where Ammonia would otherwise silently ignore the value restriction). The type stub gives static type checkers full coverage of the keyword surface despite the implementation being compiled Rust.
Used by 3 apps in this directory
PostHog
Analytics · Monitoring · Developer Tools
The all-in-one open source product platform combining analytics, session replay, feature flags, error tracking, AI observability, and a built-in data warehouse in a single self-hostable stack.
Tracecat
Security · Automation · AI Agents
Open-source agentic security automation platform that runs AI agents and durable workflows at scale with sandboxed execution.
Weblate
Developer Tools
Continuous localization platform that commits translations directly into your version control system with full translator attribution.