Chirp + Kida
Build HTMX apps with Chirp and Kida templates.
Building dynamic web apps with Chirp? This track covers block rendering, regions, OOB updates, and framework integration—everything you need for HTMX partials and layout composition.
Tip
Duration: ~45 min | Prerequisite: Python 3.14+, basic Chirp or HTMX familiarity
Framework Integration
Block rendering, introspection, and composition APIs for Chirp, Bengal, and custom adapters
Framework Integration
Kida provides block-level rendering, introspection, and composition helpers for frameworks that need fragments (HTMX, Turbo), layout assembly, or template validation. Use these APIs when building adapters, validating routes, or composing layouts programmatically.
Overview
| API | Purpose |
|---|---|
render_block(name, **ctx) |
Render a single block — HTMX partials, cached nav |
render_with_blocks(overrides, **ctx) |
Compose layout with pre-rendered HTML in blocks |
list_blocks() |
Discover block names for validation |
template_metadata() |
Full analysis — blocks, extends, dependencies |
validate_block_exists() |
Check block exists beforerender_block |
get_structure() |
Lightweight manifest for composition planning |
Block Rendering
render_block
Render a single block from a template. Supports inherited blocks: when the template extends a parent, you can render parent-only blocks by name (e.g.render_block("sidebar") on a child that extends a base defining sidebar).
template = env.get_template("page.html")
# Render content block (HTMX partial response)
html = template.render_block("content", title="Hello", items=items)
# Render parent-only block from descendant
html = template.render_block("sidebar", site=site)
Raises KeyErrorif the block does not exist in the template or any parent.
render_with_blocks
Render a template with pre-rendered HTML injected into blocks. Enables programmatic layout composition without{% extends %}in the template source.
layout = env.get_template("_layout.html")
inner_html = "<h1>Hello</h1><p>Content here.</p>"
# Inject inner_html as the "content" block
html = layout.render_with_blocks({"content": inner_html}, title="Page Title")
Each key in block_overridesnames a block; the value is a pre-rendered HTML string that replaces that block's default content.
list_blocks
List all blocks available forrender_block(), including inherited blocks.
blocks = template.list_blocks()
# ['title', 'nav', 'content', 'footer']
Introspection
template_metadata
Get full template analysis including inheritance info, block metadata, and dependencies. ReturnsNone if AST was not preserved (preserve_ast=Falseor loaded from bytecode cache without source).
meta = template.template_metadata()
if meta:
print(meta.extends) # Parent template name
print(list(meta.blocks.keys())) # Block names
print(meta.all_dependencies()) # Context paths accessed
block_metadata
Get per-block analysis: purity, cache scope, inferred role.
blocks = template.block_metadata()
nav = blocks.get("nav")
if nav and nav.cache_scope == "site":
# Safe to cache nav across all pages
html = cache.get_or_render("nav", ...)
validate_context
Check a context dict for missing variables before rendering.
missing = template.validate_context(user_context)
if missing:
raise ValueError(f"Missing template variables: {missing}")
Composition Module
Thekida.compositionmodule provides validation helpers for frameworks:
from kida import Environment, FileSystemLoader
from kida.composition import validate_block_exists, get_structure
env = Environment(loader=FileSystemLoader("templates/"))
validate_block_exists
Check if a block exists before callingrender_block:
if validate_block_exists(env, "skills/page.html", "page_content"):
html = env.get_template("skills/page.html").render_block("page_content", ...)
else:
# Handle missing block
...
Returns Falseif the template is not found or the block is missing.
get_structure
Get a lightweight structure manifest (block names, extends parent, dependencies). Cached by Environment for reuse.
struct = get_structure(env, "page.html")
if struct and "page_root" in struct.block_names:
# Template has page_root block — suitable for layout composition
...
block_role_for_framework
Classify block metadata into framework-relevant roles ("fragment", "page_root", or None). Useful for frameworks that need to distinguish content blocks from layout roots.
from kida.composition import block_role_for_framework
meta = template.template_metadata()
for name, block in meta.blocks.items():
role = block_role_for_framework(block)
if role == "fragment":
# Suitable for HTMX partial
...
Adapter Pattern
A minimal template adapter wraps Kida's APIs:
from kida import Environment
from typing import Any
class KidaAdapter:
"""TemplateAdapter implementation using Kida's block/layout APIs."""
def __init__(self, env: Environment) -> None:
self._env = env
def render_template(self, template: str, context: dict[str, Any]) -> str:
return self._env.get_template(template).render(context)
def render_block(self, template: str, block: str, context: dict[str, Any]) -> str:
return self._env.get_template(template).render_block(block, context)
def compose_layout(
self,
template: str,
block_overrides: dict[str, str],
context: dict[str, Any],
) -> str:
return self._env.get_template(template).render_with_blocks(
block_overrides, **context
)
def template_metadata(self, template: str) -> object | None:
from kida.environment.exceptions import (
TemplateNotFoundError,
TemplateSyntaxError,
)
try:
return self._env.get_template(template).template_metadata()
except (TemplateNotFoundError, TemplateSyntaxError):
return None
Chirp uses this pattern in KidaAdapter.
Case Studies
Bengal (Static Site Generator)
- Full render —
render()for page output - Bytecode cache — Persistent
.bengal/cache/kida/for cold-start - Fragment cache —
{% cache %}with site-scoped TTL - Analysis —
block_metadata(),is_cacheable()for incremental builds
Chirp (Web Framework)
- Full render —
render()for full-page responses - Block render —
render_block()for HTMX fragments, partial updates - Layout composition —
render_with_blocks()for programmatic layout assembly - Streaming —
render_stream(),render_stream_async()for chunked HTTP - Introspection —
template_metadata()for composition planning,validate_block_exists()beforerender_block - Adapter —
KidaAdapterimplements Chirp'sTemplateAdapterinterface
AST-driven OOB discovery: Chirp usestemplate_metadata()to discover OOB regions
at build time — it never hard-codes which blocks to render.build_layout_contract()
callsmeta.regions() (or filters meta.blocks by *_oobsuffix), extracts
cache_scope and depends_on from each block's BlockMetadata, and builds a
LayoutContractcached per template. On boosted navigation, Chirp renders the
page fragment plus the OOB regions ashx-swap-oobupdates. See Chirp's Kida
Integration
for the full flow.
Chirp + Regions: Step-by-Step
-
Define regions in layout templates — Use
{% region %}for blocks that should update out-of-band (sidebar, breadcrumbs, title):{% region sidebar_oob(current_path="/") %} <nav>{{ current_path }}</nav> {% end %} -
Discover at build time — Chirp calls
meta = template.template_metadata()andmeta.regions()to get region blocks. Each hasregion_params,depends_on, andcache_scope. -
Render on navigation — For HTMX boosted requests, Chirp renders the main content block plus each OOB region via
render_block(), passing the region's params from the request context. -
No hard-coding — The layout contract is built from AST analysis. Add or remove regions in templates; Chirp adapts automatically.
See Regions for syntax and Chirp Kida Integration for the full Chirp flow.
See Also
- Inheritance —
render_blockwith{% extends %} - Regions — Parameterized blocks for OOB and fragments
- Static Analysis — Full introspection API
- render_block and Def Scope — Def scope in blocks
Functions
Define reusable template functions and macros
Functions
Define reusable template functions within your templates.
Defining Functions
Use{% def %}to create a function:
{% def greet(name) %}
<p>Hello, {{ name }}!</p>
{% end %}
{{ greet("Alice") }}
{{ greet("Bob") }}
Parameters
Required Parameters
{% def button(text, url) %}
<a href="{{ url }}" class="button">{{ text }}</a>
{% end %}
{{ button("Click Me", "/action") }}
Default Values
{% def button(text, url, style="primary") %}
<a href="{{ url }}" class="button button-{{ style }}">{{ text }}</a>
{% end %}
{{ button("Save", "/save") }}
{{ button("Cancel", "/cancel", style="secondary") }}
Keyword Arguments
{% def card(title, content="", footer=none) %}
<div class="card">
<h3>{{ title }}</h3>
{% if content %}
<p>{{ content }}</p>
{% end %}
{% if footer %}
<footer>{{ footer }}</footer>
{% end %}
</div>
{% end %}
{{ card(title="Hello", content="World") }}
{{ card("Title Only") }}
Typed Parameters
Parameters can carry optional type annotations following Python syntax:
{% def card(title: str, items: list, footer: str | None = none) %}
<h3>{{ title }}</h3>
{% for item in items %}<p>{{ item }}</p>{% end %}
{% if footer %}<footer>{{ footer }}</footer>{% end %}
{% end %}
Annotations are optional per-parameter — you can mix typed and untyped:
{% def mixed(name: str, options, count: int = 0) %}
...
{% end %}
Supported Syntax
| Syntax | Meaning |
|---|---|
x: str |
Simple type |
x: int |
Simple type |
x: list |
Generic without params |
x: dict[str, int] |
Generic with params |
x: str | None |
Union (PEP 604 style) |
x: MyModel |
Custom type name |
What Annotations Do
Annotations are documentation and validation hints, not enforced at runtime.
The template engine does not performisinstancechecks. Their value is:
- Compile-time call-site validation — wrong parameter names are caught immediately when
validate_calls=Trueis set on the Environment - IDE support — annotations flow into the generated Python code, enabling autocomplete in tooling
- Self-documenting — makes component interfaces explicit
Call-Site Validation
Enablevalidate_callson the Environment to catch parameter errors at compile time:
from kida import Environment
env = Environment(validate_calls=True)
# This emits a warning: 'titl' is not a param of 'card'
env.from_string("""
{% def card(title: str) %}{{ title }}{% end %}
{{ card(titl="oops") }}
""")
Validation checks:
- Unknown parameters — keyword args not in the definition
- Missing required parameters — params without defaults not provided
*args/**kwargsin the definition relax validation accordingly
See Static Analysis for the programmatic API.
Capturing Content (Default Slot)
Functions can capture call-block content usingcaller():
{% def wrapper(title) %}
<section>
<h2>{{ title }}</h2>
<div class="content">
{{ caller() }}
</div>
</section>
{% end %}
{% call wrapper("Section Title") %}
<p>This content is passed to the wrapper.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
{% end %}
caller()without arguments reads the default slot.
Named Slots
Kida also supports named slots for multi-region components:
{% def card(title) %}
<article>
<h2>{{ title }}</h2>
<div class="actions">{% slot header_actions %}</div>
<div class="body">{% slot %}</div>
</article>
{% end %}
{% call card("Settings") %}
{% slot header_actions %}<button>Save</button>{% end %}
<p>Body content.</p>
{% end %}
How it works:
{% slot %}in adefis the default placeholder.{% slot name %}in adefis a named placeholder.- Inside
{% call %}, use{% slot name %}...{% end %}to provide named slot content. caller("name")retrieves a named slot from inside adef.
Slot Forwarding with{% yield %}
When composing macros, you often need to forward the outer caller's slot content into a nested{% call %}. The {% slot %} tag has context-dependent meaning: inside {% call %} it defines content (a SlotBlock), not a render reference. Use {% yield %}when you want to render the enclosing def's caller slot regardless of block context:
{% def selection_bar() %}<bar>{{ caller() }}</bar>{% end %}
{% def resource_index() %}
{% call selection_bar() %}
{% yield selection %}
{% end %}
{% end %}
{% call resource_index() %}
{% slot selection %}Badges{% end %}
Cards
{% end %}
{% yield %}— render the caller's default slot (same as{% slot %}inside a def).{% yield name %}— render the caller's named slotname.
{% yield %} is self-closing (no {% end %}) and always produces a render reference, even inside {% call %}blocks. It resolves to the nearest enclosing def's caller, regardless of nesting depth. No caller means no output (silent no-op).
When to use: Prefer{% yield %} over the double-nesting workaround {% slot x %}{% slot x %}{% end %}when forwarding slots through nested calls.
Slot Context Inheritance
Slot content is rendered in the caller's context. Variables from the page or render context are available in slot content without| default():
{% def form(action, method="get") %}
<form action="{{ action }}" method="{{ method }}">
{% slot %}
</form>
{% end %}
{% block page_content %}
{% call form("/search") %}
{{ search_field("q", value=q) }}
{% if selected_tags %}
{{ hidden_field("tags", value=selected_tags | join(",")) }}
{% end %}
{% end %}
{% end %}
When render_block("page_content", q="...", selected_tags=["a","b"]) is called, q and selected_tags are available inside the form slot because the slot body inherits the caller's render context. This works for both render() and render_block().
Slot Detection
When a function is called via{% call %}, it receives slot content accessible through caller(). Use the built-in has_slot() helper inside a {% def %}body to detect whether any call slot content was provided:
{% def card(title) %}
<div class="card">
<h3>{{ title }}</h3>
{% if has_slot() %}
<div class="card-body">
{{ caller() }}
</div>
{% end %}
</div>
{% end %}
When called directly, has_slot() returns false:
{{ card("Simple Card") }}
{# Output: <div class="card"><h3>Simple Card</h3></div> #}
When called with {% call %}, has_slot() returns true:
{% call card("Rich Card") %}
<p>This content appears in the card body.</p>
<button>Action</button>
{% end %}
{# Output includes the card-body wrapper #}
This pattern is useful for components that should adapt their markup depending on whether slot content is provided, for example rendering a wrapper <div>only when there is something to wrap.
Islands Wrapper Pattern
Functions are a good fit for reusable island mount wrappers in server-rendered apps:
{% def island_shell(name, attrs="") %}
<section{{ attrs }}>
<div class="island-fallback">
{% slot %}
</div>
</section>
{% end %}
{% call island_shell("editor", attrs=island_attrs("editor", {"doc_id": doc.id}, "editor-root")) %}
<p>Server-rendered fallback editor UI.</p>
{% end %}
Guidelines:
- Keep fallback slot content usable without JavaScript.
- Pass serialized props via helper globals (avoid manual inline JSON).
- Prefer stable mount IDs for deterministic remount behavior.
- Use explicit wrapper signatures when creating state primitives:
{% def grid_state_shell(state_key, columns, attrs="") %}
<section{{ attrs }}>
{% slot %}
</section>
{% end %}
Regions
Regions are parameterized blocks that work as both blocks (forrender_block()) and callables (for {{ name(args) }}). Use them when you need parameterized fragments for HTMX partials, OOB updates, or layout composition.
Syntax
{% region name(param1, param2=default) %}
...body...
{% end %}
{{ name(value1, value2) }}
Block and Callable
A region compiles to both:
- A block — call
template.render_block("name", param1=..., param2=...) - A callable — use
{{ name(args) }}in the template body
{% region sidebar(current_path="/") %}
<nav>{{ current_path }}</nav>
{% end %}
{% block content %}
{{ sidebar(current_path="/about") }}
{% endblock %}
# From Python: render the region as a block
html = template.render_block("sidebar", current_path="/settings")
Outer Context
Region bodies can read variables from the outer render context (not just parameters):
{% region crumbs(current_path="/") %}
{{ breadcrumb_items | default([{"label":"Home","href":"/"}]) | length }}
{% end %}
{{ crumbs(current_path="/x") }}
When render_block("crumbs", ...) or {{ crumbs(...) }} is called, the region receives its params plus the caller's context. breadcrumb_itemscomes from the outer context.
Region default expressions
Optional parameters can use any expression as a default, not just simple variable names. Defaults are evaluated at call time from the caller's context:
{% region sidebar(section, meta=page.metadata) %}
<nav>{{ meta.title }}</nav>
{% end %}
{% region stats(count=items | length) %}
{{ count }} items
{% end %}
{% region header(title=page?.title ?? "Default") %}
<h1>{{ title }}</h1>
{% end %}
Supported expressions include:
- Simple names —
current_page=page(zero-overhead inline lookup) - Attribute access —
meta=page.metadata - Filters —
count=items | length - Optional chaining —
title=page?.title ?? "Default" - Null coalescing —
meta=data?.info ?? {}
Static analysis (depends_on) correctly captures context paths from complex defaults for incremental build and cache scope inference.
Regions vs Defs
| Use case | Region | Def |
|---|---|---|
render_block() |
✅ Yes — region is a block | ❌ No — def is not a block |
{{ name(args) }} |
✅ Yes | ✅ Yes |
Slots /{% call %} |
❌ No | ✅ Yes |
| Outer-context access | ✅ Yes | ✅ Yes (via caller context) |
| Framework OOB discovery | ✅meta.regions() |
❌ N/A |
Use regions when you need parameterized blocks forrender_block(), HTMX OOB, or framework layout composition. Use defs when you need slots, {% call %}, or component composition.
Framework Integration
Frameworks like Chirp usetemplate_metadata().regions() to discover OOB regions at build time. Each region's BlockMetadata includes is_region, region_params, and depends_onfor cache scope inference. See Framework Integration.
Macros
Kida also supports the{% macro %}syntax:
{% macro input(name, value="", type="text") %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{{ input("username") }}
{{ input("password", type="password") }}
Importing Functions
Import functions from other templates:
{% from "macros.html" import button, input %}
{{ button("Submit", "/submit") }}
{{ input("email", type="email") }}
Import all with a namespace:
{% import "forms.html" as forms %}
{{ forms.input("name") }}
{{ forms.textarea("bio") }}
Best Practices
Single Responsibility
Each function should do one thing:
{# Good: Single purpose #}
{% def user_avatar(user, size=32) %}
<img src="{{ user.avatar_url }}"
alt="{{ user.name }}"
width="{{ size }}"
height="{{ size }}">
{% end %}
{# Avoid: Too much logic #}
{% def user_card_with_everything(user, show_bio, show_posts, ...) %}
...
{% end %}
Descriptive Names
{# Good: Clear purpose #}
{% def format_price(amount, currency="USD") %}
{% def user_badge(role) %}
{% def pagination_nav(current, total) %}
{# Avoid: Vague names #}
{% def render(x) %}
{% def do_thing(item) %}
Macro vs Context Variable Naming
When importing macros that render context variables, use different names so the macro does not shadow the variable. Prefer verb-prefixed names for macros and noun-like names for context variables:
{# Good: Macro and variable have different names #}
{% from "_route_tabs.html" import render_route_tabs %}
{% if route_tabs | default([]) %}
{{ render_route_tabs(route_tabs, current_path) }}
{% end %}
{# Avoid: Same name causes shadowing — route_tabs may resolve to the macro #}
{% from "_route_tabs.html" import route_tabs %}
{% if route_tabs | default([]) %} {# When route_tabs not in context, this is the macro (truthy) #}
{{ route_tabs(route_tabs, current_path) }} {# Passes macro as first arg → "not iterable" #}
{% end %}
| Use for | Naming | Examples |
|---|---|---|
| Macros | Verb-prefixed | render_route_tabs, format_date, render_nav |
| Context variables | Noun-like | route_tabs, items, skills |
If you seeCannot iterate over macro 'X', a macro is shadowing a context variable. Rename the macro (e.g. render_X) to avoid the collision.
See Also
- Inheritance — Extend base templates
- Includes — Include partials
- Custom Filters — Python-defined filters
Static Analysis
Analyze templates for dependencies, purity, and caching potential
Static Analysis
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.
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 |
| 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 |
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.
render_block and Def Scope
Blocks do not inherit defs from the same template
render_block and Def Scope
When you calltemplate.render_block("block_name", context), the block is rendered in isolation. It does not have access to {% def %}macros defined in the same template.
Inherited blocks:render_block() resolves blocks through the inheritance chain. You can render parent-only blocks (e.g. sidebarfrom a base template) on a descendant template. Child overrides still win. See Inheritance — render_block.
The Problem
{# page.html — def and block in same file #}
{% def helper() %}shared logic{% end %}
{% block content %}
{{ helper() }} {# NameError or UndefinedError when using render_block #}
{% end %}
If a framework (e.g. Chirp) calls template.render_block("content", ...), the block cannot see helperbecause blocks are compiled with their own scope.
Fix
Split defs into a separate file and import them:
{# _helpers.html #}
{% def helper() %}shared logic{% end %}
{# page.html #}
{% from "_helpers.html" import helper %}
{% block content %}
{{ helper() }}
{% end %}
Now both full-page render() and render_block("content", ...) work, because the block imports helperfrom another template.
Regions: An Alternative
{% region %}blocks are compiled as part of the template and have access to defs and imports from the same file. If you need a parameterized block that uses defs, consider a region instead of splitting into a separate template:
{% from "_helpers.html" import helper %}
{% region sidebar(current_path="/") %}
{{ helper(current_path) }}
{% end %}
Both render_block("sidebar", ...) and {{ sidebar(...) }} work, and the region body can call helper(). See Functions — Regions.
When This Matters
- Fragment rendering — Chirp and similar frameworks use
render_block()to return HTML fragments for htmx, SSE, etc. - Block caching — Site-scoped block caching renders blocks individually via
render_block().
See Also
- Framework Integration — Block rendering, introspection, adapters
- Functions and macros —
{% def %}and{% from %} - Block caching — Caching individual blocks