Framework Integration
Integrate Kida into your framework or build system.
Integrating Kida into a framework, SSG, or custom tool? This track covers the introspection API, adapter pattern, block rendering, and static analysis—so you can build robust integrations.
Tip
Duration: ~45 min | Prerequisite: Python 3.14+, template engine concepts
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
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.
Custom Filters
Create custom template filters
Custom Filters
Filters transform values in template expressions.
Basic Filter
from kida import Environment
env = Environment()
def double(value):
return value * 2
env.add_filter("double", double)
Template usage:
{{ 21 | double }}
{# Output: 42 #}
Decorator Syntax
@env.filter()
def double(value):
return value * 2
@env.filter("twice") # Custom name
def my_double(value):
return value * 2
Filter Arguments
@env.filter()
def truncate_words(value, count=10, end="..."):
words = str(value).split()
if len(words) <= count:
return value
return " ".join(words[:count]) + end
Template usage:
{{ text | truncate_words(5) }}
{{ text | truncate_words(10, end="[more]") }}
Keyword Arguments
@env.filter()
def format_number(value, decimals=2, separator=","):
formatted = f"{value:,.{decimals}f}"
if separator != ",":
formatted = formatted.replace(",", separator)
return formatted
Template usage:
{{ price | format_number }}
{{ price | format_number(decimals=0) }}
{{ price | format_number(separator=".") }}
Handling None
Make filters None-resilient:
@env.filter()
def upper_safe(value):
if value is None:
return ""
return str(value).upper()
Returning Markup
For HTML output, return Markup to prevent double-escaping:
from kida import Markup
@env.filter()
def bold(value):
escaped = Markup.escape(str(value))
return Markup(f"<b>{escaped}</b>")
@env.filter()
def link(value, url):
escaped_text = Markup.escape(str(value))
escaped_url = Markup.escape(str(url))
return Markup(f'<a href="{escaped_url}">{escaped_text}</a>')
Batch Registration
filters = {
"double": lambda x: x * 2,
"triple": lambda x: x * 3,
"reverse": lambda x: x[::-1],
}
env.update_filters(filters)
Common Patterns
Currency Formatting
@env.filter()
def currency(value, symbol="$", decimals=2):
if value is None:
return ""
return f"{symbol}{value:,.{decimals}f}"
Date Formatting
from datetime import datetime
@env.filter()
def format_date(value, pattern="%Y-%m-%d"):
if value is None:
return ""
if isinstance(value, str):
value = datetime.fromisoformat(value)
return value.strftime(pattern)
Text Slugification
import re
@env.filter()
def slugify(value):
if value is None:
return ""
text = str(value).lower()
text = re.sub(r"[^\w\s-]", "", text)
return re.sub(r"[-\s]+", "-", text).strip("-")
Pluralization
@env.filter()
def pluralize(count, singular, plural=None):
if plural is None:
plural = singular + "s"
return singular if count == 1 else plural
Usage:
{{ items | length }} {{ items | length | pluralize("item") }}
Best Practices
Keep Filters Pure
# ✅ Pure: no side effects
@env.filter()
def process(value):
return value.upper()
# ❌ Impure: modifies external state
counter = 0
@env.filter()
def count_calls(value):
global counter
counter += 1 # Side effect
return value
Handle Edge Cases
@env.filter()
def safe_divide(value, divisor):
if divisor == 0:
return 0 # Or raise error
return value / divisor
Document Filters
@env.filter()
def initials(name, separator=""):
"""
Extract initials from a name.
Args:
name: Full name string
separator: Character between initials
Returns:
Initials (e.g., "JD" for "John Doe")
"""
if not name:
return ""
return separator.join(
word[0].upper() for word in name.split() if word
)
See Also
- Built-in Filters — All built-in filters
- Custom Filters Tutorial — Step-by-step guide
- Filter Syntax — Using filters in templates
Custom Loaders
Build custom template loaders
Custom Loaders
Build custom loaders to load templates from databases, APIs, or other sources.
Quick Option: FunctionLoader
For simple cases, wrap a callable with the built-inFunctionLoaderinstead of writing a full class:
from kida import Environment, FunctionLoader
# Simple dict lookup
templates = {"page.html": "<h1>{{ title }}</h1>"}
env = Environment(loader=FunctionLoader(lambda name: templates.get(name)))
# CMS integration in 3 lines
def load_from_cms(name):
source = cms_client.get(f"templates/{name}")
return (source, f"cms://{name}") if source else None
env = Environment(loader=FunctionLoader(load_from_cms))
Return str for source only, tuple[str, str | None] for source + filename, or Noneif not found.
Full Custom Loader
For complex loaders with state, implement the Loader protocol.
Loader Protocol
Implement two methods:
class Loader:
def get_source(self, name: str) -> tuple[str, str | None]:
"""Return (source, filename) for template."""
...
def list_templates(self) -> list[str]:
"""Return list of all template names."""
...
Database Loader
Load templates from a database:
from kida import Environment, TemplateNotFoundError
class DatabaseLoader:
def __init__(self, connection):
self.conn = connection
def get_source(self, name: str) -> tuple[str, str | None]:
row = self.conn.execute(
"SELECT source FROM templates WHERE name = ?",
(name,)
).fetchone()
if not row:
raise TemplateNotFoundError(f"Template '{name}' not found")
# Return (source, filename for error messages)
return row[0], f"db://{name}"
def list_templates(self) -> list[str]:
rows = self.conn.execute(
"SELECT name FROM templates"
).fetchall()
return sorted(row[0] for row in rows)
# Usage
env = Environment(loader=DatabaseLoader(db_connection))
template = env.get_template("page.html")
Redis Loader
Load templates from Redis:
import redis
from kida import TemplateNotFoundError
class RedisLoader:
def __init__(self, host="localhost", port=6379, prefix="templates:"):
self.client = redis.Redis(host=host, port=port)
self.prefix = prefix
def get_source(self, name: str) -> tuple[str, str | None]:
key = f"{self.prefix}{name}"
source = self.client.get(key)
if source is None:
raise TemplateNotFoundError(f"Template '{name}' not found")
return source.decode("utf-8"), f"redis://{key}"
def list_templates(self) -> list[str]:
pattern = f"{self.prefix}*"
keys = self.client.keys(pattern)
return sorted(k.decode().removeprefix(self.prefix) for k in keys)
# Usage
env = Environment(loader=RedisLoader())
HTTP Loader
Load templates from a remote server:
import httpx
from kida import TemplateNotFoundError
class HTTPLoader:
def __init__(self, base_url: str):
self.base_url = base_url.rstrip("/")
self.client = httpx.Client()
def get_source(self, name: str) -> tuple[str, str | None]:
url = f"{self.base_url}/{name}"
try:
response = self.client.get(url)
response.raise_for_status()
except httpx.HTTPError:
raise TemplateNotFoundError(f"Template '{name}' not found")
return response.text, url
def list_templates(self) -> list[str]:
# Could fetch index from server
return []
# Usage
env = Environment(loader=HTTPLoader("https://templates.example.com"))
Combining with Built-in Loaders
Kida ships withChoiceLoader and PrefixLoaderthat work with any loader (including custom ones):
from kida import ChoiceLoader, PrefixLoader, FileSystemLoader
# Try database first, then filesystem
env = Environment(
loader=ChoiceLoader([
DatabaseLoader(db),
FileSystemLoader("templates/"),
])
)
# Namespace by prefix
env = Environment(
loader=PrefixLoader({
"db": DatabaseLoader(db),
"files": FileSystemLoader("templates/"),
})
)
# Loads from database
env.get_template("db/page.html")
# Loads from filesystem
env.get_template("files/base.html")
See Loading Templates for more on built-in loaders.
Caching Layer
Add caching to any loader:
import threading
class CachedLoader:
def __init__(self, loader, maxsize=128):
self.loader = loader
self._cache: dict[str, tuple[str, str | None]] = {}
self._lock = threading.Lock()
self._maxsize = maxsize
def get_source(self, name: str) -> tuple[str, str | None]:
if name in self._cache:
return self._cache[name]
with self._lock:
if name in self._cache:
return self._cache[name]
result = self.loader.get_source(name)
if len(self._cache) >= self._maxsize:
self._cache.pop(next(iter(self._cache)))
self._cache[name] = result
return result
def list_templates(self) -> list[str]:
return self.loader.list_templates()
def clear_cache(self):
with self._lock:
self._cache.clear()
# Usage
env = Environment(
loader=CachedLoader(DatabaseLoader(db), maxsize=256)
)
Thread safety: This uses a lock-based double-check pattern instead of
functools.lru_cache, which is not thread-safe under free-threaded Python (3.13t+). See Thread Safety for more on concurrent access patterns.
Thread Safety
Loaders should be thread-safe:
import threading
class ThreadSafeLoader:
def __init__(self, loader):
self.loader = loader
self._lock = threading.Lock()
def get_source(self, name: str) -> tuple[str, str | None]:
with self._lock:
return self.loader.get_source(name)
def list_templates(self) -> list[str]:
with self._lock:
return self.loader.list_templates()
Best Practices
Raise TemplateNotFoundError
from kida import TemplateNotFoundError
def get_source(self, name):
if not self._exists(name):
# ✅ Raise proper exception
raise TemplateNotFoundError(f"Template '{name}' not found")
# ❌ Don't return None
# return None
Include Source Location
def get_source(self, name):
source = self._load(name)
# Include meaningful filename for error messages
filename = f"db://{name}" # or full path, URL, etc.
return source, filename
Handle Encoding
def get_source(self, name):
data = self._load_bytes(name)
source = data.decode("utf-8") # Handle encoding
return source, name
See Also
- Loading Templates — All built-in loaders (FileSystemLoader, DictLoader, ChoiceLoader, PrefixLoader, PackageLoader, FunctionLoader)
- API Reference — Loader protocol and API
- Architecture — Template compilation pipeline
API Reference
Core classes and methods
API Reference
Environment
Central configuration and template management hub.
from kida import Environment, FileSystemLoader
env = Environment(
loader=FileSystemLoader("templates/"),
autoescape=True,
)
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
loader |
Loader |
None |
Template source provider |
autoescape |
bool | Callable |
True |
HTML auto-escaping |
auto_reload |
bool |
True |
Check for source changes |
cache_size |
int |
400 |
Max cached templates |
fragment_cache_size |
int |
1000 |
Max cached fragments |
fragment_ttl |
float |
300.0 |
Fragment TTL (seconds) |
static_context |
dict | None |
None |
Values for compile-time partial evaluation |
Methods
get_template(name)
Load and cache a template by name.
template = env.get_template("page.html")
Raises: TemplateNotFoundError, TemplateSyntaxError
from_string(source, name=None)
Compile a template from string (not cached in the template cache).
template = env.from_string("Hello, {{ name }}!")
Bytecode caching: If you have a
bytecode_cacheconfigured, passname=to enable it. Without a name, there's no stable cache key, so the bytecode cache is bypassed. AUserWarningis emitted if you callfrom_string()withoutname=when a bytecode cache is active.
Partial evaluation: Pass
static_context={...}to evaluate expressions at compile time. Overrides Environment'sstatic_contextfor this call.
render(template_name, **context)
Load and render in one step.
html = env.render("page.html", title="Hello", items=items)
render_string(source, **context)
Compile and render string in one step.
html = env.render_string("{{ x * 2 }}", x=21)
add_filter(name, func)
Register a custom filter.
env.add_filter("double", lambda x: x * 2)
add_test(name, func)
Register a custom test.
env.add_test("even", lambda x: x % 2 == 0)
add_global(name, value)
Add a global variable.
env.add_global("site_name", "My Site")
filter() (decorator)
Decorator to register a filter.
@env.filter()
def double(value):
return value * 2
test() (decorator)
Decorator to register a test.
@env.test()
def is_even(value):
return value % 2 == 0
cache_info()
Get cache statistics.
info = env.cache_info()
# {'template': {...}, 'fragment': {...}}
clear_cache(include_bytecode=False)
Clear all caches.
env.clear_cache()
Template
Compiled template with render interface.
Rendering (all users)
render(**context)
Render template with context.
html = template.render(name="World", items=[1, 2, 3])
render_async(**context)
Render template asynchronously (thread-pool wrapper for sync templates).
html = await template.render_async(items=async_generator())
render_stream(**context)
Render template as a sync generator of string chunks. Yields at statement boundaries for chunked HTTP and streaming.
for chunk in template.render_stream(items=data):
send_to_client(chunk)
render_stream_async(**context)
Render template as an async stream. Supports native{% async for %} and {{ await }}constructs. Also works on sync templates (wraps the sync stream).
async for chunk in template.render_stream_async(items=async_iterable):
send_to_client(chunk)
Raises: RuntimeErrorif no render function is available.
Block rendering (fragments, frameworks)
render_block(block_name, **context)
Render a single block from the template. Supports inherited blocks: when the template extends a parent, you can render parent-only blocks by name (e.g.render_block("content") on a child that extends a base defining content). Child overrides still win; super()is not supported.
html = template.render_block("content", title="Hello")
Raises: KeyErrorif the block does not exist in the template or any parent.
render_with_blocks(block_overrides, **context)
Render template with pre-rendered HTML injected into blocks. Enables programmatic layout composition without{% extends %}in the template source.
layout = env.get_template("_layout.html")
html = layout.render_with_blocks({"content": inner_html}, title="Page")
Each key in block_overridesnames a block; the value is a pre-rendered HTML string.
render_block_stream_async(block_name, **context)
Render a single block as an async stream. Supports inherited blocks likerender_block(). Falls back to wrapping the sync block stream if no async variant exists.
async for chunk in template.render_block_stream_async("content", items=data):
send_to_client(chunk)
Raises: KeyErrorif the block does not exist.
list_blocks()
List all blocks available forrender_block(), including inherited blocks.
blocks = template.list_blocks()
# ['title', 'nav', 'content', 'footer']
Introspection (frameworks, build systems)
template_metadata()
Return full template analysis (blocks, extends, dependencies). ReturnsNoneif AST was not preserved.
meta = template.template_metadata()
if meta:
print(meta.extends, meta.blocks.keys())
regions = meta.regions() # Only {% region %} blocks (for OOB discovery)
block_metadata()
Return per-block analysis (purity, cache scope, inferred role).
blocks = template.block_metadata()
nav = blocks.get("nav")
if nav and nav.cache_scope == "site":
...
depends_on()
Return all context paths this template may access.
deps = template.depends_on()
# frozenset({'page.title', 'site.pages'})
required_context()
Return top-level variable names the template needs.
names = template.required_context()
# frozenset({'page', 'site'})
validate_context(context)
Check a context dict for missing variables. Returns list of missing names.
missing = template.validate_context(user_context)
if missing:
raise ValueError(f"Missing: {missing}")
is_cacheable(block_name=None)
Check if a block (or all blocks) can be safely cached.
template.is_cacheable("nav") # True if nav is cacheable
template.is_cacheable() # True only if all blocks cacheable
Properties
| Property | Type | Description |
|---|---|---|
name |
str | None |
Template name |
filename |
str | None |
Source filename |
is_async |
bool |
True if template uses {% async for %} or {{ await }} |
Note: Calling
render()orrender_stream()on a template whereis_asyncisTrueraisesTemplateRuntimeError. Userender_stream_async()instead.
Loaders
FileSystemLoader
Load templates from filesystem directories.
from kida import FileSystemLoader
# Single directory
loader = FileSystemLoader("templates/")
# Multiple directories (searched in order)
loader = FileSystemLoader(["templates/", "shared/"])
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
paths |
str | Path | list |
Required | Search paths |
encoding |
str |
"utf-8" |
File encoding |
Methods
get_source(name)→tuple[str, str]list_templates()→list[str]
DictLoader
Load templates from a dictionary.
from kida import DictLoader
loader = DictLoader({
"base.html": "<html>{% block content %}{% end %}</html>",
"page.html": "{% extends 'base.html' %}...",
})
ChoiceLoader
Try multiple loaders in order, returning the first match.
from kida import ChoiceLoader, FileSystemLoader
loader = ChoiceLoader([
FileSystemLoader("themes/custom/"),
FileSystemLoader("themes/default/"),
])
Constructor Parameters
| Parameter | Type | Description |
|---|---|---|
loaders |
list[Loader] |
Loaders to try in order |
Methods
get_source(name)→tuple[str, str | None]— Returns first successful matchlist_templates()→list[str]— Merged, deduplicated, sorted list from all loaders
PrefixLoader
Namespace templates by prefix, delegating to per-prefix loaders.
from kida import PrefixLoader, FileSystemLoader
loader = PrefixLoader({
"app": FileSystemLoader("templates/app/"),
"admin": FileSystemLoader("templates/admin/"),
})
# env.get_template("app/index.html") → templates/app/index.html
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mapping |
dict[str, Loader] |
Required | Prefix → loader mapping |
delimiter |
str |
"/" |
Prefix delimiter |
Methods
get_source(name)→tuple[str, str | None]— Splits on delimiter, delegates to prefix loaderlist_templates()→list[str]— All templates with prefix prepended
PackageLoader
Load templates from an installed Python package viaimportlib.resources.
from kida import PackageLoader
loader = PackageLoader("my_app", "templates")
# env.get_template("pages/index.html") → my_app/templates/pages/index.html
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
package_name |
str |
Required | Dotted Python package name |
package_path |
str |
"templates" |
Subdirectory within the package |
encoding |
str |
"utf-8" |
File encoding |
Methods
get_source(name)→tuple[str, str | None]— Loads from package resourceslist_templates()→list[str]— All templates in the package directory (recursive)
FunctionLoader
Wrap a callable as a loader.
from kida import FunctionLoader
loader = FunctionLoader(lambda name: templates.get(name))
Constructor Parameters
| Parameter | Type | Description |
|---|---|---|
load_func |
Callable[[str], str | tuple[str, str | None] | None] |
Returns source,(source, filename), or None |
Methods
get_source(name)→tuple[str, str | None]— Callsload_funcand normalizes resultlist_templates()→list[str]— Always returns[](cannot enumerate)
Composition Helpers
Validation and structure helpers for frameworks. See Framework Integration for full usage.
from kida.composition import (
validate_block_exists,
validate_template_block,
get_structure,
block_role_for_framework,
)
validate_block_exists(env, template_name, block_name) → bool
Check if a block exists in a template (including inherited blocks). ReturnsFalseif template not found or block missing.
if validate_block_exists(env, "skills/page.html", "page_content"):
html = env.get_template("skills/page.html").render_block("page_content", ...)
validate_template_block(template, block_name) → bool
Check if a block exists in a loaded Template instance.
get_structure(env, template_name) → TemplateStructureManifest | None
Get lightweight structure manifest (block names, extends, dependencies). Cached by Environment.
struct = get_structure(env, "page.html")
if struct and "page_root" in struct.block_names:
...
TemplateStructureManifest fields:
| Field | Type | Description |
|---|---|---|
name |
str | None |
Template name |
extends |
str | None |
Parent template from{% extends %} |
block_names |
tuple[str, ...] |
Ordered block names |
block_hashes |
dict[str, str] |
Per-block structural hashes |
dependencies |
frozenset[str] |
Context paths accessed |
block_role_for_framework(block_metadata, ...) → str | None
Classify block metadata into framework roles:"fragment", "page_root", or None.
Exceptions
TemplateError
Base class for all template errors.
TemplateSyntaxError
Invalid template syntax.
from kida import TemplateSyntaxError
try:
env.from_string("{% if x %}") # Missing end
except TemplateSyntaxError as e:
print(e)
TemplateNotFoundError
Template file not found.
from kida import TemplateNotFoundError
try:
env.get_template("nonexistent.html")
except TemplateNotFoundError as e:
print(e)
UndefinedError
Accessing undefined variable.
from kida import UndefinedError
try:
env.from_string("{{ missing }}").render()
except UndefinedError as e:
print(e)
Markup
HTML-safe string wrapper.
from kida import Markup
# Create safe HTML
safe = Markup("<b>Bold</b>")
# Escape unsafe content
escaped = Markup.escape("<script>")
# <script>
# Format with escaping
result = Markup("<p>{}</p>").format(user_input)
Class Methods
| Method | Description |
|---|---|
escape(s) |
Escape string and return Markup |
Operations
| Operation | Behavior |
|---|---|
Markup + str |
str is escaped |
Markup + Markup |
Concatenated as-is |
Markup.format(...) |
Arguments are escaped |
LoopContext
Available asloop variable inside {% for %}loops.
| Property | Type | Description |
|---|---|---|
index |
int |
1-based index |
index0 |
int |
0-based index |
first |
bool |
True on first iteration |
last |
bool |
True on last iteration |
length |
int |
Total items |
revindex |
int |
Reverse 1-based index |
revindex0 |
int |
Reverse 0-based index |
{% for item in items %}
{{ loop.index }}/{{ loop.length }}
{% end %}
AsyncLoopContext
Available asloop variable inside {% async for %} loops. Provides index-forward properties only — properties that require knowing the total size raise TemplateRuntimeErrorsince async iterables have no known length.
| Property | Type | Description |
|---|---|---|
index |
int |
1-based index |
index0 |
int |
0-based index |
first |
bool |
True on first iteration |
previtem |
Any | None |
Previous item (Noneon first) |
cycle(*values) |
method | Cycle through values |
last |
— | RaisesTemplateRuntimeError |
length |
— | RaisesTemplateRuntimeError |
revindex |
— | RaisesTemplateRuntimeError |
revindex0 |
— | RaisesTemplateRuntimeError |
nextitem |
— | RaisesTemplateRuntimeError |
{% async for user in fetch_users() %}
{{ loop.index }}: {{ user.name }}
{% if loop.first %}(first!){% end %}
{% end %}
RenderContext
Per-render state management via ContextVar.
from kida.render_context import (
RenderContext,
render_context,
async_render_context,
get_render_context,
get_render_context_required,
)
RenderContext Dataclass
| Attribute | Type | Description |
|---|---|---|
template_name |
str | None |
Current template name |
filename |
str | None |
Source file path |
line |
int |
Current line (for errors) |
include_depth |
int |
Include nesting depth |
max_include_depth |
int |
Max depth (default: 50) |
cached_blocks |
dict[str, str] |
Site-scoped block cache |
Methods
| Method | Description |
|---|---|
check_include_depth(name) |
Raise if depth exceeded |
child_context(template_name=None, *, source=None) |
Create child for include/embed with incremented depth |
child_context_for_extends(parent_name, *, source=None) |
Create child for extends with incremented extends_depth |
get_meta(key, default=None) |
Get framework metadata (HTMX, CSRF, etc.) |
set_meta(key, value) |
Set framework metadata before rendering |
Functions
| Function | Description |
|---|---|
get_render_context() |
Get current context (None if not rendering) |
get_render_context_required() |
Get context or raise RuntimeError |
render_context(...) |
Context manager for render scope |
async_render_context(...) |
Async context manager for render scope |
Low-Level APIs
For cases where the context manager isn't suitable (e.g. nested include/embed that need manual restore):
| Function | Description |
|---|---|
set_render_context(ctx) |
Set a RenderContext, returns reset token |
reset_render_context(token) |
Restore previous context using token fromset_render_context() |
RenderAccumulator
Opt-in profiling for template rendering. When enabled viaprofiled_render(), the compiler-emitted instrumentation automatically tracks:
- Blocks — render timing (milliseconds) and call counts
- Filters — call counts per filter name
- Macros — call counts per
{% def %}name - Includes — counts per included template
Zero overhead when profiling is disabled — the instrumentation gates on a falsy check.
from kida.render_accumulator import (
RenderAccumulator,
profiled_render,
get_accumulator,
)
Usage
with profiled_render() as metrics:
html = template.render(page=page)
summary = metrics.summary()
# {
# "total_ms": 12.5,
# "blocks": {"content": {"ms": 8.2, "calls": 1}, "nav": {"ms": 1.1, "calls": 1}},
# "filters": {"upper": 3, "truncate": 2},
# "macros": {"card": 5},
# "includes": {"header.html": 1},
# }
RenderAccumulator Properties
| Property | Type | Description |
|---|---|---|
block_timings |
dict[str, BlockTiming] |
Block render times |
macro_calls |
dict[str, int] |
Macro call counts |
include_counts |
dict[str, int] |
Include counts |
filter_calls |
dict[str, int] |
Filter usage counts |
total_duration_ms |
float |
Total render time |
Methods
| Method | Description |
|---|---|
record_block(name, ms) |
Record block timing |
record_macro(name) |
Record macro call |
record_include(name) |
Record include |
record_filter(name) |
Record filter usage |
summary() |
Get metrics dict |
See Also
- Filters Reference — All built-in filters
- Tests Reference — All built-in tests
- Configuration — All options