From html2text¶
html2text converts HTML to Markdown. It subclasses the standard library
HTMLParser and streams tokens through a large, well-worn option surface: list markers, emphasis and strong markers,
link styles (inline or reference), image handling, table rendering, line wrapping at body_width, Unicode folding via
its UNIFIABLE table, and a google_doc mode that reads the inline CSS a Google Docs HTML export carries. It ships
a command-line converter alongside the library and is a long-standing default for turning arbitrary web HTML into
readable Markdown in scrapers, mail-to-text pipelines, and documentation tooling.
turbohtml covers the same conversion through to_markdown(), a single fully typed method on the
parsed WHATWG tree. It walks the tree in C instead of driving a Python HTMLParser, and groups the flat html2text
options into named config dataclasses so each concept has one name.
turbohtml vs html2text¶
Dimension |
turbohtml |
html2text |
|---|---|---|
Scope |
Full WHATWG HTML parser and tree with Markdown, plain-text, and HTML serialization |
HTML-to-Markdown conversion only |
Feature breadth |
Same Markdown option surface (lists, inline marks, links, images, tables, wrapping, code, Google Docs mode) plus text extraction, selectors, and sanitization on the shared tree |
Broad, mature Markdown-conversion options including |
Performance |
C tree walk, roughly 50x faster on the benchmark below |
Pure-Python |
Typing |
Fully type annotated, config via frozen dataclasses |
Untyped public API |
Dependencies |
Compiled C extension (prebuilt wheels) |
Pure Python, no compiled dependency |
Maintenance |
Actively developed |
Long-standing, widely deployed, stable |
Feature overlap¶
Port these html2text calls 1:1 onto to_markdown() with a Markdown config:
html2text.html2text(text)->turbohtml.parse(text).to_markdown().List markers, emphasis/strong markers, and quote characters (
ul_item_mark,emphasis_mark,strong_mark,open_quote,close_quote).Link handling: ignore, skip-internal, inline vs reference style, and
baseurlprefixing.Image handling: markdown, alt-only, ignore, or raw HTML, plus a default alt string.
Table rendering: markdown, strip, or raw HTML, with column padding.
Line wrapping at a width, for list items and links.
Unicode folding (
unicode_snob), code marking and block style, single-line-break spacing, aggressive escaping.The
google_docinline-CSS mode and its list-indent step.
What turbohtml adds¶
One parsed tree serves Markdown, layout-aware plain text (
to_text()), and HTML (serialize()); html2text is Markdown-only.Convert any subtree by calling
to_markdown()on a selected element (doc.find("article").to_markdown()) instead of pre-slicing the HTML string.Full spec-compliant WHATWG parsing feeds the conversion, so malformed markup is repaired the same way a browser does before it is rendered to Markdown.
Grouped, frozen-dataclass config (
Markdown.Links,Markdown.Images,Markdown.Tables…) is fully typed and discoverable, versus html2text’s flat set of untyped instance attributes.A shell entry point:
python -m turbohtml to-markdown(installed as theturbohtmlconsole script) reads a file or stdin, covering html2text’s command-line converter.
What html2text has that turbohtml does not¶
Pure-Python install with no compiled extension. html2text runs anywhere a Python interpreter does; turbohtml needs a prebuilt wheel (or a C toolchain to build from source) for the target platform.
baseurlin html2text andMarkdown.Links(base_url=...)both prefix rather than fully resolve; neither does RFC-3986 resolution, so there is no gap here, but do not expect turbohtml to add it.
Performance¶
to Markdown |
turbohtml |
|
|---|---|---|
HTML to Markdown — article (2 KiB) |
2.84 µs |
595 µs (210x) |
HTML to Markdown — list (4 KiB) |
4.47 µs |
1.25 ms (281x) |
HTML to Markdown — table (4 KiB) |
7.25 µs |
1.1 ms (152x) |
HTML to Markdown — configured (4 KiB) |
15.3 µs |
1.23 ms (80.5x) |
Google Docs export to Markdown — google_doc (4 KiB) |
10.7 µs |
596 µs (56.0x) |
How to migrate¶
Swap the import and the call:
# html2text
import html2text
html2text.html2text(text)
# turbohtml
import turbohtml
turbohtml.parse(text).to_markdown()
print(parse("<h1>Title</h1><p>Some <b>bold</b> text.</p>").to_markdown())
# Title
Some **bold** text.
The html2text options map onto the grouped Markdown config fields with one name per concept:
turbohtml |
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Markdown.google_doc() reads the inline-CSS styling a Google Docs HTML export carries: a font-weight of bold
or 700–900 becomes strong, font-style:italic becomes emphasis, a Courier New/Consolas
font-family becomes an inline code span, list-style-type picks the list marker, and each
Markdown.GoogleDoc(list_indent=...) pixels of margin-left add one list-nesting level. With
Markdown.Inline(hide_strikethrough=True) a text-decoration:line-through drops the struck text.
from turbohtml import Markdown
export = '<p><span style="font-weight:700">Quarterly</span> revenue</p>'
print(parse(export).to_markdown(Markdown.google_doc()))
**Quarterly** revenue
Gotchas and pitfalls¶
to_markdown()is a method on any node, so convert a subtree by calling it on the element you selected (doc.find("article").to_markdown()) instead of slicing the HTML string first.Layout-aware plain text (the
inscriptisrole,to_text()) is a separate method; for the unstructured concatenation readtext.base_urldoes simple prefixing rather than full RFC-3986 URL resolution, matching html2text’sbaseurl.html2text wraps prose at
body_width(78) by default; turbohtml wraps only when you setMarkdown.Wrapping(width=...). Set it explicitly to reproduce wrapped output, or leave it unset for unwrapped Markdown.