markdown-it-py
markdown-it-py is the Python port of the JavaScript markdown-it parser: 100% CommonMark compliant, with a token-stream architecture where every rule can be enabled, disabled, or replaced, and a plugin system (mdit-py-plugins) for tables, footnotes, front matter, and more. It is the parser under rich, myst-parser, mdformat, and Jupyter tooling, which is why it quietly sits at 155M weekly downloads. Maintained by the Executable Books project and part of Google's Assured Open Source set.
The most correct and most extensible markdown parser in Python, and the right default when compliance or token-level access matters. Its speed is only middling and the extras-based packaging trips people, but as the base of rich and MyST it has proven itself; use mistune only when raw throughput wins.
Use it if
- You need strict CommonMark behavior that matches a JS frontend: the Python and JS implementations track the same spec and mostly the same plugin semantics
- You need the token stream, not just HTML: linters, formatters like mdformat, and doc tools work on the parsed tokens, and this is the best token API in Python markdown parsers
- You want syntax extensions as composable plugins (footnotes, front matter, MyST) rather than a monolithic parser with flags
- You are already in the rich/jupyter/myst ecosystem, where it is the established base and already installed
- You want maximum speed: the Rust binding markdown-it-pyrs and mistune are both significantly faster, and for bulk conversion of millions of documents pure Python parsing is the bottleneck
- You rely on python-markdown's extension ecosystem (toc, admonition, wikilinks as configured in MkDocs): those extensions do not exist here in the same form
- You expect sanitized output: with html enabled it passes raw HTML through, and safe rendering of untrusted input needs a separate sanitizer like nh3 or bleach
- You just need README-to-HTML once: the stdlib-adjacent markdown package is simpler if you have no spec-compliance or tokenization needs
Setup reality
pip install markdown-it-py brings only mdurl, but the useful stuff is in extras you have to know about: plugins (tables beyond GFM, footnotes, front matter) require mdit-py-plugins via the [plugins] extra, and linkify requires linkify-it-py via [linkify], and forgetting either fails only at runtime. Version 4.x requires Python 3.10+. Security is explicitly your job: the docs say to disable html or sanitize output for untrusted input, and there is no built-in sanitizer. The preset system (commonmark, gfm-like, zero) changes both defaults and enabled rules, which surprises people migrating configs.
Patterns
Render markdown to HTMLbasic-render
from markdown_it import MarkdownIt
md = MarkdownIt("commonmark")
html = md.render("# Hello\n\nSome *markdown* text")The commonmark preset is strict spec behavior: no tables, no strikethrough, raw HTML passed through per spec. Pick the preset consciously; it is the biggest behavior switch.
GitHub-flavored setupgfm-style-setup
from markdown_it import MarkdownIt
md = MarkdownIt("gfm-like")
# tables, strikethrough, linkify, but requires linkify-it-py
html = md.render("| a | b |\n|---|---|\n| 1 | 2 |\n\n~~gone~~ www.example.com")gfm-like enables linkify, so it raises at construction if linkify-it-py is missing: pip install markdown-it-py[linkify]. It is 'like' GFM, not a certified GFM implementation.
Toggle individual syntax rulesenable-disable-rules
md = (
MarkdownIt("commonmark", {"breaks": True, "html": False})
.enable("table")
.disable("image")
)
html = md.render(text)Rules are toggled by name on top of the preset; breaks=True turns newlines into <br>, html=False escapes raw HTML instead of passing it through. disable('image') is a cheap anti-abuse lever for user content.
Add footnotes and front matter via pluginsplugins
from markdown_it import MarkdownIt
from mdit_py_plugins.front_matter import front_matter_plugin
from mdit_py_plugins.footnote import footnote_plugin
md = (
MarkdownIt("commonmark")
.use(front_matter_plugin)
.use(footnote_plugin)
)
html = md.render("---\ntitle: x\n---\n\nA claim [^1]\n\n[^1]: source")Plugins live in the separate mdit-py-plugins package (the [plugins] extra). The front matter plugin only tokenizes the block; parsing the YAML inside it is still your job.
Work with the token streamtoken-stream
tokens = md.parse("# Title\n\npara with [link](https://x.dev)")
for tok in tokens:
print(tok.type, tok.tag, tok.content[:30])
# inline tokens nest under 'inline' tokens:
links = [t for tok in tokens if tok.type == "inline"
for t in tok.children if t.type == "link_open"]This is the API that makes linters and formatters possible: flat block tokens, with inline content nested in token.children. Token attrs (like href) are on t.attrs as a dict.
Inline-only renderingrender-inline
md = MarkdownIt("commonmark")
snippet = md.renderInline("just *emphasis* and `code`, no <p> wrapper")renderInline skips block parsing entirely: no paragraphs, headings, or lists. Right for chat messages and single-line fields where a stray '#' should not become an h1.
Override how a token renderscustom-render-rule
def render_link_open(self, tokens, idx, options, env):
tokens[idx].attrSet("target", "_blank")
tokens[idx].attrSet("rel", "noopener")
return self.renderToken(tokens, idx, options, env)
md = MarkdownIt("commonmark")
md.add_render_rule("link_open", render_link_open)add_render_rule swaps the renderer for one token type; attrSet mutates attributes before default rendering. The canonical example, external links opening in new tabs, is exactly this.
Auto-link bare URLslinkify-urls
# pip install markdown-it-py[linkify]
md = MarkdownIt("commonmark", {"linkify": True}).enable("linkify")
html = md.render("see www.example.com and https://a.dev")Two switches, not one: the linkify option turns the feature on and enable('linkify') activates the rule. Missing linkify-it-py raises ModuleNotFoundError at construction.
Render untrusted input safelysafe-untrusted-input
import nh3
from markdown_it import MarkdownIt
md = MarkdownIt("commonmark", {"html": False}) # escape raw HTML
unsafe_html = md.render(user_text)
safe_html = nh3.clean(unsafe_html)html=False handles raw HTML blocks but rendered output can still carry unwanted attributes from plugins; the project's own security docs recommend sanitizing output rather than trusting parser options.
Convert files from the command linecli-usage
markdown-it README.md CHANGELOG.md > site.html
# or interactively:
markdown-it
# reads stdin until Ctrl-D, prints HTMLThe bundled CLI is deliberately basic (commonmark preset, no plugin flags); it is for quick checks, not a static site pipeline.
Extract front matter datafront-matter-extract
import yaml
from markdown_it import MarkdownIt
from mdit_py_plugins.front_matter import front_matter_plugin
md = MarkdownIt("commonmark").use(front_matter_plugin)
tokens = md.parse(text)
meta = {}
if tokens and tokens[0].type == "front_matter":
meta = yaml.safe_load(tokens[0].content)The plugin exposes the raw block in token.content; use yaml.safe_load, never yaml.load, since front matter is user input in most pipelines.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| mistune | PyPI | You want the fastest pure-Python markdown parser and can accept looser CommonMark compliance. |
| markdown | PyPI | You need the python-markdown extension ecosystem (MkDocs plugins, admonitions, toc) more than spec compliance. |
| markdown-it-pyrs | PyPI | You want this exact parser but faster: it is the Rust binding by the same author, still marked experimental. |