Kida can statically analyze compiled templates to extract dependency information, determine output purity, and recommend caching strategies — all without rendering. No other Python template engine provides this capability.
What Analysis Provides
Every compiled template exposes these analysis results:
| Capability | Method | Returns |
|---|---|---|
| Context dependencies | template.required_context() |
Top-level variable names the template accesses |
| Full dependency paths | template.depends_on() |
Dotted paths likepage.title, site.pages |
| Block metadata | template.block_metadata() |
Per-block purity, dependencies, cache scope |
| Full metadata | template.template_metadata() |
Complete analysis including inheritance info |
| Cache check | template.is_cacheable("nav") |
Whether a block can be safely cached |
| Context validation | template.validate_context(ctx) |
Missing variable names before rendering |
Quick Start
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")
Check What Variables a Template Needs
>>> template.required_context()
frozenset({'page', 'site'})
Validate Context Before Rendering
>>> template.validate_context({"page": page_obj})
['site'] # 'site' is missing
>>> template.validate_context({"page": page_obj, "site": site_obj})
[] # all required variables present
Inspect Block-Level Metadata
>>> meta = template.block_metadata()
>>> nav = meta["nav"]
>>> nav.depends_on
frozenset({'site.menu'})
>>> nav.is_pure
'pure'
>>> nav.cache_scope
'site'
>>> nav.is_cacheable()
True
Determine Caching Strategy
>>> template.is_cacheable("nav") # site-wide cache
True
>>> template.is_cacheable("content") # page-specific, still cacheable
True
>>> template.is_cacheable() # all blocks cacheable?
True
Analysis Concepts
Dependencies
The dependency walker extracts every context variable path a template accesses.
Results are conservative: they may include paths not taken at runtime (e.g.,
both branches of an{% if %}) but never miss a path that is used.
>>> template.depends_on()
frozenset({'page.title', 'page.content', 'site.menu'})
required_context()extracts just the top-level names:
>>> template.required_context()
frozenset({'page', 'site'})
Purity
A block is pure if its output is deterministic given the same inputs.
Pure blocks can be cached safely. Impure blocks (usingrandom, shuffle,
or similar) must be re-rendered each time.
| Purity | Meaning | Cacheable? |
|---|---|---|
"pure" |
Deterministic output | Yes |
"impure" |
Uses non-deterministic functions | No |
"unknown" |
Cannot determine statically | Treat as impure |
Cache Scope
Cache scope tells you how broadly a block's output can be reused:
| Scope | Meaning | Example |
|---|---|---|
"site" |
Same output for every page | Navigation, footer |
"page" |
Varies per page but stable for a given page | Content, title |
"none" |
Cannot cache (impure block) | Random quotes |
"unknown" |
Cannot determine | Mixed dependencies |
Landmarks
Kida detects HTML5 landmark elements (<nav>, <main>, <header>, <footer>,
<aside>) in block output and uses them to infer the block's role:
>>> meta["nav"].emits_landmarks
frozenset({'nav'})
>>> meta["nav"].inferred_role
'navigation'
Context Validation
validate_context()is designed for build systems, SSR frameworks, and testing
pipelines that need to catch missing variables before rendering:
template = env.get_template("email.html")
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 first call) and compares required top-level variable names against the provided context keys plus environment globals. It returns a sorted list of missing names, or an empty list if everything is present.
Dotted Context Contracts
Frameworks that know more than top-level keys can compare a route or handler contract against Kida's dotted dependency paths:
from kida.analysis import check_context_contract
issues = check_context_contract(
template,
provided={"page.title", "page.author.name"},
globals={"csrf_token"},
optional={"flash.message"},
)
for issue in issues:
print(issue.code, issue.path, issue.message)
provided, globals, and optionalcan be dotted-path iterables or nested
mappings. This checker is deliberately route-agnostic: frameworks supply the
contract shape, and Kida reports only whether template dependency paths are
covered. Enablecheck_extra=Truewhen a narrow contract should also warn about
provided paths the template does not read.
Literal Attribute Extraction
Framework adapters and CI reports can inspect literal HTML attributes without parsing Kida's AST themselves:
from kida.analysis import extract_literal_attributes
attrs = extract_literal_attributes(
template,
prefixes=("data-", "hx-"),
)
for attr in attrs:
print(attr.tag, attr.name, attr.value, attr.lineno)
This reports only attributes visible in static template text. Dynamic attributes
such as{{ attrs }}, xmlattr, or helper-generated markup are intentionally
not inferred. Kida provides the generic facts; frameworks decide whether a
literaldata-*, hx-*, id, or other attribute has route-specific meaning.
Escape Audit
Useaudit_escaping()to inventory where a template outputs escaped values,
where autoescape is disabled, and where filters intentionally produce trusted
markup:
from kida.analysis import audit_escaping
for finding in audit_escaping(template):
print(finding.code, finding.kind, finding.expression, finding.message)
Findings are static diagnostics only. They do not change rendering and do not
prove user input was sanitized.| safefindings include the optional
reason=text when present; missing reasons get a suggestion so code review and
CI reports can point at explicit trust boundaries.
Privacy Lint
Uselint_privacy()to catch likely private data exposure before templates or
report fixtures are published:
from kida.analysis import lint_privacy
for finding in lint_privacy(template):
print(finding.code, finding.kind, finding.path, finding.message)
The first version is intentionally narrow. It reports sensitive-looking context
paths, secret-like string literals without echoing their values,| safeon
sensitive-looking values, broad debug context output, and dynamic template names
that a framework policy cannot statically allowlist.
Call-Site Validation
Kida can validate{% def %}call sites at compile time, catching parameter errors
before any template is rendered:
from kida import Environment
env = Environment(validate_calls=True)
template = env.from_string("""
{% def button(text: str, url: str, style="primary") %}
<a href="{{ url }}" class="btn btn-{{ style }}">{{ text }}</a>
{% end %}
{{ button(text="Save", urll="/save") }}
""")
# UserWarning: Call to 'button' at <string>:6 — unknown params: urll; missing required: url
What It Checks
| Issue | Example |
|---|---|
| Unknown params | Callingbutton(labl="X") when param is label |
| Missing required | Callingbutton() when texthas no default |
*args / **kwargsrelaxation |
Definitions with*args or **kwargssuppress unknown-param warnings |
Programmatic API
For build systems and CI pipelines, use theBlockAnalyzerdirectly:
from kida import Environment, DictLoader
from kida.analysis import BlockAnalyzer
env = Environment(
loader=DictLoader({"page.html": src}),
preserve_ast=True,
)
template = env.get_template("page.html")
analyzer = BlockAnalyzer()
issues = analyzer.validate_calls(template._optimized_ast)
for issue in issues:
if not issue.is_valid:
print(f"{issue.def_name} at line {issue.lineno}: "
f"unknown={issue.unknown_params}, "
f"missing={issue.missing_required}")
CallValidation
| Field | Type | Description |
|---|---|---|
def_name |
str |
Name of the called{% def %} |
lineno |
int |
Line number of the call site |
col_offset |
int |
Column offset of the call site |
unknown_params |
tuple[str, ...] |
Keyword args not in the definition |
missing_required |
tuple[str, ...] |
Required params not provided |
duplicate_params |
tuple[str, ...] |
Params passed more than once; parser-produced ASTs reject these before validation |
| Property | Description |
|---|---|
is_valid |
Trueif no issues were found |
Configuration
UseAnalysisConfigto customize analysis behavior for your framework:
from kida import AnalysisConfig
from kida.analysis import BlockAnalyzer
config = AnalysisConfig(
# Variables indicating page-specific scope
page_prefixes=frozenset({"post.", "post", "article.", "article"}),
# Variables indicating site-wide scope
site_prefixes=frozenset({"settings.", "settings", "global."}),
# Additional functions your framework guarantees are pure
extra_pure_functions=frozenset({"asset_url", "t", "current_lang"}),
# Filters that produce non-deterministic output
extra_impure_filters=frozenset({"random_choice"}),
)
analyzer = BlockAnalyzer(config=config)
Kida ships with a default config (DEFAULT_CONFIG) that includes common SSG
pure functions likeasset_url, t, canonical_url, etc.
Case Study: Bengal Static Site Generator
Bengal uses Kida's analysis API to implement smart incremental builds:
- 1
Compile all templates
Enable AST preservation when compiling.
- 2
Analyze each template
Get block metadata for each compiled template.
- 3
Identify site-cacheable blocks
Find nav, footer, sidebar using
cache_scope == "site". - 4
Cache site-scoped blocks
Cache once per build, reuse across all pages.
- 5
Re-render page-scoped blocks only
Only when page content changes.
- 6
Track dependencies
Invalidate caches when upstream data changes.
This reduces full-site rebuild time by 40-60% for sites with shared navigation and footer blocks.
Case Study: Chirp Web Framework
Chirp uses Kida's introspection and block APIs for dynamic web apps:
- 1
Composition planning
Use
template_metadata()to discover blocks and inheritance before rendering. - 2
Block validation
Call
validate_block_exists(env, template, block)beforerender_block()to avoid KeyError. - 3
Fragment rendering
Use
render_block()for HTMX partial responses and Turbo Stream updates. - 4
Layout assembly
Use
render_with_blocks()to inject pre-rendered content into layout templates. - 5
Adapter pattern
KidaAdapterimplements Chirp'sTemplateAdapterinterface, wrapping all Kida APIs.
See Framework Integration for the full adapter pattern and API usage.
API Reference
Template Methods
| Method | Signature | Description |
|---|---|---|
required_context() |
() -> frozenset[str] |
Top-level variable names needed |
depends_on() |
() -> frozenset[str] |
All dotted dependency paths |
validate_context() |
(context: dict) -> list[str] |
Missing variable names |
check_context_contract() |
(template, provided, ...) -> list[ContextContractIssue] |
Dotted context contract diagnostics |
extract_literal_attributes() |
(template_or_ast, names=..., prefixes=...) -> list[LiteralAttribute] |
Literal HTML attributes with source locations |
audit_escaping() |
(template_or_ast, include_output_sites=True) -> list[EscapeAuditFinding] |
Static escape and trusted-markup findings |
lint_privacy() |
(template_or_ast) -> list[PrivacyFinding] |
Sensitive path, secret literal, and broad-output findings |
block_metadata() |
() -> dict[str, BlockMetadata] |
Per-block analysis results |
template_metadata() |
() -> TemplateMetadata | None |
Full template analysis |
is_cacheable() |
(block_name: str | None) -> bool |
Cache safety check |
list_blocks() |
() -> list[str] |
Block names in template |
BlockMetadata
| Field | Type | Description |
|---|---|---|
name |
str |
Block identifier |
depends_on |
frozenset[str] |
Context paths accessed |
is_pure |
"pure" | "impure" | "unknown" |
Determinism classification |
cache_scope |
"site" | "page" | "none" | "unknown" |
Recommended cache level |
emits_html |
bool |
Whether block produces output |
emits_landmarks |
frozenset[str] |
HTML5 landmarks detected |
inferred_role |
str |
Heuristic role classification |
is_region |
bool |
True if block is a{% region %} |
region_params |
tuple[str, ...] |
Parameter names (regions only) |
TemplateMetadata
| Field | Type | Description |
|---|---|---|
name |
str | None |
Template identifier |
extends |
str | None |
Parent template name |
blocks |
dict[str, BlockMetadata] |
All block metadata |
top_level_depends_on |
frozenset[str] |
Dependencies outside blocks |
| Method | Description |
|---|---|
all_dependencies() |
Union of all block and top-level dependencies |
get_block(name) |
Get metadata for a specific block |
regions() |
Return only region-typed blocks (for OOB discovery) |
cacheable_blocks() |
List of blocks whereis_cacheable()is True |
site_cacheable_blocks() |
List of blocks withcache_scope == "site" |
CallValidation
| Field | Type | Description |
|---|---|---|
def_name |
str |
Name of the called{% def %} |
lineno |
int |
Line number of the call site |
col_offset |
int |
Column offset of the call site |
unknown_params |
tuple[str, ...] |
Keyword args not in the definition |
missing_required |
tuple[str, ...] |
Required params not provided |
duplicate_params |
tuple[str, ...] |
Params passed more than once |
| Property | Type | Description |
|---|---|---|
is_valid |
bool |
Trueif no issues were found |
AnalysisConfig
| Field | Type | Default | Description |
|---|---|---|---|
page_prefixes |
frozenset[str] |
{"page.", "page", ...} |
Page-scope variable prefixes |
site_prefixes |
frozenset[str] |
{"site.", "site", ...} |
Site-scope variable prefixes |
extra_pure_functions |
frozenset[str] |
frozenset() |
Additional pure function names |
extra_impure_filters |
frozenset[str] |
frozenset() |
Additional impure filter names |
Integration with Frameworks
Frameworks like Chirp use Kida templates and run contract validation (chirp check) to verify that hx-post, hx-get, and action URLs in templates match registered routes. Dynamic URLs built with ~ or {{ var }}are correctly skipped — only literal URLs are validated.