mrkeyoor.com_
Thu 06 Aug 07:41 UTC
PyPIUtilsupdated 06 Aug 2026

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.

Verdict

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.

API stability4/5The 3.x API has held since 2023 and 3.3.4 is a point release, but the 0.8 to 2 to 3 history is three incompatible rewrites, so anyone with older code learned to distrust major bumps here.
Docs4/5mistune.lepture.com has separate pages for the guide, renderers, plugins, directives, the AST and upgrading, each with working code. It is thinner on security guidance, which is where the html() versus create_markdown() escaping difference should be spelled out loudly.
Maintenance3/5Pushed July 2026 with only 17 open issues (18 counting PRs), so the tracker is genuinely tended. The reservation is direction: the README's first section promotes the author's replacement parser, and PyPI still labels 3.3.4 as Beta.
Ecosystem4/5About 18.7 million weekly downloads, largely transitive through nbconvert and documentation tooling, with built-in plugins covering most common Markdown extensions. There is very little third-party plugin activity compared with Python-Markdown.

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

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 unchanged

mistune.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>&lt;div&gt;hi&lt;/div&gt;</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 ast

Useful 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

PackageRegistryPick it when
markdown-it-pyPyPIYou need real CommonMark conformance, a token stream, or plugin parity with the JavaScript markdown-it ecosystem.
MarkdownPyPIYou want the largest extension catalog in Python, especially with pymdown-extensions, and MkDocs-style output; speed is not the constraint.
wenmodePyPIYou want the same author's newer parser: mdast-compatible AST, safe HTML defaults, streaming output, and faster on his benchmarks.
mistletoePyPIYou want a small pure-Python CommonMark parser whose AST and renderer design is the easiest to read and extend.