Modern Markdown parser for Python 3.14t — CommonMark compliant, free-threading ready, 40-50% faster than mistune
Project description
ฅᨐฅ Patitas
The secure, typed Markdown parser for modern Python.
from patitas import Markdown
md = Markdown()
html = md("# Hello **World**")
Why Patitas?
| 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.
Installation
pip install patitas
Requires Python 3.14+
Optional extras:
pip install patitas[syntax] # Syntax highlighting via Rosettes
pip install patitas[all] # All optional features
Quick Start
| Function | Description |
|---|---|
parse(source) |
Parse Markdown to typed AST |
render(doc) |
Render AST to HTML |
Markdown() |
All-in-one parser and renderer |
Security
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 →
Performance
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.py
Key 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.
Features
| 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 |
Usage
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).
Migrate from mistune
# 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
The Bengal Cat Family
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.
Development
git clone https://github.com/lbliii/patitas.git
cd patitas
uv sync --group dev
pytest
Run benchmarks:
pip install mistune markdown-it-py
python benchmarks/benchmark_vs_mistune.py
License
MIT License — see LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file patitas-0.1.0.tar.gz.
File metadata
- Download URL: patitas-0.1.0.tar.gz
- Upload date:
- Size: 150.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
02ba2989fc6b672233c580d673023481a3290e097a4cf9531caa2ed39a4969ad
|
|
| MD5 |
422445307a823d36a13f2347f8c1c308
|
|
| BLAKE2b-256 |
caaf79f9d9ff91e90681fe3918049cc5a06087ff3696198a8263d43306676133
|
File details
Details for the file patitas-0.1.0-py3-none-any.whl.
File metadata
- Download URL: patitas-0.1.0-py3-none-any.whl
- Upload date:
- Size: 183.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
24e43e96a22f2f34041f153d06d44da975ad44d840deb47567ae8f08fff91f74
|
|
| MD5 |
06c4afbe35e7e556059fc8f0fdb2760d
|
|
| BLAKE2b-256 |
a2811b39376b61978ec5a75d6c2bd13b17bfe1c5c72f883500f1d83cab783139
|