How to convert HTML to Markdown in Python
Two libraries do most of the work in Python: markdownify and html2text. We ran both on the same real pages; they differ in code blocks, in-page links and tables, and both need help with relative links.
1. Install
pip install markdownify html2text beautifulsoup4
2. The one-liners
from markdownify import markdownify
md = markdownify(html, heading_style='ATX')
import html2text
h = html2text.HTML2Text(); h.body_width = 0 # 0 = no hard wrapping at 78 columns
md = h.handle(html)
3. What differed on real pages
| markdownify | html2text | |
|---|---|---|
| Code blocks (MDN, 23 blocks) | Fenced ```; language via a callback | Indented 4 spaces, no language |
In-page links (#section) | Kept (20 on MDN, 72 on Wikipedia) | Dropped as links: text only |
| Tables | GFM with outer pipes (| --- |) | Pipes between cells only (---|---), also renders on GitHub |
| Link titles | "fetch()" | Escaped: "fetch\(\)" |
| Line wrapping | None | Wraps at 78 columns unless body_width = 0 |
Relative links (/en-US/docs/...) | Left relative by both (html2text has a baseurl option); they break once the Markdown leaves the site | |
For Markdown that goes into docs, notes or an LLM prompt, markdownify's fenced code blocks are the main reason to pick it. html2text is fine for prose-only pages and email bodies.
4. A complete script
Downloads a page, keeps only the main content (no menus or footers), makes links and images absolute, keeps the code language and writes a .md file. Save as h2md.py:
import re, sys, urllib.request
from urllib.parse import urljoin
from bs4 import BeautifulSoup
from markdownify import markdownify
url = sys.argv[1]
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (h2md)'})
soup = BeautifulSoup(urllib.request.urlopen(req).read(), 'html.parser')
main = soup.select_one('article, main, [role=main]') or soup.body # skip menus, headers and footers
for tag in main.select('script, style, nav, aside, form, button'):
tag.decompose()
for a in main.select('a[href]'): # relative links would break outside the site
if not a['href'].startswith('#'): a['href'] = urljoin(url, a['href'])
for img in main.select('img[src]'):
img['src'] = urljoin(url, img['src'])
def lang(pre): # 'brush: js', 'language-python', 'highlight-python' -> fence language
m = re.search(r'(?:brush:\s*|language-|highlight-)(\w+)', ' '.join(pre.get('class', []) + (pre.code.get('class', []) if pre.code else [])))
return m.group(1) if m else ''
md = markdownify(str(main), heading_style='ATX', bullets='-', code_language_callback=lang)
md = re.sub(r'\n{3,}', '\n\n', md).strip() + '\n'
open(sys.argv[2], 'w', encoding='utf-8').write(md)
print(f'{len(md)} chars, {md.count(chr(10))} lines -> {sys.argv[2]}')
python3 h2md.py https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch fetch.md
Our run: 32151 chars, 668 lines -> fetch.md, with all 23 code blocks fenced as ```js and every link absolute. The first lines:
# Using the Fetch API
The [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) provides a JavaScript interface for making HTTP requests and processing the responses.
5. Limits
- Pages built by JavaScript have little or no content in the downloaded HTML. Use a headless browser, or convert the page you already see in your browser (below).
- The
article, mainselector fits most sites; adjust it if a page puts the text elsewhere. - Footnote markers and edit links on Wikipedia come through as links (
[[1]](#cite_note-1)); strip them with a regex if you do not want them.
6. No code
To convert pasted HTML, use the free HTML to Markdown converter (runs in your browser). To copy the page you are reading as clean Markdown, including pages built by JavaScript, use MDGrab.
Frequently asked questions
What is the best Python library to convert HTML to Markdown?
markdownify for most uses: it keeps fenced code blocks (with a language if you supply a callback), in-page links and GitHub-style tables. html2text is older and also good for prose, but indents code and drops anchor links.
How do I convert an HTML table to Markdown in Python?
markdownify converts <table> to a GitHub-style Markdown table by default. Cells with line breaks or nested lists are flattened, so check complex tables.
Can it handle pages that need JavaScript?
No. Both libraries convert the HTML you give them. Render the page first (for example with Playwright) or copy it from your browser.
Written with AI assistance. On 2026-10-01 markdownify 1.2.3 and html2text 2025.4.15 (with BeautifulSoup 4.15) were run on MDN's 'Using the Fetch API' page (23 code blocks) and Wikipedia's 'Comma-separated values' article (tables, footnotes), with Python 3.13. MDGrab is our extension; the other methods are free.