mrkeyoor.com_
Sun 20 Sept 11:43 UTC
PyPIUtilsupdated 19 Sept 2026

markdownify review

markdownify 1.2.3 turns an HTML string or a selected Beautiful Soup node into Markdown. Its converters cover headings, emphasis, links, lists, images, fenced code, line breaks, and ordinary tables. Options choose heading markers, bullet rotation, escaping, paragraph wrapping, parser features, and which tags participate. A subclass can override output for a particular HTML element. Version 1.2.3 fixes text placed inside `br` when using `html.parser` and corrects the declared `None` types for `strip_pre` and `wrap_width`. It converts structure; it does not identify the useful article inside a full page.

Verdict

markdownify 1.2.3 installed in 0.2 seconds and occupied 1 MB in our sandbox, with 0 audit findings. Install it for selected HTML fragments; pair it with extraction for whole pages and reject structures that Markdown cannot represent.

We installed it

Lab card: what happened when we installed markdownifyScreenshot of markdownify documentation
Install✓ · 0.2s5 packages on disk · 1 MB
Importimport markdownify in 0.38s · pure Python · py.typed
Known vulns0(pip-audit)

Answers from our run

Does markdownify install cleanly?

Yes. In a fresh container with an empty cache, pip install markdownify finished in 0.2s, leaving 5 packages and 1 MB on disk. pip-audit reported no known vulnerabilities.

What does markdownify need to run?

Python 3.x, and nothing compiled: it is pure Python. In our run import markdownify succeeded in 0.38s, and the package ships py.typed for type checkers.

markdownify or html2text: which should you use?

html2text: Pick it for readable plain-text output with established wrapping, reference-link, and image switches. markdownify 1.2.3 installed in 0.2 seconds and occupied 1 MB in our sandbox, with 0 audit findings.

When should you not use markdownify?

A complete web page must be reduced to its article automatically. Menus, banners, cookie notices, and footers remain unless another step removes them.

API stability3/5The callable `markdownify()`, `MarkdownConverter`, and keyword options make a small public API, and ordinary calls remain recognizable across releases. Subclasses saw a real break in 1.0 when tag converters replaced `convert_as_inline` with `parent_tags`. Escaping, edge-newline stripping, and other defaults also change exact text snapshots, so consumers should pin the release and test a representative HTML corpus.
Docs3/5The README documents each option and its default, then shows filtering, parser choice, BeautifulSoup-node conversion, subclass overrides, and the CLI. That gets a normal converter running. There is no separate element-support matrix, versioned API reference, or detailed account of losses around nested tables, forms, iframes, and unknown tags. Those cases have to be established with fixtures and source inspection.
Maintenance4/5PyPI released 1.2.3 on June 30, 2026, and GitHub records a push the same day. The release fixed nested text inside `br` for `html.parser` and corrected public option types after typed-package work in 1.2.2. GitHub now shows 2,244 stars and 44 open issues and pull requests. That is recent, concrete maintenance for a focused converter.
Ecosystem4/5The stored usage snapshot counts 17,671,150 weekly downloads. Accepting either HTML text or an existing BeautifulSoup node lets markdownify sit directly after scrapers, CMS exports, and DOM selectors. Its scope stops at conversion. Article discovery comes from tools such as trafilatura, plain-text preferences fit html2text, and broader document formats require Pandoc or another conversion system.

Use it if

  • The article or content fragment is already selected and its headings, links, lists, code, and simple tables should survive as Markdown.
  • A CMS import needs one pinned policy for heading syntax, emphasis markers, bullets, escaping, and line wrapping.
  • A site-specific HTML element can be handled by overriding one `MarkdownConverter.convert_*` method.
  • The scraper already has a BeautifulSoup subtree and can pass it to `convert_soup()` without parsing the document again.
Skip it if

Setup reality

Our fresh Python 3.12 install of markdownify 1.2.3 took 0.2 seconds, created 5 packages, and occupied 1 MB. pip-audit found 0 known vulnerabilities. The pure-Python distribution has 2 direct dependencies, includes py.typed, carries an MIT license, and declares no Python version constraint in package metadata. import markdownify succeeded in 0.38 seconds. Optional parsers were outside that measurement.

Beautiful Soup uses the built-in html.parser unless bs4_options selects something else. Malformed markup may build a different tree under lxml or html5lib, so install and pin the chosen parser when output is snapshot-tested. Pass only the article node to convert_soup(). Feeding the root document also converts navigation, cookie copy, and footer text. Clean or sanitize HTML in a separate stage.

Defaults are visible in diffs: H1 and H2 use Setext underlines, unordered-list bullets rotate through *+- by depth, and literal asterisks and underscores are escaped. A br emits 2 trailing spaces plus newline unless BACKSLASH is selected. Formatters often remove those spaces. strip and convert are mutually exclusive. Current custom overrides accept (self, el, text, parent_tags); extensions written for the old convert_as_inline argument will break when that tag appears.

Patterns

Translate a short HTML fragment convert-html

from markdownify import markdownify as md

md('<b>Yay</b> <a href="http://github.com">GitHub</a>')
# '**Yay** [GitHub](http://github.com)'

md('<h1>Title</h1><p>Hello <b>world</b></p>')
# 'Title\n=====\n\nHello **world**'

The return value is text. Version 1.2.3 removes separator newlines at both outer edges by default while preserving spacing inside the document.

Emit hash-prefixed headings atx-headings

from markdownify import markdownify as md, ATX, ATX_CLOSED

md('<h1>Title</h1>', heading_style=ATX)         # '# Title'
md('<h2>Title</h2>', heading_style=ATX_CLOSED)  # '## Title ##'
md('<h1>Title</h1>')                            # 'Title\n====='

H1 and H2 default to underlined Setext output. Select `ATX` when every heading level should use hashes.

Suppress selected tag formatting strip-or-keep-tags

md('<b>Yay</b> <a href="http://github.com">GitHub</a>', strip=['a'])
# '**Yay** GitHub'

md('<b>Yay</b> <a href="http://github.com">GitHub</a>', convert=['b'])
# '**Yay** GitHub'

`strip` and `convert` cannot appear together; version 1.2.3 raises `ValueError`. Both retain child text while controlling which tags add Markdown syntax.

Control literal Markdown punctuation control-escaping

md('<p>a_b*c</p>')
# 'a\\_b\\*c'

md('<p>a_b*c</p>', escape_asterisks=False, escape_underscores=False)
# 'a_b*c'

md('<p>see - and # in text</p>', escape_misc=True)

Literal `*` and `_` are protected by default. Turn off an escape only after testing the destination renderer with real content.

Add a language to code fences code-blocks

md('<pre><code>x = 1</code></pre>', code_language='python')
# '```python\nx = 1\n```'

def lang_from_pre(el):
    code = el.find('code')
    if code and code.has_attr('class'):
        return code['class'][0].removeprefix('language-')
    return None

md(html, code_language_callback=lang_from_pre)

The callback receives `<pre>`, even when the syntax class lives on its child `<code>`. Inspect that nested node before returning a language.

Promote the first table row to a header tables

html = '<table><tr><td>1</td><td>2</td></tr><tr><td>3</td><td>4</td></tr></table>'

md(html)
# '|  |  |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |'

md(html, table_infer_header=True)
# '| 1 | 2 |\n| --- | --- |\n| 3 | 4 |'

Without `<th>` or `<thead>`, the default inserts an empty header row. Inference consumes the first body row as headings; nested tables remain lossy.

Convert only the selected article node convert-soup

import requests
from bs4 import BeautifulSoup
from markdownify import MarkdownConverter, ATX

soup = BeautifulSoup(requests.get(url, timeout=10).content, 'html.parser')
article = soup.select_one('article') or soup.body

markdown = MarkdownConverter(heading_style=ATX).convert_soup(article)

DOM selection is the extraction step. Call `convert_soup()` for an existing node; `convert()` expects HTML text and parses it again.

Override output for site-specific elements custom-converter

from markdownify import MarkdownConverter

class ImageBlockConverter(MarkdownConverter):
    def convert_img(self, el, text, parent_tags):
        return super().convert_img(el, text, parent_tags) + '\n\n'

    def convert_p(self, el, text, parent_tags):
        if 'callout' in (el.get('class') or []):
            return f'\n> {text.strip()}\n\n'
        return super().convert_p(el, text, parent_tags)

markdown = ImageBlockConverter(heading_style='ATX').convert(html)

The 1.x override signature ends with `parent_tags`. A 0.x subclass using `convert_as_inline` fails only when its overridden element is converted.

Choose visible hard breaks and wrapping line-breaks-and-wrapping

md('a<br>b')                            # 'a  \nb'   two trailing spaces
md('a<br>b', newline_style='BACKSLASH') # 'a\\\nb'

md(long_html, wrap=True, wrap_width=80)
md(long_html, wrap=True, wrap_width=None)   # reflow to one long line

Default hard breaks depend on 2 trailing spaces that formatters may strip. `BACKSLASH` survives diffs but is an extended Markdown convention.

Lock the parser used for HTML repair choose-parser

md(html, bs4_options='lxml')
md(html, bs4_options=['html5lib'])
md(html, bs4_options={'features': 'lxml', 'from_encoding': 'iso-8859-8'})

lxml and html5lib are optional installs. A string or list selects parser features; a dictionary passes BeautifulSoup constructor options such as encoding.

Use one bullet marker at every depth bullets-and-lists

md('<ul><li>a<ul><li>b</li></ul></li></ul>')
# '* a\n  + b'

md('<ul><li>a<ul><li>b</li></ul></li></ul>', bullets='-')
# '- a\n  - b'

md('<ol><li>a</li><li>b</li></ol>')
# '1. a\n2. b'

The default `*+-` rotates by nesting depth. Supplying one character keeps every unordered list on the same marker.

Convert a file or stdin from the shell cli-usage

$ markdownify example.html > example.md
$ cat example.html | markdownify --heading-style ATX > example.md
$ markdownify -h

No input path means stdin. CLI flags use dashed forms of the same option names accepted by the Python function.

Alternatives

PackageRegistryPick it when
html2textPyPIPick it for readable plain-text output with established wrapping, reference-link, and image switches.
pypandocPyPIPick it when HTML is only one of several input formats and a Pandoc installation is acceptable.
beautifulsoup4PyPIPick it alone when you only need DOM selection and cleanup, without Markdown generation.
trafilaturaPyPIPick it when locating article content and metadata in a complete page is the primary job.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.