On the four HTML fixtures shared with markitdown, markdownify produced the same number of emitted Markdown table rows under our counter — 36 against 36 — and recovered all 16 checked content strings. Its 21,062-token output was nominally lowest, although the first three converters were within 1.3%. It installs in 1.8 MiB, is MIT licensed, and had 2,235 GitHub stars at the snapshot.
It is the least discussed library in this category and, on these numbers, the one I would reach for.
What markdownify is
markdownify is a Python HTML-to-Markdown converter built on BeautifulSoup. That is the whole architecture: parse with BeautifulSoup, walk the tree, emit Markdown. Version tested: 1.2.3, MIT, 2,235 stars, 42 open issues, last released 2026-06-30 — actively maintained, 44 releases.
Official reference: python-markdownify's official repository.

The API is one function:
from markdownify import markdownify
md = markdownify(html)
There is a class form (MarkdownConverter) if you want to override element handling, and a good set of options — heading style, bullet characters, code language detection, element stripping. But the one-liner is the common case and it works.
pip install markdownify pulls 5 packages and 1.8 MiB, and cold-imports in 0.046 s — the fastest of the three converters I tested it against. The dependency tree is BeautifulSoup and its usual companions, which many Python projects already carry, so the marginal cost is often close to zero.
The measurement
I ran it on the four HTML fixtures another pack in this base had already used for markitdown, with that pack's pre-registered probe strings — 16 exact body strings checked for survival, plus boilerplate checks for page chrome. A fifth fixture, Nothing but tables, is a separate table-heavy diagnostic and is excluded from the four-fixture aggregate below.
| Converter | Body probes | Output chars | Tokens (o200k) | Markdown table rows | Links |
|---|---|---|---|---|---|
| markdownify | 16/16 | 76,868 | 21,062 | 36 | 599 |
| html2text | 16/16 | 76,452 | 21,176 | 32 | 545 |
| 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, table rows counted by one rule across all four — including a recount of markitdown's stored output, which matched its published figure exactly.
Three things stand out.
It ties markitdown on emitted table-row count. 36 rows each on the four shared fixtures, counted by the same heuristic. That does not prove cell-for-cell equivalence; it says both outputs exposed the same number of recognizable Markdown table rows to this counter.
It has the lowest token count. 21,062, marginally below html2text's 21,176 and markitdown's 21,336, and 19.7% below turndown's 26,236. The first three are within 1.3% of each other, which I would call a tie rather than a win; the gap that matters is to turndown.
Every content probe survived. All 16. So did they for the other three — content survival is not where these libraries differ.
The table it gets right
The hockey-statistics fixture is where converters separate. markdownify:
| Team Name | Year | Wins | Losses | OT Losses | Win % | ... |
| --- | --- | --- | --- | --- | --- | --- |
| Boston Bruins | 1990 | 44 | 24 | | 0.55 | ... |
Standard GFM with leading and trailing pipes, a separator row, and — note the empty cell between 24 and 0.55 — it emits the empty cell rather than skipping it. That sounds trivial and is not: a converter that drops empty cells shifts every value after it into the wrong column, and the output still looks like a valid table.

turndown on the same input emits every value as its own paragraph with no column structure at all. html2text produces a correct table in a different style, without the outer pipes.
If the Markdown is going to a model, the pipes and the preserved empty cell are what let it answer "how many games did Boston lose" instead of guessing.
Two things it removes that turndown does not
Table fidelity is the visible difference. This one is bigger and nobody mentions it.
markdownify strips <script> and <style> content. turndown does not. Counted by markers that occur only inside those elements, markdownify's output across the fixtures carries zero script markers and zero style markers; turndown's carries 10 and 84. On the Wikipedia fixture that is the difference between 59,561 characters and 74,939 — and eight lines of MediaWiki's inline JavaScript config and CSS account for 14,644 characters, 95% of that gap (script-style-stripping.json).
For anything feeding a model, that is the most expensive thing in this whole comparison: a JavaScript config blob costs tokens and carries no information at all. markdownify removes it without being asked. html2text does too.
Per fixture, markdownify and html2text agree exactly where the table is simple and diverge where it is not:
| Fixture | markdownify | html2text |
|---|---|---|
| Hockey statistics | 27 rows | 27 rows |
| Wikipedia | 9 rows | 5 rows |
| Nothing but tables (separate diagnostic) | 62 rows | 59 rows |
markdownify emits more recognized rows on both diagnostic comparisons. On Wikipedia specifically it keeps nine rows where html2text keeps five under this counter. That is a useful warning to inspect representative nested and irregular tables before you pick, not proof that every emitted cell is semantically correct.
Install and licence, next to the alternatives
| Library | Packages | Disk | Cold import | Licence | Stars | Last release |
|---|---|---|---|---|---|---|
| markdownify | 5 | 1.8 MiB | 0.046 s | MIT | 2,235 | 2026-06-30 |
| html2text | 1 | 0.2 MiB | 0.077 s | GPL-3.0-or-later | 2,168 | 2025-04-15 |
| turndown | 3 (npm) | 8.8 MiB | 0.056 s | MIT | 11,386 | 2026-04-03 |
Official reference: markdownify on PyPI.
install-and-import.json and metadata-snapshot.json. Each library installed into its own empty environment.
html2text is nine times smaller on disk, and it is GPL-3.0-or-later. For a distributed product, treat that as a checkpoint for whoever owns licensing; this article is not legal advice. markdownify is MIT, which is generally more permissive, but still belongs in the normal compliance review.
The 1.8 MiB is also somewhat notional if your project already uses BeautifulSoup, which a great many Python scraping projects do — in that case markdownify is close to free.
markdownify's release cadence is the healthiest of the three: 44 releases and a push six weeks before testing, against html2text's last release in April 2025.
What one line of Python actually gets you
The whole library is markdownify(html), and it is worth being explicit about what that call decides on your behalf, because three of those decisions are the ones this comparison turned on.
It parses with BeautifulSoup, removes <script> and <style> without being asked, and emits GFM-style pipe tables with outer pipes and preserved empty cells. Parser pedigree alone does not guarantee malformed-input behavior, so that question is measured separately below.
None of those are options you set. They are what the default does, and in a category where turndown's default keeps JavaScript and html2text's default hard-wraps at 78 characters, a library whose out-of-the-box behaviour needs no correction is worth something on its own.
The options exist when you want them — heading_style, bullets, code_language, strip and convert for element allowlists and denylists, and MarkdownConverter for per-element overrides. Everything measured here used none of them.
Memory, and what broken HTML does to it

The broader stress-test context is in the ten-library memory and malformed-HTML comparison.
Two things every review in this batch listed as untested, now measured.
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.
markdownify sits in the middle: a 23.9 MiB floor and 278.5 MiB on a 10 MB document. That is 3.9× html2text's peak and about a tenth of turndown's, which is the trade for BeautifulSoup underneath it.
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.
markdownify raised on 1 of 14 and returned nothing on 0, recovering 30/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. Table fidelity equal to markitdown, counted by the same rule. Lowest token count of the four. MIT. 1.8 MiB, and close to zero marginal cost if BeautifulSoup is already in your tree. Fastest cold import at 0.046 s. Actively released. Preserves empty table cells. Overridable per-element conversion through MarkdownConverter. The plain one-line call is the right call.
Against it. Depends on BeautifulSoup, so it is nine times the disk of html2text if you do not already have it. 2,235 stars means a smaller community than turndown's — fewer worked examples when you hit something odd. Python only, so no help in a Node stack. And 42 open issues.
Who should use it, and who should not
Start with markdownify for a Python workload resembling these fixtures. It matches markitdown's emitted row count under this counter, sits in the lowest-token cluster, and is MIT. If you already depend on BeautifulSoup, its marginal install footprint may be small; validate the actual dependency delta in your environment.
Consider html2text instead if the dependency budget is measured in hundreds of kilobytes and its output shape works for your pages. Review GPL-3.0-or-later with whoever owns licensing before distribution.
Consider markitdown instead if you are already converting PDFs or Office documents. Their HTML results had the same emitted row count here, but broader fidelity was not shown to be equivalent.
Skip it in Node, where turndown is the natural answer — with turndown-plugin-gfm installed alongside it, because turndown's core produces no tables at all.
Where a managed API fits
markdownify converts HTML you already have. It does not fetch, does not render JavaScript, does not handle an anti-bot layer — none of the four converters do, and on a lot of 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.
Our own developer stack at Thunderbit addresses that upstream job: POST /distill fetches a URL and returns Markdown, while POST /extract returns schema-shaped JSON. Both are available through an MCP server and CLI; pricing is on the Thunderbit pricing page. Those hosted endpoints were not benchmarked against these local converters, so this is a category distinction, not a performance comparison.
The honest framing: if you hold the HTML and want Markdown, markdownify is free and does the job as well as anything here. If you are fetching pages, or you 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 markdownify?
For Python, it is a strong candidate when your inputs resemble these fixtures; test it on your own tables and malformed pages before making it the default.
It ties markitdown on emitted table-row count, sits in the lowest-token cluster, imports fastest in this run, and is MIT. Against that, it used more memory than html2text, is Python-only, and raised on one malformed-input fixture. Disk and licensing are additional selection factors, not the only arguments.
What strikes me is the star count. turndown has five times the attention and, out of the box, converts none of the tables markdownify handles correctly. Popularity in this category is not tracking measured behaviour, which is most of why this comparison was worth running.
Try Thunderbit for Web Data Extraction Get Started Free
FAQs
Is markdownify really as good as markitdown on HTML? On these four fixtures, they emitted 36 recognized Markdown table rows each, recovered all 16 checked strings, and produced output within 0.2% in character count. That is not a cell-level or rendering-equivalence test. markitdown also handles PDF and Office formats, so this comparison is only about the measured HTML outputs.
Does it need BeautifulSoup? Yes — that is its parser, and most of the 1.8 MiB. If your project already uses BeautifulSoup, markdownify's marginal cost is small. If not, and disk really matters, html2text is 0.2 MiB with one package, at the cost of a GPL-3.0-or-later licence.
How does it handle empty table cells? It emits them. In the fixture with gaps in the data, the output keeps the empty cell in position, so every subsequent value stays in its correct column. A converter that skips empty cells produces a table that still looks valid and has every value one column to the left, which is the worse failure because nothing signals it.
Should I use the function or the class?
The markdownify() function for the common case. MarkdownConverter when you need to override how a specific element converts — everything measured here used the plain function with default options.
What was not tested here? Four shared fixtures plus one table-only diagnostic are not a representative corpus. The separate malformed suite covered 12 named breakages and two controls, but not every form of broken HTML. Nested lists, definition lists, footnotes, math, Markdown-to-HTML round trips, cell-level table equivalence, and configuration variants were not compared. Conversion speed was also not compared.


