Skip to content

lbliii/patitas

Repository files navigation

ฅᨐฅ Patitas

PyPI version Build Status Python 3.14+ License: MIT CommonMark ReDoS Safe

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

Full migration guide →


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.

About

ฅᨐฅ Patitas — CommonMark Markdown parser for Python 3.14+ with typed AST and free-threading

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published