Async

Async iteration and await in templates

4 min read 776 words

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: Callingrender() or render_stream() on an async template raises TemplateRuntimeError. 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