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 rendering —
render_stream()andrender_stream_async()yield chunks as they're produced - Async-first —
AsyncLoopContext,render_block_stream_async(),Template.is_async,async_render_context() - Four new loaders —
ChoiceLoader,PrefixLoader,PackageLoader,FunctionLoader - Static analysis —
validate_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 warning —
from_string()withoutname=emits aUserWarningwhen abytecode_cacheis configured *args/**kwargsin{% def %}— Template-defined functions accept variadic argumentsTemplate.is_async— Boolean property for detecting async templates;render()raises on async templatesasync_render_context()— Async context manager matching the syncrender_context()APIrender_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
UndefinedErrorsuggests similar variable names via fuzzy matchingTemplateSyntaxErrorincludes source snippets with surrounding line contextDictLoadersuggests similar template names on miss- Bare
RuntimeErrors include template name and line number
Performance
- 48% faster cold-start —
AnalysisConfig,BlockMetadata, andTemplateMetadataare 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-based
tychecker
Internal Improvements
- Compiler mixin extraction —
CachingMixin,WithBlockMixin, andPatternMatchingMixinextracted from monolithic compiler modules template.pysplit intotemplate/package — 1,277-line module split intocore.py,helpers.py,introspection.py,loop_context.py,cached_blocks.py- Narrowed type annotations — Specific subclasses replace broad
Nodetypes in visitor methods;Anytypes tightened across compiler and template modules - Sorted
__all__— Public API exports alphabetically sorted
Upgrade Guide
- No breaking changes — All existing
render()calls continue to work unchanged - Dict attribute resolution — If you relied on
{{ my_dict.items }}callingdict.items(), use theitemsfilter instead:{{ my_dict | items }} - New async guard — Templates containing
{% async for %}or{{ await }}will now raiseTemplateRuntimeErrorif called with syncrender(). Userender_async()orrender_stream_async().