Patitas is designed for Python 3.14t free-threading.
Design Principles
1. Immutable AST
All AST nodes are frozen dataclasses:
@dataclass(frozen=True, slots=True)
class Heading:
level: int
children: tuple[Inline, ...]
Frozen = no mutation = no race conditions.
2. No Shared State
Parser and renderer instances don't share mutable state:
# Each call creates fresh instances
doc1 = parse(source1) # Thread 1
doc2 = parse(source2) # Thread 2 - no conflict
3. Thread-Local Where Needed
Global extension points use thread-local storage:
# Safe for concurrent use
set_highlighter(my_highlighter)
Concurrent Parsing
Parse multiple documents in parallel:
import concurrent.futures
from patitas import parse
documents = ["# Doc 1", "# Doc 2", "# Doc 3"]
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(parse, documents))
Concurrent Rendering
Render multiple ASTs in parallel:
from patitas import render
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
futures = [
executor.submit(render, doc, source=src)
for doc, src in zip(docs, sources)
]
htmls = [f.result() for f in futures]
Best Practices
Do
- Create parser/renderer per thread or per call
- Use frozen AST nodes (default)
- Share source text (immutable strings are safe)
Don't
- Mutate AST nodes (they're frozen anyway)
- Share mutable registries across threads
- Use global mutable state
Python 3.14t
Patitas is built for PEP 703 (free-threading):
# pyproject.toml
[tool.maturin]
module-name = "patitas._core"
# Declares GIL-free compatibility
# _Py_mod_gil = 0 (if there were C extensions)
Run with GIL disabled:
PYTHON_GIL=0 python -c "from patitas import parse; parse('# Test')"