From nh3¶
nh3 is the Python binding (via PyO3/maturin) for the Rust ammonia HTML sanitizer, and a common landing spot for projects leaving the
unmaintained bleach. It is an allowlist sanitizer: you declare the tags, attributes, URL schemes, and CSS properties you
trust, and everything else is stripped or dropped. It ships as a single self-contained wheel with no Python runtime
dependencies, plus two helpers – nh3.clean_text to escape plain text and nh3.is_html for a quick content
heuristic.
nh3 stays narrowly a sanitizer. It has no linkifier, and, unlike bleach, no escape-instead-of-strip mode: a disallowed
tag is removed rather than rendered as visible text. turbohtml.clean covers the same allowlist surface behind a
frozen Policy, adds that escape mode and a companion linkifier, and lives inside a full
WHATWG-conformant parser, so the same tree can be parsed, queried, sanitized, and minified without leaving the library.
turbohtml vs nh3¶
Dimension |
turbohtml |
nh3 |
|---|---|---|
Scope |
Full WHATWG parser with a sanitize/linkify/minify/CSS-minify suite |
HTML sanitization only, plus |
Feature breadth |
Escape/strip/remove modes, |
Allowlist sanitize with |
Performance |
Native C, leads on the corpus below |
Native Rust (ammonia core), same tier |
Typing |
Fully annotated Python API with |
Typed via bundled stubs |
Dependencies |
Single C extension, no Python runtime deps |
Single Rust extension, no Python runtime deps |
Maintenance |
Active, part of a broader HTML toolkit |
Active binding over the mature ammonia crate |
Feature overlap¶
The allowlist surface ports one-to-one; only the call shape changes.
Tag allowlist:
tags=->Policy.tags.Per-tag attributes (with
"*"wildcards):attributes=->Policy.attributes.Drop a tag and its whole subtree:
clean_content_tags=->Policy.remove_with_content.Forced
reltokens on kept links:link_rel=->Policy.add_link_rel.URL scheme allowlist:
url_schemes=->Policy.url_schemes.Comment stripping:
strip_comments=->Policy.strip_comments.Per-attribute callable
(tag, attr, value) -> str | Nonethat drops or rewrites a value:attribute_filter=->Policy.attribute_filter.Values forced onto every kept instance of a tag:
set_tag_attribute_values=->Policy.set_attributes.Inline-style property allowlist:
filter_style_properties=->Policy.css_properties.Attribute-name prefix allowlist (the
data-*family):generic_attribute_prefixes=->Policy.attribute_prefixes.Restrict an attribute to literal values:
tag_attribute_values=->Policy.attribute_values(narrows an attribute already admitted byattributes; it cannot admit a new one).
What turbohtml adds¶
An escape mode.
OnDisallowedisESCAPEby default, so a disallowed tag becomes visible text (<x>renders as<x>) instead of vanishing; nh3 only strips.STRIP(drop tag, keep children) andREMOVE(drop the subtree) select nh3-style behavior from one enum.Ready-made presets:
Policy.strict(),Policy.basic(), andPolicy.relaxed().A
allow_relative_urlstoggle to accept or reject scheme-less URLs independently of the scheme allowlist.A reusable compiled
Sanitizer: build it once from a frozenPolicyand call it from any thread.A companion linkifier,
turbohtml.clean.linkify(), for auto-linking plain URLs; nh3 has none.HTML and CSS minifiers (
minify(),minify_css()) in the same module, over the same parse tree.
What nh3 has that turbohtml does not¶
nh3.is_html(text)– a heuristic bool for whether a string contains HTML. No turbohtml equivalent.nh3.clean_text(text)– escape a string for safe insertion as text. Workaround:sanitize(text, Policy.strict())escapes all markup to text.
Performance¶
sanitize |
turbohtml |
|
|---|---|---|
comment |
1.57 µs |
5.77 µs (3.7x) |
post 4 KiB |
42.5 µs |
142 µs (3.4x) |
turbohtml.clean stays in the same native tier as the Rust binding and leads it on the benchmark corpus above.
How to migrate¶
Swap the import and wrap the keyword arguments in a Policy. nh3’s flat keyword call becomes a
frozen config object plus sanitize().
turbohtml |
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(drops disallowed tags) |
|
# nh3
import nh3
nh3.clean(text, tags={"a"}, attributes={"a": {"href"}})
# turbohtml
from turbohtml.clean import sanitize, Policy, OnDisallowed
sanitize(text, Policy(tags=frozenset({"a"}), attributes={"a": frozenset({"href"})}))
# match nh3's default of dropping (not escaping) disallowed tags
sanitize(
text,
Policy(
tags=frozenset({"a"}),
attributes={"a": frozenset({"href"})},
on_disallowed_tag=OnDisallowed.STRIP,
),
)
Gotchas and pitfalls¶
Disallowed-tag default differs. nh3 strips a disallowed tag and keeps its children; turbohtml escapes it to visible text by default. Set
on_disallowed_tag=OnDisallowed.STRIPto reproduce nh3’s behavior exactly.Set-typed fields must be sets.
tags,url_schemes,remove_with_content, andcss_propertiesexpect aset/frozenset; passing another type raisesTypeError. nh3 accepts any iterable for the equivalent arguments.Attribute allowlisting is by exact name, a per-tag
"*"wildcard, or a name prefix inattribute_prefixes(nh3’sgeneric_attribute_prefixes, e.g."data-"for everydata-*).attribute_valuesrestricts a kept attribute to literal values; it narrows an attributeattributesalready admits and cannot admit a new one.sanitizetakes an HTML fragment and returns a fragment; it does not add<html>/<body>scaffolding, matching nh3’s fragment-in, fragment-out contract.Reuse the compiled
Sanitizerwhen sanitizing many inputs with one policy; each baresanitize()call recompiles the policy.