mrkeyoor.com_
Sun 20 Sept 17:53 UTC
PyPIUtilsupdated 20 Sept 2026

nh3 review

nh3 0.3.7 turns untrusted HTML fragments into policy-limited HTML through Rust ammonia bindings. It parses markup, drops forbidden tags and attributes, checks link schemes, optionally removes entire dangerous subtrees, and returns serialized text. A reusable `Cleaner` avoids rebuilding the policy for each call. Version 0.3.7 adds ID prefixing, rejects conflicting broad and value-specific attribute rules, and moves from ammonia 4.1.2 to 4.1.4. Our measurements cover 0.3.6, the immediately preceding native wheel, rather than this new release.

Verdict

Our nh3 0.3.6 install took 0.3 seconds, occupied 3 MB, imported in 0.01 seconds, and had no audit findings; current 0.3.7 was published afterward. Choose nh3 for fragment sanitation with a tested allowlist, but choose another parser for DOM work or linkification.

We installed it

Lab card: what happened when we installed nh3Screenshot of nh3 documentation
Install✓ · 0.3s1 package on disk · 3 MB
Importimport nh3 in 0.01s · compiled extensions · py.typed · requires Python >=3.8
Known vulns0(pip-audit)

Answers from our run

Does nh3 install cleanly?

Yes. In a fresh container with an empty cache, pip install nh3 finished in 0.3s, leaving 1 package and 3 MB on disk. pip-audit reported no known vulnerabilities.

What does nh3 need to run?

Python >=3.8, and a platform wheel with compiled extensions. In our run import nh3 succeeded in 0.01s, and the package ships py.typed for type checkers.

nh3 or bleach: which should you use?

bleach: Keep it during a compatibility migration that still depends on Bleach signatures or linkify, despite deprecation. Our nh3 0.3.6 install took 0.3 seconds, occupied 3 MB, imported in 0.01 seconds, and had no audit findings; current 0.3.7 was published afterward.

When should you not use nh3?

Plain URLs must become anchor tags; nh3 offers sanitation and no linkify API.

API stability4/5Although nh3 remains below 1.0, its main `clean`, `Cleaner`, `escape`, and `is_html` entry points have remained recognizable while policy switches accumulate. Version 0.3.7 adds `id_prefix` without changing ordinary calls and now raises `ValueError` for one contradictory attribute configuration. That upgrade can surface a latent mistake, an observable but defensible compatibility break.
Docs4/5Read the Docs describes each Cleaner option with examples for removing subtree content, filtering classes and style properties, limiting or forcing attribute values, handling relative URLs, setting link rel values, callback filtering, and prefixing IDs. The repository README contains little beyond install, a benchmark, and a link to those pages. It lacks a complete Bleach migration map and a threat-model checklist for policy authors.
Maintenance4/5PyPI published 0.3.7 on August 23, 2026, GitHub shows a push on August 25, 2026, and the unarchived repository has 391 stars plus 5 open issues and pull requests. The release upgraded ammonia, added ID namespacing, removed a deprecated build feature, and tightened policy validation. The remaining concern is a small maintenance surface around security-sensitive native bindings.
Ecosystem4/5The supplied download estimate is 11,048,289 per week, while GitHub reports 391 stars. nh3 deliberately exposes a narrow cleaning boundary rather than plugins or DOM operations, and downstream systems inherit core sanitation behavior from Rust ammonia. That makes it attractive inside rendering and packaging stacks, but applications must own policy tests because integrations cannot infer the tags and schemes their product needs.

Use it if

  • Rendered user Markdown or rich-text editor output must be cleaned before it reaches a browser.
  • The application needs a precise allowlist for tags, attributes, schemes, classes, or style properties.
  • One sanitizing policy handles many fragments and can live in a shared Cleaner instance.
  • A Bleach migration needs HTML cleaning but does not depend on Bleach's separate linkify operation.
Skip it if

Setup reality

We installed nh3 0.3.6 in our lab before 0.3.7 reached PyPI. That Python 3.12 Bookworm install took 0.3 seconds, produced 1 package using 3 MB, and imported in 0.01 seconds. The measured wheel had 0 direct dependencies, required Python 3.8 or newer, included compiled .so files and py.typed, and carried the MIT license. pip-audit reported 0 known vulnerabilities. The 0.3.7 artifact may differ, so those numbers belong only to 0.3.6.

No service account or configuration file is needed. Policy design is the setup. Default cleaning keeps a fairly broad rich-text vocabulary and adds rel="noopener noreferrer" to links. A compact comments field should normally pass smaller tag and attribute sets. Forbidden tags are unwrapped by default, leaving their text. Put script and style in clean_content_tags when their contents must disappear with the element.

Create one Cleaner when rules repeat; passing option sets to clean reconstructs policy for every fragment. Value filters must not conflict with an attribute rule that already accepts every value. Version 0.3.7 turns that ineffective tag_attribute_values configuration into a ValueError. The same principle applies to class filtering: do not broadly allow class and then expect allowed_classes to be the boundary.

Review links separately from elements. Limit url_schemes, choose whether relative URLs survive or receive a base URL, and keep a safe link_rel unless another trusted component supplies it. The new id_prefix converts retained IDs such as billing into a host-specific namespace, avoiding collisions with page controls. For Markdown, render first and sanitize the resulting HTML; cleaning Markdown source cannot inspect raw markup emitted or preserved by the renderer.

Patterns

Remove disallowed attributes with the stock policy clean-default

import nh3
raw = '<p>Hello <img src="x" onerror="alert(1)"></p>'
safe = nh3.clean(raw)
print(safe)

The event handler is removed, while allowed rich-text markup remains; tighten defaults for a small comment field.

Define a compact formatting allowlist limit-markup

safe = nh3.clean(raw_html,
 tags={"p", "br", "strong", "em", "code", "a"},
 attributes={"a": {"href", "title"}},
 url_schemes={"http", "https", "mailto"})

Disallowed wrappers normally vanish while their text remains, and each accepted URL still passes the scheme rule.

Discard script and style contents drop-subtree

result = nh3.clean('<script>alert(1)</script><p>safe</p>', clean_content_tags={"script", "style"})
assert result == '<p>safe</p>'

Without `clean_content_tags`, removing a forbidden element can preserve its text content.

Keep one Cleaner for repeated fragments reuse-policy

COMMENT_CLEANER = nh3.Cleaner(tags={"p", "strong", "em", "a"}, attributes={"a": {"href"}}, url_schemes={"https"})
def clean_comment(raw: str) -> str:
    return COMMENT_CLEANER.clean(raw)

A Cleaner retains parsed settings, avoiding per-call policy construction when all comments share the same rules.

Accept only selected role values restrict-values

safe = nh3.clean('<div role="banner">notice</div>', tags={"div"}, attributes={}, tag_attribute_values={"div": {"role": {"alert", "status"}}})

Version 0.3.7 raises if `attributes` also broadly admits `role`, because that would bypass this value set.

Namespace IDs retained from user HTML prefix-ids

safe = nh3.clean('<h2 id="billing">Billing</h2>', tags={"h2"}, attributes={"h2": {"id"}}, id_prefix="user-content-")

`id_prefix` is new in 0.3.7; the element and ID attribute still must pass their allowlists.

Resolve relative URLs against a trusted host rewrite-relative-link

safe = nh3.clean('<a href="/help">Help</a>', tags={"a"}, attributes={"a": {"href"}}, url_relative=("rewrite_with_base", "https://docs.example.com"))

The default can retain relative paths, which may resolve against the wrong site when HTML moves into email or another origin.

Clean HTML after Markdown rendering sanitize-markdown

import markdown, nh3
raw_html = markdown.markdown(user_markdown)
safe_html = nh3.clean(raw_html, tags={"p", "strong", "em", "a", "code", "pre"}, attributes={"a": {"href"}})

Rendering comes first because a Markdown engine can emit or preserve raw HTML that the sanitizer must inspect.

Alternatives

PackageRegistryPick it when
bleachPyPIKeep it during a compatibility migration that still depends on Bleach signatures or linkify, despite deprecation.
lxml-html-cleanPyPIUse it when cleaning is part of an existing lxml document-tree workflow.
html-sanitizerPyPIUse it when its opinionated editor-output normalization matches the stored content.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.