The Most Popular HTML-to-Markdown Library Doesn't Convert Tables

Last Updated on August 17, 2026
The Most Popular HTML-to-Markdown Library Doesn't Convert Tables
AI Summary
turndown was the most-starred of the four tested libraries at the metadata snapshot, with 11,386 GitHub stars. On the four shared HTML fixtures it produced zero Markdown tables with its core defaults and used 24.6% more tokens than markdownify for the same inputs. Every one of the four recovered every content probe. The differences are entirely in what happens to structure, and in what that structure costs you downstream. Feeding a model, or storing structured content from pages like these? Start with markdownify. It ties markitdown on emitted table-row count under this counter, sits in the lowest-token cluster, is MIT, and installs in 1.8 MiB.

turndown was the most-starred of the four tested libraries at the metadata snapshot, with 11,386 GitHub stars. On the four shared HTML fixtures it produced zero Markdown tables with its core defaults and used 24.6% more tokens than markdownify for the same inputs.

Every one of the four recovered every content probe. The differences are entirely in what happens to structure, and in what that structure costs you downstream.

What was measured, and on whose fixtures

This research base already had a pack for markitdown with five HTML fixtures and pre-registered probe strings — exact strings checked for survival, plus boilerplate strings used to detect page chrome. The aggregate comparison uses the four fixtures shared by all four converters: a bookshop catalogue, a quotes site, a hockey-statistics table, and the Wikipedia article on web scraping. A fifth fixture, Nothing but tables, is used only as a table-heavy diagnostic and is excluded from the 24.6% aggregate.

The four: turndown 7.2.4 (Node), markdownify 1.2.3, html2text 2025.4.15, and markitdown, whose published rows are used as-is. Pages: a bookshop catalogue, a quotes site, a hockey-statistics table, and the Wikipedia article on web scraping.

Every metric name below matches markitdown's own artifact field for field, so the rows sit next to each other without anyone reconciling two definitions of the same word.

The table

Measured results chart: Token output vs Markdown table rows

ConverterBody probesOutput charsTokens (o200k)Bytes/tokenMarkdown table rowsLinks
turndown16/1695,18826,2363.630611
markdownify16/1676,86821,0623.6536599
html2text16/1676,45221,1763.6132545
markitdown16/1676,99521,3363.6136598

Four fixtures — the ones markitdown also ran. Full per-fixture numbers in fiveway-scores.json. Tokens counted with o200k_base; markitdown's table rows recounted from its own stored Markdown with the same counter used on the others.

Content survival is a tie. All sixteen body probes across the four fixtures survived every converter. If your only question is "will the text make it through", any of these four answers yes.

Structure is not a tie. On the four shared fixtures, markdownify and markitdown both emit 36 Markdown table rows; html2text emits 32; turndown emits none. In the separate table-only diagnostic, markdownify emitted 62 rows and html2text 59; those rows are not included in the aggregate above.

Token cost is 24.6% higher for turndown, and the reason is not the tables. I assumed it was, and the per-fixture numbers say otherwise — see below.

What turndown does to a table

Here is the hockey-statistics fixture, the same rows, three ways.

turndown:

System diagram: Core Table Paths Diverge

Team Name

Year

Wins

Losses

Boston Bruins

1990

44

24

markdownify:

| Team Name | Year | Wins | Losses | ... |
| --- | --- | --- | --- | --- |
| Boston Bruins | 1990 | 44 | 24 | ... |

html2text:

Team Name  |  Year  |  Wins  |  Losses  | ...
---|---|---|---|---
Boston Bruins  |  1990  |  44  |  24  | ...

Every value survives turndown's conversion, which is why it scores 16/16 on the probes. What does not survive is which column each value belongs to. Read the turndown output and 44 is just a number on a line; you cannot recover that it is Boston's win count without counting positions and hoping no cell was empty. In this fixture some cells are empty, so counting positions does not work either.

For a model reading the output, that is the difference between a table it can answer questions about and a list of numbers it will guess at.

This is not a defect so much as a documented boundary — turndown's core does not handle tables, and turndown-plugin-gfm exists to add them. But the default install does not include it, and 11,386 stars suggests a lot of people are using the default.

Where the token gap actually comes from

I wrote the paragraph above believing the 24.6% token gap was the flattened tables showing up as characters. Then I looked at it per fixture, and it is not.

Fixtureturndown tokens Ă· markdownify tokens
Quotes site (no tables)0.99Ă—
Bookshop catalogue1.06Ă—
Hockey statistics (one big table)1.37Ă—
Wikipedia (mostly prose, 9 table rows)1.29Ă—
Nothing but tables0.78Ă—

On the fixture that is nothing but tables, turndown is 22% cheaper — because pipe scaffolding costs tokens too, and turndown emits none of it. Flattening a table is not, on its own, a token penalty.

The Wikipedia fixture is 74% of the total, and it has nine table rows. Its 15,378-character gap cannot be tables. It is this:

(function(){var className="client-js vector-feature-language-in-header-enabled…
.mw-parser-output cite.citation{font-style:inherit;word-wrap:break-word}…
(RLQ=window.RLQ||[]).push(function(){mw.config.set({"wgHostname":"mw-web…

turndown does not strip <script> and <style> content. markdownify and html2text both do. Counted by markers that only occur inside those elements: turndown's output carries 10 script markers and 84 style markers across the fixtures; the other two carry zero of each. On the Wikipedia page, eight lines of MediaWiki's inline JavaScript config and CSS account for 14,644 characters — 95% of the entire gap (script-style-stripping.json).

That is the finding I would actually act on. A flattened table is a structure problem you can see. A JavaScript config blob in your Markdown is pure cost with no information in it at all, and on a real-world page it dwarfs everything else in this comparison.

html2text writes tables you might not recognise

html2text scored 32 rows where markdownify and markitdown scored 36, and the first version of my counter scored it at 1.

That was my counting rule, not the library. html2text emits Team Name | Year | Wins without leading and trailing pipes — a common Markdown table form, but invisible to a regex that requires ^\|.*\|$. I had written that regex, run it, and been ready to report that html2text does not do tables.

It does. The corrected counter uses a heuristic: a run of consecutive pipe-containing lines with a separator row inside it. With that rule, html2text goes from 1 to 32. This is not a full Markdown parser, so the row totals should be read as measurements under a documented counter rather than universal rendering results.

Worth knowing if you post-process the Markdown with your own regex: two of these four libraries emit outer pipes and one does not.

The licence nobody mentions

ConverterLicenceInstallCold importStarsLast release
turndownMIT3 npm packages, 8.8 MiB0.056 s11,3862026-04-03
markdownifyMIT5 packages, 1.8 MiB0.046 s2,2352026-06-30
html2textGPL-3.0-or-later1 package, 0.2 MiB0.077 s2,1682025-04-15
markitdownSee its package metadataNot measured in this install runNot measured——

install-and-import.json. Each library installed into its own empty environment. Licences confirmed from three places: the registry metadata, the GitHub repo, and the installed package's own METADATA file, which reads License-Expression: GPL-3.0-or-later.

The lightest library in this install comparison — one package, 0.2 MiB — is GPL-3.0-or-later. Whether that affects a project depends on how the software is combined and distributed. Treat it as a selection checkpoint for whoever owns licensing; this article is not legal advice. markitdown is shown as unmeasured here because its install and licence were not captured by this particular artifact.

That trade is easy to miss because the licence is the one property that does not show up in a benchmark.

All three are dramatically lighter than the article-extraction libraries in the same category — those run 21 to 70 MiB. A converter is a much smaller thing than an extractor, and worth separating in your dependency budget.

Two confounds I had to fix before this table was true

The numbers above are the third version. The first two were wrong in ways worth naming, because both are easy to reproduce.

Five fixtures against four. markitdown ran four of these five files; the other three ran all five. Totalling each tool over its own set gave markdownify 98 table rows against markitdown's 36 and made it look like a capability gap. Over the same four, it is 36 against 36 — an exact tie. The fifth fixture is the table-heavy one, so the confound ran in the worst possible direction, inflating the newcomers against the incumbent by nearly threefold.

System diagram: Make the Comparison Comparable

Two counters, one column. markitdown's published md_table_rows came from its own code, which I had not read. Comparing that number against mine could have been comparing two counters rather than two converters. Its Markdown output is stored on disk, so the fix was to run one counter over all four — and when I did, markitdown's recount came out at exactly its published figure per fixture (0, 0, 27, 9). The definitions agreed; I just could not have known that without checking.

Neither error would have been visible in the output. Both would have produced a confident, wrong table.

Who should use what

Feeding a model, or storing structured content from pages like these? Start with markdownify. It ties markitdown on emitted table-row count under this counter, sits in the lowest-token cluster, is MIT, and installs in 1.8 MiB. Validate it on your own page shapes before standardising.

Dependency budget measured in kilobytes, and you are not distributing? html2text. One package, 0.2 MiB, tables intact. Check the GPL question first, and note the last release was April 2025.

Already in a Node stack? turndown, with turndown-plugin-gfm installed alongside it — and strip <script> and <style> from the HTML before you hand it over, because turndown will not. Those two omissions cost a quarter more tokens and your table structure, and neither is visible unless you look at the output.

Already converting other document formats? markitdown handles PDF, Office and more, and its HTML output is competitive with the dedicated converters. One dependency rather than two is worth something.

Where a managed API fits

All four of these take HTML you already have. None fetches a page, renders JavaScript, or handles an anti-bot layer — and for a lot of real targets, that is the harder half.

Our own developer stack at Thunderbit covers that side. POST /distill takes a URL and returns clean LLM-ready Markdown with rendering and fetching handled; POST /extract returns AI schema-matched structured JSON against a JSON Schema you supply, which is a different output shape again — rows rather than a Markdown table you would then have to parse. Both are reachable from an MCP server and a CLI (npx @thunderbit/thunderbit-cli). Pricing is on the Thunderbit pricing page.

The honest comparison: if you hold the HTML and you want Markdown, markdownify is free and does the job well, and this table says which of its rivals do too. If you are fetching the pages, or you want structured 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, and what llms.txt is trying to standardise covers where this whole category is heading.

Try Thunderbit for Web Data Extraction

Verdict

For this four-fixture workload, markdownify is the strongest default: the same emitted table-row count as markitdown under the shared counter, a token count within 1.3% of the other efficient converters, MIT, and a 1.8 MiB install.

That gap between popularity and measured behaviour is the finding. turndown is an excellent library that a great many people have installed without the plugin that makes it handle tables, and the cost of that shows up as a quarter more tokens and a table structure that no longer exists. Neither number appears anywhere until you count them.

If you take one thing: check what your converter does to a table before you trust its output to a model. Three of these four do something sensible. The most popular one does not, unless you asked it to.

Try Thunderbit for Web Data Extraction Get Started Free

FAQs

Does turndown really not support tables? Its core does not. Tables come from turndown-plugin-gfm, a separate package, and the plain npm install turndown does not include it. Without the plugin, every cell survives as its own paragraph — the values are all there, the row and column relationships are not. Across four fixtures that produced zero Markdown table rows and 24.6% more tokens than markdownify for the same content.

Why did html2text look like it had no tables at first? Because my counter required leading and trailing pipes and html2text does not emit them. Team Name | Year | Wins is legal Markdown and renders correctly; it is simply a different style from | Team Name | Year |. The corrected counter looks for a run of pipe-containing lines with a separator row, the way a parser does, and html2text goes from 1 row to 32. If you post-process Markdown with your own regex, this is the difference that will bite you.

Is the GPL on html2text a real problem? It depends on how you combine and distribute the software. GPL-3.0-or-later can create obligations that MIT does not, so involve whoever owns licensing before selecting it for a distributed product. This is a compliance checkpoint, not legal advice; the licence identity was confirmed in registry metadata, the repository, and the installed package's METADATA.

Are these token counts meaningful for other pages? The 24.6% gap is mostly turndown keeping <script> and <style> content, so it scales with how much of that a page carries — heavy on a modern CMS page, near zero on a static one. Tables move it the other way: on the fixture that is nothing but tables turndown came out 22% cheaper, because it emits no pipe scaffolding. Bytes per token sat between 3.61 and 3.65 for all four, so output density is the same everywhere and the difference is quantity. Measure your own corpus if the number matters to a budget.

What was not tested here? Real-world variety — four fixtures is four fixtures. Nested lists, definition lists, footnotes, and math. Malformed HTML, which is where converters historically diverge most. Round-tripping the Markdown back to HTML. docling, which has the same fixtures on disk but whose published run does not report these fields, so it is absent rather than estimated. And configuration: html2text was run with body_width=0 because its default of 78 hard-wraps every line, which would have changed every character and token count in this table.

Ke
Ke
CTO at Thunderbit | Senior Data Scientist & ML Expert With nearly a decade of experience in machine learning and data science, Ke Shen is a Columbia University alumnus and former Senior Data Scientist at Walmart Labs. With deep, peer-recognized expertise in Python, R, Java, and Statistics, he shares battle-tested insights on taking complex AI algorithms from theory to production-grade architecture.
Table of Contents
Thunderbit · AI web data agent

Extract data from any page in 1 click

Trusted by 250,000+ users
free plan available
From webpage to spreadsheet
Describe what you need — Thunderbit's AI Agent scrapes it and exports to Excel, Google Sheets, Airtable, or Notion. Free to start.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week