Kida is designed for concurrent rendering in free-threaded Python.
Free-Threading Support
Kida declares GIL-independence via PEP 703:
# In kida/__init__.py
def __getattr__(name):
if name == "_Py_mod_gil":
return 0 # Py_MOD_GIL_NOT_USED
This signals that Kida is safe for true parallel execution in Python 3.14t+.
Thread-Safe Design
Immutable Configuration
Environment configuration is frozen after construction:
env = Environment(
loader=FileSystemLoader("templates/"),
autoescape=True,
)
# Configuration is now immutable
Copy-on-Write Updates
Adding filters/tests creates new dictionaries:
def add_filter(self, name, func):
# Copy-on-write: no locking needed
new_filters = self._filters.copy()
new_filters[name] = func
self._filters = new_filters
Local Rendering State
Eachrender()call uses only local variables:
def render(self, **context):
_out = [] # Local buffer
# ... render logic uses only local state
return "".join(_out)
No shared mutable state between render calls.
Lock-Free Caching
LRU caches use atomic operations:
# Thread-safe cache access
cached = self._cache.get(name)
self._cache.set(name, template)
Concurrent Rendering
With ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor
from kida import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("templates/"))
template = env.get_template("page.html")
def render_page(context):
return template.render(**context)
contexts = [{"name": f"User {i}"} for i in range(100)]
# On Python 3.14t, this runs with true parallelism
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(render_page, contexts))
With asyncio
import asyncio
async def render_async(env, template_name, **context):
template = env.get_template(template_name)
return template.render(**context)
async def render_many():
tasks = [
render_async(env, "page.html", user=f"User {i}")
for i in range(100)
]
return await asyncio.gather(*tasks)
What's Safe
| Operation | Thread-Safe |
|---|---|
get_template() |
✅ Yes |
from_string() |
✅ Yes |
template.render() |
✅ Yes |
add_filter() |
✅ Yes (copy-on-write) |
add_test() |
✅ Yes (copy-on-write) |
add_global() |
✅ Yes (copy-on-write) |
clear_cache() |
✅ Yes |
Best Practices
Create Environment Once
# ✅ Create once, reuse everywhere
env = Environment(loader=FileSystemLoader("templates/"))
def handle_request(request):
template = env.get_template(request.path)
return template.render(**request.context)
Don't Mutate During Rendering
# ❌ Don't add filters during concurrent rendering
def render_with_filter(value):
env.add_filter("custom", custom_func) # Race condition!
return template.render(value=value)
# ✅ Add filters at startup
env.add_filter("custom", custom_func)
def render(value):
return template.render(value=value)
Use Template Caching
# Templates are compiled once, then cached
# Concurrent get_template() calls for the same name
# wait for the first compilation to complete
template = env.get_template("page.html")
Performance with Free-Threading
Kida Scaling (vs single-threaded baseline)
| Workers | Time | Speedup |
|---|---|---|
| 1 | 3.31ms | 1.0x |
| 2 | 2.09ms | 1.58x |
| 4 | 1.53ms | 2.16x |
| 8 | 2.06ms | 1.61x |
Kida vs Jinja2 (Concurrent)
| Workers | Kida | Jinja2 | Kida Advantage |
|---|---|---|---|
| 1 | 3.31ms | 3.49ms | 1.05x |
| 2 | 2.09ms | 2.51ms | 1.20x |
| 4 | 1.53ms | 2.05ms | 1.34x |
| 8 | 2.06ms | 3.74ms | 1.81x |
Key insight: Jinja2 shows negative scaling at 8 workers (slower than 4 workers), indicating internal contention. Kida's thread-safe design avoids this.
See Also
- Architecture — Rendering internals
- Performance — Optimization tips
- Async — Async template rendering