Kida 0.2.0

Streaming rendering, async streaming, four new loaders, static analysis API, and better error messages

Released: February 8, 2026

Streaming rendering for both sync and async, four new loaders for flexible template architectures, a public static analysis API, and significantly improved error messages.

Highlights

  • Streaming renderingrender_stream() and render_stream_async()yield chunks as they're produced
  • Async-firstAsyncLoopContext, render_block_stream_async(), Template.is_async, async_render_context()
  • Four new loadersChoiceLoader, PrefixLoader, PackageLoader, FunctionLoader
  • Static analysisvalidate_context()catches missing variables before render
  • Better errors — Variable name suggestions, source snippets, template name suggestions
  • 48% faster cold-start — Lazy analysis imports avoid eagerly loading AST definitions

Added

Streaming Rendering

template.render_stream(**ctx)yields template output as string chunks via Python generators:

from kida import Environment

env = Environment(loader=...)
template = env.get_template("report.html")

for chunk in template.render_stream(items=data):
    send_to_client(chunk)

The compiler generates both StringBuilder (render()) and generator (render_stream()) functions in a single compilation pass. Full support for template inheritance, includes, and all control flow. Buffering blocks ({% capture %}, {% spaceless %}, {% cache %}, {% filter %}) buffer internally and yield the processed result. Zero performance impact on the existing render()path.

Native Async Streaming

template.render_stream_async(**ctx)yields chunks via async generators:

async for chunk in template.render_stream_async(items=data):
    await response.write(chunk)

Supports {% async for %} over async iterables, {{ await expr }} for inline coroutine resolution, and {% empty %}fallback clauses. Async child templates can extend sync parents seamlessly.

RenderedTemplate

Lazy iterable wrapper aroundrender_stream():

from kida import RenderedTemplate

rendered = RenderedTemplate(template, {"items": data})
for chunk in rendered:
    send_to_client(chunk)

AsyncLoopContext

Loop variable (loop) for {% async for %}. Provides index-forward properties (index, index0, first, previtem, cycle()). Size-dependent properties (last, length, revindex) raise TemplateRuntimeErrorsince async iterables have no known length.

Four New Loaders

ChoiceLoader — Try multiple loaders in order (theme fallback):

from kida import ChoiceLoader, FileSystemLoader

loader = ChoiceLoader([
    FileSystemLoader("themes/custom/"),
    FileSystemLoader("themes/default/"),
])

PrefixLoader — Namespace templates by prefix (plugin architectures):

from kida import PrefixLoader, FileSystemLoader, DictLoader

loader = PrefixLoader({
    "app": FileSystemLoader("templates/app/"),
    "admin": FileSystemLoader("templates/admin/"),
    "shared": DictLoader({"header.html": "<header/>"}),
})
env.get_template("app/index.html")  # Delegates to app loader

PackageLoader — Load from installed Python packages via importlib.resources:

from kida import PackageLoader

loader = PackageLoader("my_framework", "templates")

FunctionLoader — Wrap any callable as a loader:

from kida import FunctionLoader

loader = FunctionLoader(lambda name: my_db.get_template(name))

Static Analysis API

Pre-render variable validation and template introspection:

from kida import Environment, AnalysisConfig

env = Environment(loader=...)
template = env.get_template("page.html")

# Check for missing variables before rendering
errors = template.validate_context({"title": "Hello"})
for error in errors:
    print(error)  # "Missing variable: 'items'"

AnalysisConfig, BlockMetadata, and TemplateMetadata are exported from kida(lazy-loaded to avoid cold-start penalty).

Compiler-Emitted Profiling

profiled_render()now automatically tracks blocks, filters, and macros without manual instrumentation:

from kida import profiled_render

with profiled_render() as metrics:
    html = template.render(page=page)

print(metrics.summary())
# Blocks with timing, filter call counts, macro call counts

Zero overhead when profiling is disabled.

Other Additions

  • Include scope propagation — Loop variables from{% for %} and block-scoped {% set %} are now visible inside {% include %}templates
  • Bytecode cache warningfrom_string() without name= emits a UserWarning when a bytecode_cacheis configured
  • *args/**kwargs in {% def %} — Template-defined functions accept variadic arguments
  • Template.is_async — Boolean property for detecting async templates; render()raises on async templates
  • async_render_context() — Async context manager matching the sync render_context()API
  • render_block_stream_async() — Render a single block as an async stream

Changed

Dict-Safe Attribute Resolution

_safe_getattr now tries subscript before getattrfor dict objects:

{{ section.items }}
{# Now resolves to section["items"] (user data), not dict.items method #}

This prevents dict method names (items, keys, values, get, pop, update) from shadowing user data keys. Non-dict objects retain the previous getattr-first behavior.

Better Error Messages

  • UndefinedError suggests similar variable names via fuzzy matching
  • TemplateSyntaxError includes source snippets with surrounding line context
  • DictLoader suggests similar template names on miss
  • BareRuntimeErrors include template name and line number

Performance

  • 48% faster cold-startAnalysisConfig, BlockMetadata, and TemplateMetadata are lazy-loaded via __getattr__, avoiding eager import of 974 lines of AST definitions
  • CI: mypy replaced with ty — All type checking now uses Astral's Rust-basedtychecker

Internal Improvements

  • Compiler mixin extractionCachingMixin, WithBlockMixin, and PatternMatchingMixinextracted from monolithic compiler modules
  • template.py split into template/ package — 1,277-line module split into core.py, helpers.py, introspection.py, loop_context.py, cached_blocks.py
  • Narrowed type annotations — Specific subclasses replace broadNode types in visitor methods; Anytypes tightened across compiler and template modules
  • Sorted__all__ — Public API exports alphabetically sorted

Upgrade Guide

  1. No breaking changes — All existingrender()calls continue to work unchanged
  2. Dict attribute resolution — If you relied on{{ my_dict.items }} calling dict.items(), use the items filter instead: {{ my_dict | items }}
  3. New async guard — Templates containing{% async for %} or {{ await }} will now raise TemplateRuntimeError if called with sync render(). Use render_async() or render_stream_async().