This post is about a capability most template engines do not expose at all: asking a template what it needs before you render it.
One call. No HTML produced. Just a list of variables the template needs before runtime.
template.required_context() # frozenset({'page', 'site'})
Jinja2, Django templates, and Mako usually tell you about missing variables when UndefinedError happens at render time. Kida can tell you earlier, at compile time. That is not a feature bolted on later. It falls out of compiling to Python AST instead of source strings.
The capability
Every compiled Kida template exposes analysis results without a render() call:
from kida import Environment, DictLoader
env = Environment(loader=DictLoader({
"page.html": """
{% extends "base.html" %}
{% block title %}{{ page.title }}{% end %}
{% block nav %}
<nav>{% for item in site.menu %}<a href="{{ item.url }}">{{ item.label }}</a>{% end %}</nav>
{% end %}
{% block content %}{{ page.content }}{% end %}
""",
"base.html": """
<html><head><title>{% block title %}{% end %}</title></head>
<body>{% block nav %}{% end %}{% block content %}{% end %}</body></html>
""",
}))
template = env.get_template("page.html")
template.required_context() # frozenset({'page', 'site'})
template.validate_context({"page": page_obj}) # ['site'] ← catch missing before runtime
template.is_cacheable("nav") # True
Block metadata
>>> meta = template.block_metadata()
>>> nav = meta["nav"]
>>> nav.depends_on
frozenset({'site.menu'})
>>> nav.cache_scope
'site'
>>> nav.is_cacheable()
True
The nav block depends only on site.menu, so it produces the same output for every page. That tells Bengal it is safe to cache at site scope, once per build, for every page.
How it works
Kida compiles templates to Python AST directly, not source strings like Jinja2. That means the compiler has a structured representation it can analyze instead of opaque generated code.
The analysis pass walks that AST, extracts variable access, infers purity from filters and functions, and classifies cache scope from dependency patterns.
Jinja2 generates Python source strings and compiles those. Generated code is much harder to analyze after the fact. In Kida, the AST is the primary artifact, so analysis becomes a natural byproduct of compilation.
What it enables
Bengal — incremental builds (40-60% faster)
This is where the feature turns from "interesting compiler trick" into "useful application capability."
Bengal uses block_metadata() to find site-cacheable blocks. Cache those once per build, then re-render only page-scoped blocks when content changes:
if template.is_cacheable("nav"):
html = cache.get_or_render("nav", lambda: template.render_block("nav", site=site))
else:
html = template.render_block("nav", site=site)
That is where the 40-60% rebuild reduction for shared layouts comes from. Without the analysis, you either re-render everything or guess.
Chirp — safe fragment rendering
Chirp returns Fragment for HTMX partials and calls render_block() under the hood. Before rendering, Chirp calls validate_block_exists() to avoid KeyError. Before layout assembly, it uses template_metadata() to discover blocks and inheritance chains.
Pre-render validation in CI
You can also use the same analysis for validation before you ever serve a request:
missing = template.validate_context(user_context)
if missing:
raise ValueError(f"Missing template variables: {missing}")
result = template.render(**user_context)
This runs dependency analysis, cached after the first call, and compares required variables against the context. No HTML produced, just a pass/fail check.
Why this matters
If you are building an SSG, a web framework, or any system that composes templates programmatically, this changes what is possible. Incremental builds. Safe fragment rendering. Pre-flight validation. These are not add-ons. They are direct consequences of the AST-native architecture.
Jinja2, Django templates, and Mako do not expose this kind of introspection. They are primarily render-time tools. Kida can tell you what a template needs and how it behaves before you render it.
Static analysis replaces runtime discovery with compile-time knowledge. The template knows what it needs. The analysis exposes it. Asking is cheap.
Further reading
- Kida — A Template Engine Built for Free-Threaded Python — the threading model underneath the analysis: immutable AST, copy-on-write config, ContextVar isolation
- Kida documentation — Static Analysis — full API reference for
required_context(),block_metadata(),validate_context() - The Vertical Stack Thesis — why Chirp and Bengal can use these APIs: they're designed to use each other