This guide helps you migrate from mistune to Patitas.
The basic API is nearly identical:
# Before (mistune)
import mistune
md = mistune.create_markdown()
html = md(source)
# After (patitas)
from patitas import Markdown
md = Markdown()
html = md(source)| mistune | Patitas | Notes |
|---|---|---|
mistune.create_markdown() |
Markdown() |
Same pattern |
md(source) |
md(source) |
Identical |
mistune.html(source) |
parse() + render() |
Separate functions |
Like mistune, Patitas keeps GFM-style syntax opt-in — enable it via the
plugins=[...] argument. Plain Markdown() is pure CommonMark. Use
Markdown(plugins=["all"]) to enable every built-in plugin at once.
Available plugin names: table, strikethrough, task_lists, footnotes,
math, autolinks.
# mistune
md = mistune.create_markdown(plugins=['table'])
# Patitas — enable explicitly
from patitas import Markdown
md = Markdown(plugins=["table"])# mistune
md = mistune.create_markdown(plugins=['strikethrough'])
# Patitas
md = Markdown(plugins=["strikethrough"]) # ~~strikethrough~~ works# mistune
md = mistune.create_markdown(plugins=['footnotes'])
# Patitas
md = Markdown(plugins=["footnotes"]) # [^1] footnotes workThis is the biggest difference. mistune uses RST-style syntax, Patitas uses MyST:
# mistune (RST-style)
.. note::
This is a note.
# Patitas (MyST-style)
:::{note}
This is a note.
:::| mistune | Patitas |
|---|---|
.. note:: |
:::{note} |
.. warning:: |
:::{warning} |
.. admonition:: Title |
:::{admonition} Title |
# mistune
from mistune.directives import RSTDirective
class MyDirective:
def parse(self, block, m, state):
...
def __call__(self, md):
md.block.register_rule('mydirective', ...)
md = mistune.create_markdown(plugins=[RSTDirective([MyDirective()])])
# Patitas
from patitas import Markdown, create_registry_with_defaults
class MyDirective:
names = ("mydirective",)
token_type = "mydirective"
def render(self, directive, renderer):
return f'<div class="mydirective">{directive.title}</div>'
builder = create_registry_with_defaults()
builder.register(MyDirective())
md = Markdown(directive_registry=builder.build())# mistune — Dict[str, Any]
tokens = mistune.create_markdown(renderer=None)(source)
# tokens is a list of dicts
# Patitas — Typed dataclasses
from patitas import parse
from patitas.nodes import Heading, Paragraph
doc = parse(source)
# doc.children is tuple of typed nodes
for node in doc.children:
if isinstance(node, Heading):
print(f"Heading level {node.level}")Patitas advantages:
- IDE autocomplete works
- Type errors caught at development time
- Nodes are immutable (safe to share)
# mistune — subclass BaseRenderer
class MyRenderer(mistune.BaseRenderer):
def heading(self, text, level):
return f'<h{level} class="custom">{text}</h{level}>'
md = mistune.create_markdown(renderer=MyRenderer())
# Patitas — pass custom registry with render methods
# Or subclass HtmlRenderer
from patitas.renderers.html import HtmlRenderer
class MyRenderer(HtmlRenderer):
def _render_heading(self, heading, sb):
sb.append(f'<h{heading.level} class="custom">')
self._render_inlines(heading.children, sb)
sb.append(f'</h{heading.level}>\n')Patitas prioritizes ReDoS safety, typed immutable ASTs, and free-threading over raw single-thread speed. Benchmark your own corpus before treating performance as a migration reason:
# Run benchmark
uv pip install mistune markdown-it-py
python benchmarks/benchmark_vs_mistune.py| Reason | Details |
|---|---|
| Security | Patitas is ReDoS-proof; mistune uses regex |
| Performance | Predictable O(n) behavior, incremental parsing, and free-threading-friendly structure |
| Type Safety | Typed AST vs Dict[str, Any] |
| Free-threading | Native Python 3.14t support |
| MyST Syntax | Modern directive syntax, Jupyter Book compatible |
MyST syntax requires ::: (three colons), not RST's ..:
# Wrong (RST syntax)
.. note::
Content
# Correct (MyST syntax)
:::{note}
Content
:::Like mistune with escape=False (and markdown-it-py with html: true), the
default Patitas renderer is CommonMark-compliant and does not sanitize: raw
HTML and javascript:/data: URLs are emitted verbatim. To strip unsafe content
from untrusted input, sanitize the AST before rendering:
from patitas import parse, sanitize, render
from patitas.sanitize import web_safe
doc = parse(untrusted_source)
html = render(sanitize(doc, policy=web_safe)) # HTML + unsafe URLs removedSee Security for the full threat model.
Both parsers support table syntax, but rendering may differ slightly. Patitas tracks CommonMark compliance separately from GFM-style plugin behavior; see GFM compliance tracking before relying on an official GFM pass count.