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

1

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 renderrender()for page output
  • Bytecode cache — Persistent.bengal/cache/kida/for cold-start
  • Fragment cache{% cache %}with site-scoped TTL
  • Analysisblock_metadata(), is_cacheable()for incremental builds

Chirp (Web Framework)

  • Full renderrender()for full-page responses
  • Block renderrender_block()for HTMX fragments, partial updates
  • Layout compositionrender_with_blocks()for programmatic layout assembly
  • Streamingrender_stream(), render_stream_async()for chunked HTTP
  • Introspectiontemplate_metadata() for composition planning, validate_block_exists() before render_block
  • AdapterKidaAdapter implements Chirp's TemplateAdapterinterface

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

  1. 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 %}
    
  2. Discover at build time — Chirp callsmeta = template.template_metadata() and meta.regions() to get region blocks. Each has region_params, depends_on, and cache_scope.

  3. Render on navigation — For HTMX boosted requests, Chirp renders the main content block plus each OOB region viarender_block(), passing the region's params from the request context.

  4. 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

2

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. 1

    Compile all templates

    Enable AST preservation when compiling.

  2. 2

    Analyze each template

    Get block metadata for each compiled template.

  3. 3

    Identify site-cacheable blocks

    Find nav, footer, sidebar usingcache_scope == "site".

  4. 4

    Cache site-scoped blocks

    Cache once per build, reuse across all pages.

  5. 5

    Re-render page-scoped blocks only

    Only when page content changes.

  6. 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. 1

    Composition planning

    Usetemplate_metadata()to discover blocks and inheritance before rendering.

  2. 2

    Block validation

    Callvalidate_block_exists(env, template, block) before render_block()to avoid KeyError.

  3. 3

    Fragment rendering

    Userender_block()for HTMX partial responses and Turbo Stream updates.

  4. 4

    Layout assembly

    Userender_with_blocks()to inject pre-rendered content into layout templates.

  5. 5

    Adapter pattern

    KidaAdapter implements Chirp's TemplateAdapterinterface, 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.

3

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

4

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 offunctools.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
5

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 abytecode_cache configured, pass name= to enable it. Without a name, there's no stable cache key, so the bytecode cache is bypassed. A UserWarning is emitted if you call from_string() without name=when a bytecode cache is active.

Partial evaluation: Passstatic_context={...} to evaluate expressions at compile time. Overrides Environment's static_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: Callingrender() or render_stream() on a template where is_async is True raises TemplateRuntimeError. Use render_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 match
  • list_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 loader
  • list_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 resources
  • list_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] — Calls load_funcand normalizes result
  • list_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>")
# &lt;script&gt;

# 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

✓ Track Complete