Kida supports native async/await syntax for async template rendering.
Async For
Iterate over async iterables:
{% async for user in fetch_users() %}
<li>{{ user.name }}</li>
{% end %}
The template must be rendered with an async method — either render_stream_async() for streaming or render_async()for buffered output:
import asyncio
from kida import Environment
async def main():
env = Environment()
template = env.from_string("""
{% async for item in items %}
{{ item }}
{% end %}
""")
async def items():
for i in range(3):
yield i
# Streaming (preferred for large output)
async for chunk in template.render_stream_async(items=items()):
print(chunk, end="")
# Buffered
result = await template.render_async(items=items())
print(result)
asyncio.run(main())
Await Expressions
Await async functions in expressions:
{{ await fetch_data(user_id) }}
Async Loop Variables
Inside{% async for %}, the loopvariable provides index-forward properties. Properties that require knowing total size are not available (async iterables have no known length):
| Property | Available | Description |
|---|---|---|
loop.index |
Yes | 1-based index |
loop.index0 |
Yes | 0-based index |
loop.first |
Yes | True on first iteration |
loop.previtem |
Yes | Previous item |
loop.cycle(...) |
Yes | Cycle through values |
loop.last |
No | Raises error |
loop.length |
No | Raises error |
loop.revindex |
No | Raises error |
{% async for user in fetch_users() %}
{{ loop.index }}: {{ user.name }}
<tr class="{{ loop.cycle('odd', 'even') }}">
{% end %}
Inline Filtering
Filter items as they arrive with inlineif:
{% async for user in fetch_users() if user.active %}
{{ user.name }}
{% end %}
Empty Clause
Render fallback content when the async iterable yields nothing:
{% async for notification in get_notifications() %}
{{ notification.message }}
{% empty %}
<p>No notifications.</p>
{% end %}
Async Streaming
render_stream_async()is the primary way to render async templates. It returns an async generator that yields chunks as they are produced — ideal for HTTP streaming responses:
async def render_page():
template = env.get_template("page.html")
async for chunk in template.render_stream_async(items=async_data()):
yield chunk # send to client immediately
You can also stream individual blocks:
async for chunk in template.render_block_stream_async("content", items=data):
yield chunk
Detecting Async Templates
Checktemplate.is_asyncto determine whether a template uses async constructs:
template = env.get_template("page.html")
if template.is_async:
async for chunk in template.render_stream_async(**ctx):
send(chunk)
else:
html = template.render(**ctx)
Important: Calling
render()orrender_stream()on an async template raisesTemplateRuntimeError. Always use the async methods for async templates.
Sync vs Async Rendering
| Method | Use Case |
|---|---|
render() |
Sync code, no async operations |
render_stream() |
Sync streaming |
render_async() |
Async code, buffered output |
render_stream_async() |
Async streaming (preferred for async templates) |
render_block_stream_async() |
Async streaming of a single block |
# Sync rendering (blocks)
html = template.render(name="World")
# Async streaming (non-blocking, chunked)
async for chunk in template.render_stream_async(items=async_generator()):
send_to_client(chunk)
# Async buffered (non-blocking, full string)
html = await template.render_async(items=async_generator())
Async Patterns
Parallel Fetching
Fetch data concurrently before rendering:
import asyncio
async def render_dashboard():
# Parallel fetching
user, posts, stats = await asyncio.gather(
fetch_user(),
fetch_posts(),
fetch_stats(),
)
template = env.get_template("dashboard.html")
return await template.render_async(
user=user,
posts=posts,
stats=stats,
)
Streaming Iteration
Process large async iterables without buffering — userender_stream_async()to stream output directly to the client:
template = env.from_string("""
{% async for record in database_cursor() %}
{{ record.name }}
{% end %}
""")
async for chunk in template.render_stream_async(
database_cursor=get_cursor,
):
response.write(chunk)
Free-Threading
Kida is designed for Python 3.14t free-threading (PEP 703). Combined with async, you can achieve high concurrency:
from concurrent.futures import ThreadPoolExecutor
import asyncio
async def render_many(templates):
"""Render multiple templates concurrently."""
return await asyncio.gather(*[
t.render_async(data=data)
for t, data in templates
])
Error Handling
Async errors propagate normally:
async def main():
try:
result = await template.render_async(items=failing_generator())
except TemplateError as e:
print(f"Render failed: {e}")
Best Practices
Use Async Sparingly
Not everything needs async:
{# ✅ Async for I/O-bound operations #}
{% async for user in fetch_users_from_api() %}
{# ❌ Sync iteration is fine for in-memory data #}
{% for item in items %}
Pre-Fetch When Possible
# ✅ Better: Parallel fetch, then sync render
users = await fetch_users()
posts = await fetch_posts()
html = template.render(users=users, posts=posts)
# Slower: Sequential async in template
# {% async for user in fetch_users() %}
See Also
- Thread Safety — Free-threading support
- Performance — Performance optimization
- API Reference — Template methods, AsyncLoopContext