From nh3

nh3 latest releasenh3 supported Pythonsnh3 licensenh3 monthly downloadsnh3 total downloadsnh3 GitHub starsnh3 last commit

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 clean_text/is_html helpers

Feature breadth

Escape/strip/remove modes, Policy presets, linkifier, attribute-prefix/value allowlists, embedded-media host allowlist, HTML and CSS minifiers

Allowlist sanitize with attribute_filter, style-property filter, attribute-prefix and value allowlists

Performance

Native C, leads on the corpus below

Native Rust (ammonia core), same tier

Typing

Fully annotated Python API with .pyi stubs

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.

What turbohtml adds

  • An escape mode. OnDisallowed is ESCAPE by default, so a disallowed tag becomes visible text (<x> renders as &lt;x&gt;) instead of vanishing; nh3 only strips. STRIP (drop tag, keep children) and REMOVE (drop the subtree) select nh3-style behavior from one enum.

  • Ready-made presets: Policy.strict(), Policy.basic(), and Policy.relaxed().

  • A allow_relative_urls toggle to accept or reject scheme-less URLs independently of the scheme allowlist.

  • A reusable compiled Sanitizer: build it once from a frozen Policy and 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

nh3

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().

nh3

turbohtml

nh3.clean(text, ...)

turbohtml.clean.sanitize() with a Policy

tags=, attributes=

Policy.tags, Policy.attributes

clean_content_tags=

Policy.remove_with_content

link_rel=

Policy.add_link_rel

url_schemes=

Policy.url_schemes

strip_comments=

Policy.strip_comments

attribute_filter=

Policy.attribute_filter

set_tag_attribute_values=

Policy.set_attributes

filter_style_properties=

Policy.css_properties

generic_attribute_prefixes=

Policy.attribute_prefixes

tag_attribute_values=

Policy.attribute_values

(drops disallowed tags)

OnDisallowed (ESCAPE by default; STRIP / REMOVE for nh3-style dropping)

# 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.STRIP to reproduce nh3’s behavior exactly.

  • Set-typed fields must be sets. tags, url_schemes, remove_with_content, and css_properties expect a set/frozenset; passing another type raises TypeError. nh3 accepts any iterable for the equivalent arguments.

  • Attribute allowlisting is by exact name, a per-tag "*" wildcard, or a name prefix in attribute_prefixes (nh3’s generic_attribute_prefixes, e.g. "data-" for every data-*). attribute_values restricts a kept attribute to literal values; it narrows an attribute attributes already admits and cannot admit a new one.

  • sanitize takes 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 Sanitizer when sanitizing many inputs with one policy; each bare sanitize() call recompiles the policy.