Library / SDK
EmilStenstrom/justhtml avatar
EmilStenstrom/justhtml

JustHTML: a pure Python HTML5 parser that sanitizes before you get to query it

A pure Python HTML5 parser that just works. No C extensions to compile. No system dependencies to install. No complex API to learn.

1,160 stars47 forksPythonNOASSERTION

At a glance

What is it?
JustHTML is a dependency-free Python HTML5 parser with browser-style error recovery, sanitization on by default, CSS selectors, transforms and Markdown output. This looks at how the security default shapes the API, how the project argues correctness, and where the pure Python trade stops paying.
Who is it for?
JustHTML fits a Python service that handles untrusted or malformed HTML, wants one dependency instead of a parser plus a sanitizer, and needs to run where a compiler is not available, including PyPy and Pyodide.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One dependency instead of a parser plus a sanitizer

The problem the project names is a mess of HTML. Real web HTML is often malformed, user supplied, scraped from unknown pages, or headed for a browser where small parsing differences can become security bugs. JustHTML gives Python projects one small dependency for the common jobs: parse HTML like a browser including broken markup, sanitize untrusted HTML by default, query with CSS selectors, transform, serialize, extract text or convert to Markdown, and run anywhere Python runs with no C extension and no system package to install.

The positioning is against the existing options, and the documentation is blunt about each. `html.parser` is built in, but not HTML5-correct. BeautifulSoup is convenient, but depends heavily on the parser underneath. `lxml` and C or Rust backed parsers are fast, but usually leave sanitization as a separate concern. And `html5lib` and Bleach shaped the Python ecosystem, but both are no longer the obvious foundation for new projects.

That last point is the interesting one. Bleach was the default answer for sanitizing HTML in Python for years, and JustHTML's own documentation carries a Bleach migration guide, which tells you the project is aimed squarely at codebases that were written against it.

The evidence the project offers for real world use is specific rather than a claim about popularity: Mozilla Support migrated from Bleach to JustHTML in Kitsune, the Django application behind support.mozilla.org. The package requires Python 3.10 or later and installs with a single command.

Sanitization is the default, and the switch that disables it is explicit

The single most consequential design decision in the library is that `JustHTML(html)` sanitizes before you query or serialize. That inverts the usual order, where you parse, walk the tree, and only later remember to escape the output.

python
from justhtml import JustHTML

doc = JustHTML(
    "<p>Hello<script>alert(1)</script> "
    "<a href='javascript:alert(1)'>bad</a> "
    "<a href='https://example.com'>ok</a></p>",
    fragment=True,
)

print(doc.to_html(pretty=False))
# => <p>Hello <a>bad</a> <a href="https://example.com">ok</a></p>

The output shows both halves of the policy. The script element is gone entirely, and the `javascript:` URL is stripped, leaving the anchor without an href, while the ordinary https link survives untouched. The `fragment=True` argument says the input is a fragment rather than a whole document, which changes how the parser builds the tree.

Turning sanitization off takes an explicit keyword, and the documentation says to do it only for trusted input:

python
doc = JustHTML("<main><p class='intro'>Hello</p></main>", sanitize=False)
intro = doc.query_one("p.intro")

print(intro.to_text())
# => Hello

The security section then says the part that is easy to skim past. JustHTML sanitizes by default, but output safety still depends on where you put it. HTML body output is not automatically safe inside JavaScript, CSS, URL attributes, or other contexts. A sanitized document that you then interpolate into a script tag is still a vulnerability, and the library does not claim otherwise.

One DOM, and the transforms that reshape it

The library's structure is one document with several ways to consume it: parse once, then sanitize, query, transform, serialize, extract text, or produce Markdown. The transform system is the part that carries the most design weight.

python
from justhtml import JustHTML, Linkify, SetAttrs, Unwrap

doc = JustHTML(
    "<p>Hello <span>world</span> example.com</p>",
    fragment=True,
    sanitize=False,
    transforms=[
        Unwrap("span"),
        Linkify(),
        SetAttrs("a", rel="nofollow"),
    ],
)

print(doc.to_html(pretty=False))
# => <p>Hello world <a href="http://example.com" rel="nofollow">example.com</a></p>

Transforms are a pipeline applied at construction, and the three in the example do three different things. `Unwrap("span")` removes the span element but keeps its text. `Linkify()` turns a bare domain in the text into a link. `SetAttrs("a", rel="nofollow")` sets an attribute on every anchor. The documented transform set covers unwrapping, dropping, editing attributes, linkifying, and composing cleanup pipelines, which is close to what a content pipeline actually needs and more than a sanitizer alone gives you.

Around that, the documented surface is CSS selectors through `query()` and `query_one()`, allowlisted HTML cleaning with URL policies and inline CSS controls, text output with `to_text()` and Markdown generation, a builder API for constructing nodes directly from Python, and streaming for processing large inputs incrementally. The separate documentation pages for each of these are linked from the readme, so the API is documented per area rather than in one long reference.

A command line for the one-off job

There is a `justhtml` entry point, registered in the package metadata as `justhtml = "justhtml.__main__:main"`, and it covers the jobs you would otherwise write a ten line script for.

bash
# Pretty-print an HTML file
justhtml index.html

# Parse from stdin
curl -s https://example.com | justhtml -

# Extract text from selected nodes
justhtml index.html --selector "main p" --format text

# Convert selected HTML to Markdown
justhtml index.html --selector "article" --format markdown

The stdin form is the one that earns its keep, since it lets you fetch a URL with curl and pipe it straight into the parser. The `--selector` and `--format` pair is the other useful combination, and it maps directly onto the library API: select nodes, then emit text or Markdown.

The package also ships a console script, so the command is available as soon as the package is installed, and the `Makefile` in the repository shows that the documentation site is served from a Jekyll container with a `docs` target. There is also a hosted playground linked from the readme, which is a reasonable way to try the sanitizing behaviour on a sample document before writing any code.

Correctness is argued with the web platform test suite

The argument for a parser is usually speed, and this project does not make it. The argument here is conformance, and the evidence is specific.

JustHTML is tested against the web platform html5 treebuilder tests, plus html5lib serializer and encoding fixtures, and project specific sanitizer, selector, transform, CLI and regression tests. The current test suite enforces 100 percent combined line and branch coverage, including the parser engine. And the parser engine additionally requires exact agreement with the reference path across the web platform html5 treebuilder suite. The details are in a correctness document in the repository.

That is a strong claim to make and an expensive one to keep, which is why the repository has an `ARCHITECTURE.md`, a `CHANGELOG.md`, a `CONTRIBUTING.md`, an `AGENTS.md`, a `Makefile`, a `run_tests.py`, a pre-commit configuration, and a `test-summary.txt` committed at the root. It also has a `PERFORMANCE.md` and a `benchmarks/` directory, and the benchmark extra in the package manifest pulls in zstandard, html5lib, lxml, beautifulsoup4, selectolax, html5-parser, markupever and turbohtml, which is a list of the parsers it is willing to be measured against.

The build is hatchling, the linting is Ruff with a large rule selection and a long ignore list, and type checking is mypy with strict-ish settings, skipped on PyPy.

Where JustHTML is the wrong tool

The project states its own limit, and it is worth repeating in the project's own framing: if you are parsing terabytes of trusted HTML, use a C or Rust parser. If you need reliable handling of untrusted or malformed HTML inside a Python app, use JustHTML.

That is a performance boundary, and it is not the only one. Pure Python parsing costs more per document than a C library, and while the repository carries benchmark tooling, the readme does not quote throughput numbers, so a team with a throughput requirement will have to measure it themselves.

The second limit is output context. The security section is explicit that HTML body output is not automatically safe inside JavaScript, CSS, URL attributes, or other contexts. If your application injects untrusted content into a script or a style block, the sanitizer does not solve that, and you need a context-aware escaping step that this library does not claim to provide.

The third limit is the Python version floor. Python 3.10 or later, which excludes a chunk of deployed environments, and while the package works on PyPy and Pyodide, the mypy configuration explicitly skips type checking on PyPy, which is a small signal that the two runtimes are not equally first class.

The fourth is a scope question about the ecosystem. A project that wants to use the reference `html.parser`, or that already has a working BeautifulSoup setup with a sanitizer in front of it, gains little by adding another library. The readme links a comparison document and a playground, and those are the right places to check the specific overlap before migrating.

MIT terms, frequent releases, and a licence metadata mismatch

On the licence there is a small discrepancy worth flagging, and the right behaviour is to read the file. The readme states that JustHTML is MIT, free to use for commercial and non-commercial projects, and the package metadata carries the classifier `License :: OSI Approved :: MIT License` with the licence declared as a file. The repository's own metadata, however, lists the licence as a custom one that GitHub cannot classify, with a pointer to the `LICENSE` file. The project also ships a `SECURITY.md` with a supported-version policy and vulnerability reporting instructions, which is the document that matters for a library that processes untrusted input.

The release cadence is fast and the version numbers are high for a young project: v3.13.1 published on 2026-10-12, v3.13.0 on 2026-09-30, v3.12.0 on 2026-09-27, with the last push to the repository on 2026-10-12 as well. A high minor number with releases days apart suggests active development rather than a settled API, which matters for a library you intend to depend on for a long time.

The project is the work of Emil Stenström, and the repository carries a code of conduct, a contributing guide, an `llms.txt` file, a `project_ideas.md`, and a `Makefile` whose only target serves the documentation with a Jekyll container. The docs site lives at emilstenstrom.github.io/justhtml, and the comparison, migration and per-area documentation pages linked from the readme cover most of the questions a new user would ask.

Editorial conclusion

JustHTML fits a Python service that handles untrusted or malformed HTML, wants one dependency instead of a parser plus a sanitizer, and needs to run where a compiler is not available, including PyPy and Pyodide. It does not fit a pipeline parsing terabytes of trusted HTML, where the project itself says a C or Rust parser is the right answer, and it is not a substitute for context-aware output escaping, since its documentation states that HTML body output is not automatically safe inside JavaScript, CSS or URL attributes. Before you adopt it, read `docs/comparison.md`, and if you are migrating from Bleach, use the migration guide rather than porting policies by hand.

Frequently asked questions

How do I install JustHTML and parse HTML with it?

Run `pip install justhtml`, which requires Python 3.10 or later and installs no runtime dependencies. Then `from justhtml import JustHTML`, construct a document with `JustHTML(html)`, and query it with `query()` or `query_one()` using CSS selectors.

Does JustHTML sanitize HTML by default?

Yes. `JustHTML(html)` sanitizes before you query or serialize, removing script elements and unsafe URL schemes such as javascript:. You disable that only for trusted input by passing `sanitize=False`.

Is JustHTML safe to interpolate into JavaScript or CSS?

No. The project documentation states that HTML body output is not automatically safe inside JavaScript, CSS, URL attributes, or other contexts, even though the library sanitizes by default. Context-specific escaping is still the caller's responsibility.

When should I use a C or Rust HTML parser instead of JustHTML?

The project's own guidance is that if you are parsing terabytes of trusted HTML you should use a C or Rust parser, and if you need reliable handling of untrusted or malformed HTML inside a Python app you should use JustHTML. The repository also ships benchmarks and a PERFORMANCE.md document.

How do I migrate code that uses Bleach?

The project documentation includes a Bleach migration guide at docs/bleach-migration.md, described as a way to move existing sanitizer code to JustHTML policies. Mozilla Support is cited as having migrated from Bleach to JustHTML in Kitsune, the Django application behind support.mozilla.org.

Official sources

  1. EmilStenstrom/justhtml on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/emilstenstrom-justhtml.svg)](https://hysenlabs.com/projects/emilstenstrom-justhtml)

Was this useful?