The secure, typed Markdown parser for modern Python.
from patitas import Markdown
md = Markdown()
html = md("# Hello **World**")Patitas is a pure-Python Markdown parser that parses to a typed AST and renders to HTML. It's CommonMark 0.31.2 compliant, has zero runtime dependencies, and is built for Python 3.14+.
| Function | Description |
|---|---|
parse(source) |
Parse Markdown to typed AST |
parse_notebook(content, source_path?) |
Parse Jupyter .ipynb to (markdown, metadata) |
parse_incremental(new, prev, ...) |
Re-parse only the changed region (O(change)) |
render(doc) |
Render AST to HTML |
Markdown() |
All-in-one parser and renderer |
- ReDoS-proof — O(n) finite state machine lexer, no regex backtracking. Safe for untrusted input in web apps and APIs.
- Typed AST — Frozen dataclasses (
Heading,Paragraph,Strong, etc.) with IDE autocomplete and type checking. - CommonMark — Full 0.31.2 spec compliance (652 examples).
- Incremental parsing — Re-parse only changed blocks; ~200x faster for small edits than full re-parse.
- Thread-safe — Zero shared mutable state. Parse in parallel under Python 3.14t free-threading.
- Directives — MyST-style blocks (admonition, dropdown, tabs) plus custom directives.
- Plugins — Tables, footnotes, math, strikethrough, task lists.
- Zero dependencies — Pure Python, stdlib only.
pip install patitasRequires Python 3.14+
Optional extras:
pip install patitas[syntax] # Syntax highlighting via Rosettes
pip install patitas[all] # All optional featuresfrom patitas import parse, render
doc = parse("# Hello **World**")
html = render(doc)
# <h1 id="hello-world">Hello <strong>World</strong></h1>Parse Jupyter notebooks (.ipynb) to Markdown content and metadata — zero dependencies, stdlib JSON only:
from patitas import parse_notebook
with open("demo.ipynb") as f:
content, metadata = parse_notebook(f.read(), "demo.ipynb")
# content: Markdown string (cells → fenced code, outputs → HTML)
# metadata: title, type, notebook{kernel_name, cell_count}, etc.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
# 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 →
- 652 CommonMark examples — ~26ms single-threaded
- Incremental parsing — For a 1-char edit in a ~100KB doc,
parse_incrementalis ~200x faster than full re-parse (~160µs vs ~32ms) - Parallel scaling — ~2.5x speedup with 4 threads under Python 3.14t free-threading
# From repo (after uv sync --group dev):
python benchmarks/benchmark_vs_mistune.py
pytest benchmarks/benchmark_vs_mistune.py benchmarks/benchmark_incremental.py -v --benchmark-onlyTyped 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
from patitas.directives.decorator import directive
# Define a custom directive with the @directive decorator
@directive("alert")
def render_alert(node, children: str, sb) -> None:
sb.append(f'<div class="alert">{children}</div>')
# Extend defaults with your directive
builder = create_registry_with_defaults() # Has admonition, dropdown, tabs
builder.register(render_alert())
# 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).
Same API — swap the import:
from patitas import Markdown
md = Markdown()
html = md(source)A structured reactive stack — every layer written in pure Python for 3.14t free-threading.
| ᓚᘏᗢ | Bengal | Static site generator | Docs |
| ∿∿ | Purr | Content runtime | — |
| ⌁⌁ | Chirp | Web framework | Docs |
| =^..^= | Pounce | ASGI server | Docs |
| )彡 | Kida | Template engine | Docs |
| ฅᨐฅ | Patitas | Markdown parser ← You are here | Docs |
| ⌾⌾⌾ | Rosettes | Syntax highlighter | Docs |
Python-native. Free-threading ready. No npm required.
git clone https://github.com/lbliii/patitas.git
cd patitas
uv sync --group dev
pytestRun benchmarks (after uv sync --group dev):
python benchmarks/benchmark_vs_mistune.pyMIT License — see LICENSE for details.