mistune
mistune turns Markdown into HTML, and it is fast because the parsing is done with a small number of large regular expressions rather than a character-by-character scanner. One call, mistune.html(text), covers the common case. Past that, create_markdown() lets you pick which plugins are on (tables, footnotes, strikethrough, task lists, math, definition lists and more), swap the renderer for one you subclass, or turn the renderer off entirely and get an AST of plain dicts you can walk yourself. It also ships renderers that emit Markdown and reStructuredText, plus a directive system for admonitions and tables of contents. No dependencies outside typing-extensions on older Pythons.
The fastest mainstream Markdown parser in Python, with an AST and a renderer system that make customization genuinely easy. Use create_markdown() rather than mistune.html() so escaping is on, and keep an eye on Wenmode, which the author now presents as the successor.
Use it if
- You render a lot of Markdown and speed matters: the maintainer's own benchmark table has mistune ahead of Python-Markdown, markdown2, mistletoe and markdown-it-py on most cases, several times over on some
- You want an AST rather than a string. create_markdown(renderer=None) returns nested dicts with type, attrs and children, which is far easier to post-process than parsing HTML back out
- You need to customize output at the node level: subclassing HTMLRenderer and overriding one method, for example block_code to run Pygments, is a handful of lines
- You need Markdown converted to something other than HTML: MarkdownRenderer (normalizing round-trip) and RSTRenderer ship in the box
- You are rendering untrusted input and reach for mistune.html(). That entry point does not escape raw HTML, so a comment containing a script tag passes through verbatim. create_markdown() escapes by default, but neither is a sanitizer; you still need nh3 or bleach on the output
- Strict CommonMark conformance is a requirement. The docs claim compatibility with CommonMark 0.31.2, but markdown-it-py is the one that runs the published spec suite and matches the JavaScript reference implementation edge case for edge case
- You want the maintainer's current focus. The README now opens by pointing at Wenmode, his newer parser, which he reports as roughly 1.5 to 1.8 times as fast as mistune on his benchmark corpora. mistune still gets fixes, but the succession has been announced in public
- You are on mistune 0.8.4 or 2.x and expect an upgrade. v3 is a rewrite: renderer method signatures changed, the old Renderer class and mistune.markdown() are gone, and plugins are named strings or functions now. Budget real time, not an afternoon
- You need a large extension catalog. Python-Markdown plus pymdown-extensions covers wikilinks, superfences, snippets, keys and a hundred other niceties that mistune has no equivalent for
Setup reality
pip install mistune, no dependencies to speak of, and mistune.html(text) works immediately. The setup work is deciding on one Markdown instance and reusing it, because create_markdown() compiles its parsers each time you call it and building one per request is a waste. Then two decisions that are easy to get wrong. Escaping: mistune.html() ships with escape off and with strikethrough, table and footnotes on, while create_markdown() ships with escape on and no plugins at all, so the two entry points behave differently in the way that matters most for security. Plugins: pass them as strings to create_markdown(plugins=[...]), except directives, which have to be wrapped in FencedDirective or RSTDirective because v3 supports two syntaxes. Two smaller notes: the historical speedup plugin is accepted but ignored now that its fast paths are in the core parsers, and PyPI still classifies 3.3.4 as Development Status 4, Beta.
Patterns
One-call Markdown to HTMLrender-quickly
import mistune
mistune.html("# Title\n\n~~gone~~")
# '<h1>Title</h1>\n<p><del>gone</del></p>\n'
mistune.html("<script>alert(1)</script>")
# '<script>alert(1)</script>\n' <- passed through unchangedmistune.html() has escaping OFF and strikethrough, table and footnotes ON. It is the right call for your own trusted content and the wrong call for anything a user typed.
Escape raw HTML from untrusted authorsrender-untrusted-input
import mistune
md = mistune.create_markdown() # escape=True by default
md("<div>hi</div>")
# '<p><div>hi</div></p>\n'
mistune.create_markdown(escape=False)("<div>hi</div>")
# '<div>hi</div>\n'Escaping stops raw HTML blocks, but it does not stop a javascript: URL in a link or an onerror in an image the renderer emits. For user content, run the output through nh3 or bleach as well.
Turn on the Markdown extensions you wantenable-plugins
import mistune
md = mistune.create_markdown(plugins=[
"strikethrough", "table", "footnotes",
"task_lists", "url", "def_list", "math",
])
md("- [x] done\n")
# '<ul>\n<li class="task-list-item"><input class="task-list-item-checkbox" '
# 'type="checkbox" disabled checked/>done</li>\n</ul>\n'Plugins are opt-in for create_markdown() and cost parsing time each, so list only what you render. The historical "speedup" plugin is still accepted but does nothing now: its fast paths were folded into the core parsers.
Build the parser once, not per requestreuse-one-instance
# markdown.py
import mistune
render_md = mistune.create_markdown(
escape=True, plugins=["table", "strikethrough"]
)
# views.py
from .markdown import render_md
html = render_md(post.body)create_markdown() assembles block and inline parsers and their plugin hooks every call. The returned object is a reusable callable and is fine to hold at module scope for read-only rendering.
Override one node type, such as fenced codecustom-renderer
import mistune
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
class HighlightRenderer(mistune.HTMLRenderer):
def block_code(self, code, info=None):
if info:
lexer = get_lexer_by_name(info, stripall=True)
return highlight(code, lexer, HtmlFormatter())
return "<pre><code>" + mistune.escape(code) + "</code></pre>\n"
md = mistune.create_markdown(renderer=HighlightRenderer())
md("```python\nx = 1\n```")Renderer methods map one-to-one onto node types: link(text, url, title), image(alt, url, title), heading(text, level, **attrs), list(text, ordered, **attrs). Accept **attrs on the ones that take it so a plugin adding an attribute does not raise TypeError.
Get an AST instead of a stringast-output
import mistune
ast = mistune.create_markdown(renderer=None)
ast("hello **world**")
# [{'type': 'paragraph', 'children': [
# {'type': 'text', 'raw': 'hello '},
# {'type': 'strong', 'children': [{'type': 'text', 'raw': 'world'}]}]}]
mistune.create_markdown(renderer="ast")("# hi")
# [{'type': 'heading', 'attrs': {'level': 1}, 'style': 'atx',
# 'children': [{'type': 'text', 'raw': 'hi'}]}]Nodes are plain dicts, so walking them needs no imports, but the shape is not stable across major versions: leaf nodes carry raw, container nodes carry children, and some carry attrs. Write one recursive walker and keep it in one place.
Render to reStructuredText or normalized Markdownconvert-to-rst-or-markdown
import mistune
from mistune.renderers.rst import RSTRenderer
from mistune.renderers.markdown import MarkdownRenderer
to_rst = mistune.create_markdown(renderer=RSTRenderer())
to_rst("# Title\n\nhi **b**")
# 'Title\n=====\n\nhi **b**\n'
mistune.create_markdown(renderer=MarkdownRenderer())("# Title\n\nhi")
# '# Title\n\nhi\n'MarkdownRenderer is the practical way to normalize user-submitted Markdown before storing it. Neither renderer covers every plugin's node types, so exotic syntax can round-trip as literal text.
Add admonitions with a directive pluginadmonitions-and-directives
import mistune
from mistune.directives import FencedDirective, Admonition, TableOfContents
md = mistune.create_markdown(plugins=[
FencedDirective([Admonition(), TableOfContents()]),
])
md("```{note}\nbe careful\n```")
# '<section class="admonition note">\n'
# '<p class="admonition-title">Note</p>\n<p>be careful</p>\n</section>\n'Directives cannot be passed as plain strings in plugins, because v3 supports two syntaxes; wrap them in FencedDirective for the ```{name} form or RSTDirective for the .. name:: form. Admonition covers note, warning, danger, tip and five others.
Generate a table of contents with anchor idstable-of-contents
import mistune
from mistune.directives import RSTDirective, TableOfContents
md = mistune.create_markdown(plugins=[RSTDirective([TableOfContents()])])
md(".. toc::\n :max-level: 2\n\n# A\n\n## B\n")
# '<details class="toc" open>...<a href="#toc_1">A</a>...'
# '<h1 id="toc_1">A</h1>\n<h2 id="toc_2">B</h2>\n'Heading ids are generated as toc_1, toc_2 and so on, not slugified from the heading text, so links are not stable if someone inserts a section. Override the renderer's heading method if you need slug anchors.
Pass math through for KaTeX or MathJaxmath-blocks
import mistune
md = mistune.create_markdown(plugins=["math"])
md("$$\na^2 + b^2\n$$")
# '<div class="math">$$\na^2 + b^2\n$$</div>\n'
md("inline $a^2$ here")The plugin only wraps the math and leaves the source intact; it does not render anything. You still load KaTeX or MathJax on the page and point it at .math.
Build the Markdown object from partsexplicit-construction
import mistune
from mistune.plugins.formatting import strikethrough
from mistune.plugins.table import table
renderer = mistune.HTMLRenderer(escape=True)
md = mistune.Markdown(renderer, plugins=[strikethrough, table])
md("~~x~~") # '<p><del>x</del></p>\n'Importing plugin functions directly, instead of naming them as strings, is what you want when you ship your own plugin alongside the built-ins or when a typo in a plugin name must fail at import rather than at runtime.
Convert files from the shellcommand-line
python -m mistune -m "Hi **Markdown**"
# <p>Hi <strong>Markdown</strong></p>
python -m mistune -f README.md -o readme.html
cat README.md | python -m mistune --escape --hardwrap
python -m mistune -f doc.md -p table strikethrough -r astUseful for a quick check that a document parses the way you expect before wiring it into an application. The -r flag takes html, ast, markdown or rst, which is the fastest way to eyeball the AST for a confusing block.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| markdown-it-py | PyPI | You need real CommonMark conformance, a token stream, or plugin parity with the JavaScript markdown-it ecosystem. |
| Markdown | PyPI | You want the largest extension catalog in Python, especially with pymdown-extensions, and MkDocs-style output; speed is not the constraint. |
| wenmode | PyPI | You want the same author's newer parser: mdast-compatible AST, safe HTML defaults, streaming output, and faster on his benchmarks. |
| mistletoe | PyPI | You want a small pure-Python CommonMark parser whose AST and renderer design is the easiest to read and extend. |