Every static site generator, documentation pipeline and issue tracker eventually reaches the same moment: someone renders a Markdown string in Python, the page takes 40 ms instead of 4 ms, and nobody can say why. The answer is almost always the parser you picked three years ago without benchmarking it.
There are four serious options in the Python ecosystem — mistune, markdown-it-py, Python-Markdown and mistletoe — and their published claims are not comparable. One says “fast”, another says “high speed”, a third says “the fastest pure-Python CommonMark implementation”. None of them tells you what happens when your document has 120 tables and 480 fenced code blocks.
So I measured it. Same document, same machine, same iteration count, features normalized so nobody wins by silently skipping tables.
TL;DR: Mistune for Throughput, markdown-it-py for CommonMark Fidelity
- Rendering thousands of documents in a pipeline → mistune. It was the fastest in my benchmark at 46.2 ms for a 129 KiB document with tables and strikethrough enabled — about 20% faster than markdown-it-py and roughly 32% faster than the other two.
- You need strict CommonMark compliance and a plugin ecosystem (footnotes, front matter, definition lists, task lists) → markdown-it-py. Slower, but its token stream is the best-designed API of the four and it is the engine behind MyST and much of the modern Python docs stack.
- You maintain a legacy Flask/Django app with
markdown.markdown()calls everywhere → Python-Markdown. The extension API is old but stable, and it will not surprise you. - You need a non-HTML output format (LaTeX, JATS, Markdown-to-Markdown reflow) → mistletoe. Its AST-plus-renderer split is the cleanest way to emit something other than HTML.
Benchmark Results (Measured, Not Quoted)
Setup: a synthetic but realistic technical document — 129.0 KiB of Markdown containing 120 H2 sections, 120 tables, 120 fenced YAML code blocks, 120 blockquotes, 480 list items, links, inline code, emphasis and strikethrough. Eight iterations after a warm-up, best-of-eight reported, Python 3.11, single core, Linux.
| Library | Version | Best time | Throughput | Output size | Tables rendered |
|---|---|---|---|---|---|
| mistune (with table + strikethrough plugins) | 3.4.0 | 46.2 ms | 2.86 MB/s | 178,690 B | 120 |
| markdown-it-py | 4.2.0 | 58.1 ms | 2.27 MB/s | 175,330 B | 120 |
| Python-Markdown (tables + fenced_code) | 3.11.2 | 68.5 ms | 1.93 MB/s | 175,809 B | 120 |
| mistletoe | 1.6.0 | 69.6 ms | 1.90 MB/s | 194,530 B | 120 |
Two footnotes that matter more than the table:
- mistune’s default configuration is faster still (35.3 ms) because it escapes raw HTML and skips extension parsing. If you quote “mistune is 2x faster”, that number usually comes from the default config, which does not render tables at all. The 46.2 ms figure above is the apples-to-apples one.
- All four are order-of-magnitude slower than a compiled parser. If your pipeline renders more than a few thousand pages per build, the real answer is to move rendering to a Rust/Go component (pulldown-cmark, goldmark, comrak — see our cross-language Markdown parser comparison) or to cache rendered HTML. Micro-optimizing between these four buys you 20 ms, not 20 seconds.
Comparison Table: What You Actually Get
Live GitHub data pulled 2026-10-23.
| Project | Stars | License | Last push | CommonMark | Extension model | Output formats |
|---|---|---|---|---|---|---|
| mistune | 3,078 | BSD | 2026-10-19 | Near-complete | Plugins: table, strikethrough, footnote, task lists | HTML |
| markdown-it-py | 1,382 | MIT | 2026-10-19 | 100% | Rule-based, mdit-py-plugins collection | HTML, token stream |
| Python-Markdown | 4,254 | BSD-3 | 2026-10-22 | Partial (Gruber reference) | Extension subclasses | HTML |
| mistletoe | 1,069 | MIT | 2026-10-22 | 100% | Custom tokens + swappable renderers | HTML, LaTeX, AST, Markdown |
Note that Python-Markdown has the most stars but the weakest spec compliance — it implements John Gruber’s original Markdown plus extensions, not CommonMark. That divergence is visible in edge cases: nested list indentation, intraword underscores (snake_case_name staying literal), and how HTML blocks terminate.
Decision Matrix
| Your constraint | Pick | Reason |
|---|---|---|
| Bulk rendering, output correctness verified by tests | mistune | Fastest measured throughput with plugins enabled |
| CommonMark output must match GitHub rendering | markdown-it-py | Full CommonMark compliance, same rule model as the JS original |
Existing code uses markdown.markdown(text) | Python-Markdown | Migration cost exceeds the speed gain |
| Emitting LaTeX, AST JSON, or reflowed Markdown | mistletoe | Renderer is a separate class; output format is swappable |
| You need footnotes + front matter + definition lists today | markdown-it-py | mdit-py-plugins covers all three with maintained releases |
| Rendering inside a request handler with a 5 ms budget | None of these | Pre-render at build time or cache |
| Untrusted user input | markdown-it-py | Explicit html=False and a documented security model |
mistune — Fastest, and Honest About Escaping
mistune 3.x is a rewrite with a clean plugin system. The default is deliberately conservative:
| |
Raw HTML is escaped unless you opt in. That default is the single best security decision in this comparison — most Markdown vulnerabilities in Python apps come from parsers that pass raw HTML through and let the developer forget to sanitize.
For a real documentation pipeline you enable what you need explicitly:
| |
The plugin API is a class with parse_* methods, and the renderer is separate (mistune.HTMLRenderer), so you can subclass either. For very large documents, md.parse(text) gives you the AST, which you can walk without regenerating HTML.
Where mistune wins: raw speed, a small dependency surface, and sane defaults. Where it hurts: the plugin set is smaller than markdown-it-py’s, and escape=False plus a plugin that emits raw HTML is a real XSS path if you render user content.
markdown-it-py — CommonMark Fidelity and the Best Token API
markdown-it-py is a Python port of the JavaScript markdown-it. It passes the CommonMark test suite and exposes a token stream instead of only HTML, which is why documentation tools like MyST build on it.
| |
The tokens list is the reason to choose this library for anything beyond “string in, HTML out”. Each token carries .type, .tag, .nesting, .attrs and .map (source line numbers), so you can implement syntax highlighting by line range, build a table of contents from heading tokens, or rewrite links before rendering:
| |
Security posture is explicit: html=False (default in the commonmark preset) drops raw HTML instead of escaping it, and the project documents its threat model.
Where it wins: correctness, plugin breadth, and a token API that supports real transformations. Where it hurts: the slowest-installed dependency chain of the four (mdurl, plus mdit-py-plugins and linkify-it-py if you want the full set), and the config surface is large enough to be confusing — MarkdownIt("commonmark"), "js-default" and "zero" presets behave differently in ways that matter.
Python-Markdown — The Legacy Workhorse
Python-Markdown is what most Django and Flask tutorials used, so it is embedded in an enormous amount of production code. The API has not changed in a decade:
| |
Extensions are classes registered via entry points, and the ecosystem (pymdown-extensions) is the reason MkDocs Material works at all — admonitions, tabbed blocks, superfences, critic markup and arithmetic all come from there.
Where it wins: stability, the largest extension ecosystem in absolute terms, and zero-surprise upgrades. Where it hurts: it is not CommonMark, so output differs from GitHub for nested lists and intraword emphasis; it was also the slowest non-mistletoe option in my benchmark. If you are starting fresh in 2026, there is no reason to choose it over mistune or markdown-it-py unless you need a pymdown-extensions-specific feature.
mistletoe — The Only One With a Real Renderer Abstraction
mistletoe parses to an AST and then hands that AST to a renderer class. That inversion is its whole value proposition:
| |
Swapping output formats is a one-line change:
| |
The contrib package ships renderers for LaTeX, MathJax HTML, Pygments-highlighted HTML, GitHub wiki links, Jira markup and XWiki syntax. If you are converting a corpus between documentation systems, that is a substantial head start.
Where it wins: output-format flexibility and a genuinely clean AST. Where it hurts: speed (last in my benchmark), a smaller community than the other three, and the docs are thin — expect to read source for custom tokens.
Pitfalls: The Bugs That Ship to Production
1. Raw HTML is the security boundary, not a formatting detail. mistune.create_markdown() escapes by default; mistune.html() (the legacy helper) does not. markdown-it-py with html=True, Python-Markdown (which passes HTML through), and mistletoe’s HTML renderer all allow raw HTML. If any user input reaches the parser, run the output through a sanitizer such as bleach or nh3 — never rely on the parser.
2. Tables are an extension in three of the four libraries. Forgetting .enable("table") in markdown-it-py or plugins=["table"] in mistune silently renders pipe tables as paragraphs. Nothing errors. If your tests only assert “output contains the row text”, they will pass while the table markup is wrong. Assert on <table presence.
3. Footnotes and front matter need plugin order. In markdown-it-py, front_matter_plugin must be registered before other block rules run, or the YAML header is parsed as a thematic break plus a heading. This produces a stray <hr> at the top of thousands of pages and is easy to miss in review.
4. Python-Markdown’s codehilite requires Pygments at render time. If you enable codehilite and Pygments is missing in the production image, you get a silent fallback to plain <pre> blocks — highlighting disappears but nothing fails. Pin Pygments explicitly in your runtime dependencies, not just dev.
5. Caching is worth more than any parser choice. In my measurements the spread between the fastest and slowest library was 23 ms on a 129 KiB document. A single Redis lookup or an on-disk HTML cache eliminates all 23 ms and the parse entirely. Cache keyed on a content hash, and invalidate on parser version change — a parser upgrade changes output more often than people expect.
6. Rendered output is not stable across versions. Minor releases of all four have changed whitespace, attribute ordering and entity encoding. If you snapshot rendered HTML in tests, pin the parser version in requirements.txt and treat upgrades as intentional diffs.
For the JavaScript and Rust side of the same decision, see our marked vs markdown-it vs remark comparison and the WYSIWYG Markdown editor component roundup.
FAQ
Which Python Markdown library is fastest in 2026? Mistune 3.4.0 was fastest in my measurement: 46.2 ms for a 129 KiB document with tables and strikethrough enabled, versus 58.1 ms for markdown-it-py 4.2.0, 68.5 ms for Python-Markdown 3.11.2 and 69.6 ms for mistletoe 1.6.0. Mistune’s default configuration is faster still (35.3 ms) because it skips extension parsing and escapes raw HTML.
Is markdown-it-py fully CommonMark compliant? Yes. markdown-it-py passes the CommonMark specification suite and follows the same rule structure as the JavaScript markdown-it library. Python-Markdown implements the older Gruber reference syntax with extensions, and diverges from CommonMark on nested lists and intraword underscores.
Can I use these libraries to render untrusted user input?
Only with explicit sanitization. mistune escapes raw HTML by default, which is the safest starting point. markdown-it-py with html=False drops raw HTML entirely. Python-Markdown and mistletoe pass raw HTML through, so their output must be sanitized with a library such as bleach or nh3 before it reaches a browser.
Why do my Markdown tables render as plain paragraphs?
Because tables are an extension, not part of the base syntax, in mistune, markdown-it-py and Python-Markdown. Enable plugins=["table"] in mistune, call .enable("table") in markdown-it-py, or add extensions=["tables"] in Python-Markdown. mistletoe supports tables natively.
Should I render Markdown at request time in a web application? No. Even the fastest option costs roughly 45 ms per 129 KiB document, which is 45 ms of latency added to every request. Pre-render at build time, or cache the HTML keyed by a content hash, and re-render only when the source or the parser version changes.
Which library should I pick for generating LaTeX or PDFs? mistletoe, because its renderer is a separate class from its parser. It ships with LaTeX, AST, Markdown and several wiki-syntax renderers in the core package and contrib directory, so converting a corpus to another output format does not require rewriting the parse layer.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com