The secure, typed Markdown parser for modern Python.
from patitas import Markdown
md = Markdown()
html = md("# Hello **World**")| Patitas | mistune | markdown-it-py | |
|---|---|---|---|
| ReDoS-proof | ✅ O(n) FSM lexer | ❌ Regex-based | ✅ Token-based |
| CommonMark | 0.31.2 ✅ | Partial | 0.31.2 ✅ |
| Free-threading | ✅ Python 3.14t safe | ✅ Works | ❌ Crashes |
| Typed AST | ✅ Frozen dataclasses | ❌ Dict[str, Any] |
❌ Token objects |
| Dependencies | Zero | Zero | Zero |
| Directives | ✅ MyST syntax | RST-style | Plugin required |
Patitas is the only CommonMark-compliant parser with typed AST that works safely under Python 3.14t free-threading.
pip install patitasRequires Python 3.14+
Optional extras:
pip install patitas[syntax] # Syntax highlighting via Rosettes
pip install patitas[all] # All optional features| Function | Description |
|---|---|
parse(source) |
Parse Markdown to typed AST |
render(doc) |
Render AST to HTML |
Markdown() |
All-in-one parser and renderer |
Patitas is immune to ReDoS attacks.
Traditional Markdown parsers use regex patterns vulnerable to catastrophic backtracking:
# Malicious input that can freeze regex-based parsers
evil = "a](" + "\\)" * 10000
# mistune: hangs for seconds/minutes
# Patitas: completes in milliseconds (O(n) guaranteed)Patitas uses a hand-written finite state machine lexer:
- Single character lookahead — No backtracking, ever
- Linear time guaranteed — Processing time scales with input length
- Safe for untrusted input — Use in web apps, APIs, user-facing tools
Learn more about Patitas security →
Python 3.14t (free-threading) — 652 CommonMark examples:
| Parser | Single Thread | 4 Threads | Thread-safe? |
|---|---|---|---|
| mistune | 11ms | 4ms | ✅ |
| Patitas | 17ms | 7ms | ✅ |
| markdown-it-py | 20ms | CRASH | ❌ |
# Run benchmarks yourself
PYTHONPATH=src python3.14t benchmarks/benchmark_vs_mistune.pyKey insights:
- mistune is faster — regex engines are highly optimized
- Patitas scales linearly — 2.5x speedup with 4 threads
- markdown-it-py crashes under free-threading (race condition in URL encoding)
Patitas prioritizes safety over raw speed: O(n) guaranteed parsing, typed AST, and full thread-safety.
| Feature | Description |
|---|---|
| CommonMark | Full 0.31.2 spec compliance (652 examples) |
| Typed AST | Immutable frozen dataclasses with slots |
| Plugins | Tables, footnotes, math, strikethrough, task lists |
| Directives | MyST-style blocks (admonition, dropdown, tabs) |
| Roles | Inline semantic markup |
| Thread-safe | Zero shared mutable state, free-threading ready |
Basic Parsing
from patitas import parse, render
# Parse to AST
doc = parse("# Hello **World**")
# Render to HTML
html = render(doc)
# <h1 id="hello-world">Hello <strong>World</strong></h1>Typed AST — IDE autocomplete, catch errors at dev time
from patitas import parse
from patitas.nodes import Heading, Paragraph, Strong
doc = parse("# Hello **World**")
heading = doc.children[0]
# Full type safety
assert isinstance(heading, Heading)
assert heading.level == 1
# IDE knows the types!
for child in heading.children:
if isinstance(child, Strong):
print(f"Bold text: {child.children}")All nodes are @dataclass(frozen=True, slots=True) — immutable and memory-efficient.
Directives — MyST-style blocks
:::{note}
This is a note admonition.
:::
:::{warning}
This is a warning.
:::
:::{dropdown} Click to expand
Hidden content here.
:::
:::{tab-set}
:::{tab-item} Python
Python code here.
:::
:::{tab-item} JavaScript
JavaScript code here.
:::
:::Custom Directives — Extend with your own
from patitas import Markdown, create_registry_with_defaults
# Define a custom directive
class AlertDirective:
names = ("alert",)
token_type = "alert"
def render(self, directive, renderer):
return f'<div class="alert">{directive.title}</div>'
# Extend defaults with your directive
builder = create_registry_with_defaults() # Has admonition, dropdown, tabs
builder.register(AlertDirective())
# Use it
md = Markdown(directive_registry=builder.build())
html = md(":::{alert} This is important!\n:::")Syntax Highlighting
With pip install patitas[syntax]:
from patitas import Markdown
md = Markdown(highlight=True)
html = md("""
```python
def hello():
print("Highlighted!")""")
Uses [Rosettes](https://github.com/lbliii/rosettes) for O(n) highlighting.
</details>
<details>
<summary><strong>Free-Threading</strong> — Python 3.14t</summary>
```python
from concurrent.futures import ThreadPoolExecutor
from patitas import parse
documents = ["# Doc " + str(i) for i in range(1000)]
with ThreadPoolExecutor() as executor:
# Safe to parse in parallel — no shared mutable state
results = list(executor.map(parse, documents))
Patitas is designed for Python 3.14t's free-threading mode (PEP 703).
# Before (mistune)
import mistune
md = mistune.create_markdown()
html = md(source)
# After (patitas) — same API!
from patitas import Markdown
md = Markdown()
html = md(source)Key differences:
- Patitas uses MyST directive syntax (
:::{note}) vs mistune's RST (.. note::) - Patitas AST is typed dataclasses vs mistune's
Dict[str, Any] - Patitas is ReDoS-proof; mistune uses regex
Patitas is part of the Bengal ecosystem — a zero-dependency Python stack:
ᓚᘏᗢ Bengal — Static site generator
)彡 Kida — Template engine
⌾⌾⌾ Rosettes — Syntax highlighter
ฅᨐฅ Patitas — Markdown parser ← You are here
Build complete documentation sites with pure Python. No C extensions. No Node.js.
git clone https://github.com/lbliii/patitas.git
cd patitas
uv sync --group dev
pytestRun benchmarks:
pip install mistune markdown-it-py
python benchmarks/benchmark_vs_mistune.pyMIT License — see LICENSE for details.