mrkeyoor.com_
Wed 05 Aug 23:09 UTC
PyPIUtilsupdated 05 Aug 2026

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.

Verdict

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.

API stability4/5The core parse/render/token API mirrors markdown-it JS and has been stable across 2.x, 3.x, and 4.x; majors mostly moved Python floors and plugin packaging rather than breaking the parser API.
Docs4/5readthedocs covers architecture, presets, plugins, security, and performance with honest benchmark tables; the renderer-customization docs are thinner and lean on reading markdown-it JS docs.
Maintenance4/5Active in the Executable Books org (pushed August 2026, 4.2.0 current), with funding visibility via Google's Assured OSS; a small maintainer pool is the main risk.
Ecosystem5/5mdit-py-plugins covers most syntax extensions, and being the parser inside rich, myst-parser, mdformat, and Jupyter gives it a massive installed base at 155M weekly downloads.

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
Skip it if

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 HTML

The 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

PackageRegistryPick it when
mistunePyPIYou want the fastest pure-Python markdown parser and can accept looser CommonMark compliance.
markdownPyPIYou need the python-markdown extension ecosystem (MkDocs plugins, admonitions, toc) more than spec compliance.
markdown-it-pyrsPyPIYou want this exact parser but faster: it is the Rust binding by the same author, still marked experimental.