html2text installs one package taking 0.2 MiB — nine times smaller than the nearest Python alternative and forty times smaller than the Node one. It completed all four pages in the conversion suite and retained all 16 registered body probes. Those probes check whether selected body strings survive; they do not score hierarchy, list nesting, link destinations, repeated content, or full table fidelity.
It is also licensed GPL-3.0-or-later, which is the one property in this comparison that does not show up in any benchmark and can rule a library out entirely.
What html2text is
html2text is a Python library that turns HTML into Markdown-ish plain text. The lineage goes back to Aaron Swartz's original, and the current maintained line sits at 2025.4.15 — a date-versioned release from April 2025, with 2,168 GitHub stars, 95 open issues, and a last push in October 2025.
Official reference: html2text's official repository.
import html2text
h = html2text.HTML2Text()
h.body_width = 0 # see below; the default will surprise you
md = h.handle(html)
pip install html2text pulls 1 package and 0.2 MiB, cold-importing in 0.077 s. Zero dependencies. In a container image or a Lambda layer, that is a meaningful difference from markdownify's 1.8 MiB and turndown's 8.8 MiB.
The default that changes every number

body_width defaults to 78. html2text hard-wraps every line of output at 78 characters unless you turn it off.
That is a reasonable default for a library whose original job was producing readable plain text for terminals and email. It is the wrong default for anything feeding a model or a diff, where inserted newlines change tokenisation, break long links across lines, and make output comparison meaningless.
I set body_width = 0 for everything below, and I am flagging it rather than burying it: with wrapping on, every character count and token count in this article would be different. If you are benchmarking converters yourself, this is the setting that will silently make your numbers incomparable.
The measurement
I ran html2text on a named four-page conversion suite also used for the markitdown comparison, with pre-registered probe strings — selected body strings that must survive and boilerplate strings whose presence shows page chrome came through. Same four files, same probes, one scorer across all four converters. Probe survival measures registered string presence, not structural correctness; the table and link columns are separate for exactly that reason.
| Converter | Body probes | Output chars | Tokens (o200k) | Markdown table rows | Links |
|---|---|---|---|---|---|
| html2text | 16/16 | 76,452 | 21,176 | 32 | 545 |
| markdownify | 16/16 | 76,868 | 21,062 | 36 | 599 |
| markitdown | 16/16 | 76,995 | 21,336 | 36 | 598 |
| turndown | 16/16 | 95,188 | 26,236 | 0 | 611 |
fourway-scores.json. Four fixtures, tokens counted with o200k_base.
Smallest output of the four at 76,452 characters, and effectively tied on tokens with markdownify and markitdown — 21,176 against 21,062 and 21,336, a spread of 1.3% that I would not call a difference.
32 table rows against markdownify's and markitdown's 36 in the four-page suite. The four-row gap appears on the irregular Wikipedia fixture rather than in the outer-pipe formatting difference described next.
Fewest links at 545, against 598 to 611 for the others. Worth checking against your own pages if link preservation matters — it is the one column where html2text sits clearly below the group rather than within it.
The tables my regex could not see

This one is worth telling because I nearly published the wrong finding about it.
My first table-row counter required leading and trailing pipes — ^\|.*\|$. Against that rule, html2text scored 1 table row across five files: the four-page conversion suite plus a separate synthetic complex-table fixture. That counter measured one Markdown style, not tables.

It does. It emits them like this:
Team Name | Year | Wins | Losses | Win %
---|---|---|---|---
Boston Bruins | 1990 | 44 | 24 | 0.55
No outer pipes. This is conventional pipe-table syntax, but the harness did not validate it across Markdown renderers. It is invisible to a regex expecting the outer-pipe style. Rewriting the counter to look for a run of pipe-containing lines with a separator row inside it moved html2text from 1 row to 32 in the four-page suite and 91 across all five files.
So the finding is not that html2text lacks tables. It is that two of these four converters emit outer pipes and one does not, which matters if you post-process the Markdown with your own pattern matching. That is a real thing to know and I would have missed it entirely by trusting my first number.
What it removes, and where it loses rows
Two findings that pull in opposite directions.
It strips <script> and <style>. Counted by markers that only occur inside those elements, html2text's output across the fixtures carries zero script markers and zero style markers. turndown's carries 10 and 84 — on the Wikipedia fixture, eight lines of MediaWiki's inline JavaScript config and CSS worth 14,644 characters (script-style-stripping.json). For model-bound output, this was the largest observed source of avoidable text on that fixture. No downstream cost model was measured.
It loses table rows on the hard page. The four-page suite reconciles as follows:
| Fixture | html2text | markdownify |
|---|---|---|
| Books to Scrape | 0 rows | 0 rows |
| Quotes to Scrape | 0 rows | 0 rows |
| Hockey statistics | 27 rows | 27 rows |
| Wikipedia | 5 rows | 9 rows |
| Four-page total | 32 rows | 36 rows |
The separate synthetic complex-table fixture adds 59 html2text rows and 62 markdownify rows, bringing the five-file totals to 91 and 98. It is not part of the four-page headline comparison. They agree on the clean hockey table. On Wikipedia, where the tables are nested and irregular, html2text emits five rows where markdownify emits nine.
So the pattern is: simple tables, identical; awkward tables, html2text keeps less. If your pages carry the kind of tables Wikipedia does, test that before committing. If they carry the kind a stats page does, the two are interchangeable on this axis.
The licence
| Library | Licence | Packages | Disk |
|---|---|---|---|
| html2text | GPL-3.0-or-later | 1 | 0.2 MiB |
| markdownify | MIT | 5 | 1.8 MiB |
| turndown | MIT | 3 (npm) | 8.8 MiB |
Official reference: html2text on PyPI.
Confirmed in three places: the PyPI metadata, the GitHub repository, and the installed package's own METADATA file, which reads License-Expression: GPL-3.0-or-later.
What that means depends on how the software is integrated, conveyed, and distributed. Internal or network-only use is generally a different GPL scenario from shipping software that includes or combines with the package, but this article is not a legal analysis. Teams that distribute software should have counsel review the exact integration and distribution model.
The awkward part is the correlation. The library with the smallest footprint, the one you would pick precisely because you are trying to keep a distributable artifact small, is the one with the licence that most constrains distribution. Both alternatives are MIT.
I am not a lawyer and this is not advice — just the fact, sourced, because it is the property most likely to matter and least likely to appear in a comparison table.
Maintenance
Last release 2025.4.15, last repository push October 2025 — roughly ten months before testing, with 41 releases behind it. requires_python >= 3.9, and it installed and ran cleanly on Python 3.14.2.
That is quieter than markdownify (last release six weeks before testing) and turndown (four months), and considerably more active than nothing. For a library that converts HTML to text — a problem that does not move much — a ten-month gap reads as stable rather than abandoned. Ninety-five open issues is the larger flag, and worth a scan for anything resembling your use case before you commit.
Memory, and what broken HTML does to it
Two operational questions are measured separately here.
The broader stress-test context is in the ten-library memory and malformed-HTML comparison.
Peak resident memory, via /usr/bin/time -l, one fresh process per cell — the import floor is what the library costs loaded and idle, the peaks include the document.
| Library | Runtime | Import floor | 226 KB peak | 10 MB peak |
|---|---|---|---|---|
| html2text | python3.14 | 18.7 | 19.9 | 71.2 |
| pyquery | python3.14 | 30.3 | 33.9 | 172.5 |
| resiliparse | python3.14 | 20.5 | 25.1 | 225.1 |
| markdownify | python3.14 | 23.9 | 28.9 | 278.5 |
| goose3 | python3.14 | 44.1 | 52.4 | 398.5 |
| cheerio | node22 | 66.8 | 76.5 | 398.5 |
| justext | python3.14 | 30.3 | 36.6 | 431.2 |
| newspaper4k | python3.14 | 52.6 | 61.8 | 668.5 |
| trafilatura | python3.14 | 52.5 | 64.8 | 927.1 |
| turndown | node22 | 47.8 | 68.4 | 2947.1 |
memory-results.json. Python and Node baselines are not comparable to each other; the interpreter is inside both.
html2text is the lightest entry here on both measured axes. Its 18.7 MiB import floor and 71.2 MiB peak on the 10 MiB fixture give an incremental RSS of 52.5 MiB: (71.2 - 18.7) / 10 = 5.25× the fixture size. Compare that with the absolute and incremental rows in the table, keeping the stated Python/Node baseline caveat in view.
Broken HTML. Twelve documents each breaking exactly one thing — unclosed tags, mis-nested inline elements, unquoted attributes with spaces, stray closers, no <html> at all, duplicate attributes, a document truncated mid-tag, bad entities, an unclosed <script>, a lying charset declaration, a comment containing markup, and 600 levels of nesting — plus two well-formed controls at matched sizes, because "it returned nothing" only says something about malformedness if the library is not also silent on a clean document of the same size.
html2text raised on 0 of 14 and returned nothing on 0, recovering 33/33 sentinels across the broken fixtures (malformed-results.json). One fixture is excluded from that count: per HTML5 everything after an unclosed <script> is script content, so losing it there is correct and recovering it is the deviation.
Pros and cons
In its favour. One package, 0.2 MiB, zero dependencies — by far the smallest in the comparison. Smallest output of the four and effectively tied on tokens with markdownify and markitdown. Emits recognizable pipe-table syntax. Runs on Python 3.14. Long, stable lineage.
Against it. GPL-3.0-or-later, which the alternatives are not. body_width=78 by default, which hard-wraps output and changes measurements or diffs unless disabled. Fewest links preserved (545 against 598–611). Tables use the no-outer-pipe style, which breaks naive downstream regex. Ninety-five open issues, and a quieter release cadence than markdownify.
Who should use it, and who should not
Use html2text when the dependency budget is genuinely tight and the intended integration and distribution model has passed license review. One package with zero dependencies is a real operational advantage: a smaller dependency surface to audit and deploy.
Set body_width = 0 on the first line unless you specifically want wrapped plain text.
Skip it if you distribute software and copyleft is a problem — markdownify is MIT, ties it on tokens, and matches markitdown on tables for 1.6 MiB more. Skip it if link preservation matters to you, since it kept the fewest. And skip it if your downstream tooling assumes outer pipes on table rows.
Where a managed API fits
html2text converts HTML you already have. It does not fetch, render JavaScript, or handle an anti-bot layer — none of the four converters do, and on many real targets that is the harder half of the job.
For the same fixtures across all five converters, see the five-way HTML-to-Markdown comparison.
A hosted fetch/render/extraction service, including our own Thunderbit, operates at a different layer. Thunderbit was not benchmarked here. The relevant boundary is supplied-HTML conversion versus a service that acquires and processes a URL; this article supplies no same-metric quality, latency, or cost comparison.
The fair framing: if you hold the HTML, want Markdown, and the GPL is not a problem for how you ship, html2text is free and remarkably small. If you are fetching pages, or want rows rather than prose, that is a different purchase.
For the wider field, our web scraping API roundup covers hosted options and the open-source scraper pillar the self-hosted ones. Converting HTML to Markdown in Python is the practical walkthrough.
Try Thunderbit for Web Data Extraction
Should you use html2text?
It is a strong candidate when the footprint matters, wrapping is disabled deliberately, and the distribution model passes license review.
Its token count was within 1.3% of markdownify and markitdown in the four-page suite. That does not make overall quality equivalent: html2text retained fewer links and fewer rows on the irregular Wikipedia fixture. Two operational details matter immediately: body_width = 0, and the no-outer-pipe table style.
If GPL review rules it out, markdownify is MIT, had similar output size and token count here, retained more links and irregular-table rows, and used 1.6 MiB more disk in this environment.
Try Thunderbit for Web Data Extraction Get Started Free
FAQs
Does html2text convert tables?
Yes. My first counter said it produced one table row across five files, and that counter was wrong — it required leading and trailing pipes, while html2text emits Team Name | Year | Wins without them. That is conventional pipe-table Markdown, though this harness did not run a cross-renderer compatibility test. With the corrected counter, html2text produced 32 rows against markdownify's 36 in the four-page suite and 91 against 98 when the separate complex-table fixture is included.
What does body_width do, and why change it?
It hard-wraps output at 78 characters by default, a sensible choice for terminal-readable plain text and a poor one for anything else. Wrapping inserts newlines mid-sentence, splits long URLs across lines, and changes tokenisation. Every number in this review used body_width = 0; with the default, they would all be different.
Is the GPL licence a real constraint?
It depends on the exact integration and distribution model. Internal or network-only use and conveying software are different screening scenarios, but this article does not determine their legal outcome. Teams shipping software should have counsel review the GPL-3.0-or-later terms; markdownify and turndown are MIT. The html2text license expression is confirmed in PyPI metadata, GitHub, and the installed package's METADATA file.
Is a release from April 2025 a problem? Probably not, on its own. HTML-to-text conversion is a stable problem, the library installed and ran cleanly on Python 3.14.2, and 41 releases sit behind it. Ninety-five open issues is the number I would actually check — scan them for anything resembling your input before committing, because a quiet repository means you may be the one fixing it.
What was not tested here?
Four conversion fixtures and one separate complex-table fixture are still a small suite. The test did cover twelve synthetic malformed documents plus two controls: html2text raised on 0/14, returned empty on 0/14, and recovered all 33 scored sentinels. It did not cover real-world damaged pages, broader malformed patterns, nested lists, definition lists, footnotes, or math. The full option surface — ignore_links, ignore_images, unicode_snob, single_line_break and the rest — stayed at defaults apart from body_width. The link gap was observed but not diagnosed, and Markdown round-tripping was not tested.


