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.

Framework Error Views

Frameworks should render Kida diagnostics from structured exception data, not by scrapingstr(exc) or terminal-oriented format_compact()output.

from kida import TemplateError, UndefinedError

try:
    html = template.render(context)
except UndefinedError as exc:
    diagnostic = exc.to_diagnostic()
    html = diagnostic.format_html_page()
except TemplateError as exc:
    html = f"<pre>{exc.format_compact()}</pre>"

For UndefinedError, the diagnostic payload includes the error code, kind, missing name, template location, source snippet, ordered hints, documentation URL, template stack, and component stack. The built-in HTML and Markdown renderers escape source text and names at the final surface. If a framework builds its own page, it should keep the Kida template location as authoritative and collapse generated Python traceback frames by default.

Case Studies

Legacy Integration: 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
  • Literal block modifiersBlockMetadata.modifiersfor framework-owned enhancement, fallback, and transport declarations
  • 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.

Kida validates only the generic modifier contract: unique names and literal scalar values. Chirp or another framework owns the supported modifier names, allowed value vocabulary, and any response behavior derived from them. Reading metadata alone does not change Kida rendering or cache identity.

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

Shape Profiles

Use the opt-in shape profiler when tooling needs deterministic structural facts without asking Kida to decide whether a component should be extracted or flattened:

from kida.analysis import profile_source, profile_template

source_report = profile_source(source, name="components/card.kida")
compiled_report = profile_template(template)

for profile in source_report.profiles:
    print(profile.kind, profile.name, profile.span, profile.facts.node_count)

payload = source_report.to_dict()  # JSON-compatible kida.shape-profiles v1

Reports contain one whole-template profile followed by source-ordered profiles for{% def %}, {% region %}, {% block %}, and {% fragment %}owners. Each immutableShapeFactsrecord keeps node and source volume, depth, branches, loops, dynamic expressions and density, component calls, slots, literal HTML attributes, repeated AST shapes, conservative context dependencies, and a normalized structural fingerprint as separate facts. There is deliberately no combined score, severity, suggestion, or automatic edit.

Every profile has an exactSourceSpan. profile_template()re-analyzes the compiled template's retained source, so a fresh compilation and a bytecode-cache hit describe the same source shape instead of optimization artifacts. This is an opt-in analysis cost and does not change rendering or default compilation.

The shape-profile surface is programmatic only. It does not add a CLI mode, directory/multi-root discovery, or a rootkidaexport. Callers can serialize report.to_dict() with the standard jsonmodule when they need stable machine-readable facts.

Extraction Candidate Advice

Tooling can opt into conservative component-boundary advice for one source or compiled template:

from kida.analysis import advise_extraction_source, advise_extraction_template

source_report = advise_extraction_source(source, name="messages.kida")
compiled_report = advise_extraction_template(template)

for diagnostic in source_report.diagnostics:
    print(diagnostic.code, diagnostic.span, dict(diagnostic.metadata))

K-MOD-102 is an informational, conservative Diagnostic. Kida emits it only when several independent signals converge: a substantial iterated region or normalized repeated siblings, owned interaction/accessibility structure, and a small lexical input boundary. A loop, repeated markup, template size, or branch count alone is not enough. Existing defs, regions, blocks, and call boundaries are not re-recommended.

Each finding has an exact primary span, contributing signals, tentative props, possible slots, context and component dependencies, provide/consume dependencies, and related locations for repeated siblings. Metadata list values are deterministic compact JSON strings. The result deliberately has no combined score and nosafe_edit: component ownership and reuse remain human decisions.

This API is single-source and opt-in. It does not changeDiagnosticOptions, diagnose_source(), kida check, default compilation, suppression syntax, or rendering. Directory and multi-root orchestration belong to a later adapter contract.

Multi-Root Encapsulation Advice

Explicitly owned roots can be analyzed as one deterministic component call graph:

from pathlib import Path

from kida.inspection import TemplateRoot, advise_encapsulation_roots

report = advise_encapsulation_roots(
    (
        TemplateRoot("app", Path("templates")),
        TemplateRoot("adapter", Path("adapter-templates")),
    )
)

The report preserves K-MOD-102 extraction candidates and adds K-MOD-103 for exact pass-through components. Flatten advice requires one same-owner caller, one downstream component, an identical typed/defaulted prop interface, at most one identically forwarded slot, and no owned markup or behavior. Reuse, zero local callers, cross-root callers or downstream components, markup, control flow, accessibility structure, context policy, changed defaults, renamed/narrowed props, or a different slot surface suppress the finding.

Every flatten finding includes an exact definition span, the only caller and downstream definition as related locations, contributing signals, owner/source facts, and forwarded props/slots. It is informational and conservative, has no safe edit, and explicitly asks the reviewer to preserve documented public, product, test, and adapter boundaries.

This programmatic API is opt-in. It does not add a CLI flag or change default kida checkbehavior. Root ownership is explicit; Kida does not infer package or adapter semantics from ambient imports.

Adapter Advice Context

Framework adapters can translate their own route, response, or component facts into immutable context attached to exact profile spans:

from kida.analysis import AdviceContext, profile_source
from kida.inspection import TemplateRoot, advise_encapsulation_roots

profile = profile_source(source, name="app/messages.kida")
response_block = next(item for item in profile.profiles if item.kind == "block")
context = AdviceContext(
    response_block.span,
    (
        ("consumer_context", "iterated"),
        ("preserve_boundary", True),
        ("response_boundary", True),
        ("role", "framework-response"),
    ),
)

report = advise_encapsulation_roots(roots, context=(context,))

The recognized generic facts are consumer_context ("iterated"or "repeated"), boolean preserve_boundary and response_boundary, descriptive role, and visibility ("package" or "public"). An iterated response block stays intact while nested extraction candidates can become visible. Visibility and preservation facts suppress flatten advice only for an exact definition span.

Unknown facts and values are ignored; a context containing only unknown facts produces the same report as no adapter context. Kida does not interpret framework modifier names or import framework packages. Facts are sorted, invocation-local, and represented in ordinary diagnostic metadata, so no new schema or shared registry is introduced. See the adapter advice-context contract for the complete matching and compatibility rules.

Evidence-Driven Encapsulation Loop

Humans and coding agents can use the same supported loop without a specialized Kida orchestration layer:

  1. Runadvise_encapsulation_roots()and consume each finding's code, exact span, confidence, metadata, and related locations.
  2. Choose extraction, pass-through inlining, or intentional preservation from that evidence; Kida does not make the ownership decision.
  3. Rundiagnose_roots(..., options=DiagnosticOptions(validate_calls=True))to validate the resulting props and slots.
  4. Render the same context before and after, including named response blocks.
  5. Re-run advice and record remaining findings, false positives, false negatives, context effects, and analysis cost.

The runnable examples/encapsulation_loop commits five before/after cases and deterministic calibration JSON: a growing route, a healthy large layout, a pass-through micro-component, an adapter-preserved response boundary, and multiple explicit roots. The replay proves render and call-validation parity while leaving ambiguous edits to the consumer. Optional--measureoutput stays outside the stable snapshot.

uv run python examples/encapsulation_loop/app.py
uv run python examples/encapsulation_loop/app.py --measure --rounds 5

Dependencies

The dependency walker extracts every context variable path a template accesses. Results are conservative: they may include paths not taken at runtime (e.g., both branches of an{% if %}) but never miss a path that is used.

>>> template.depends_on()
frozenset({'page.title', 'page.content', 'site.menu'})

required_context()extracts just the top-level names:

>>> template.required_context()
frozenset({'page', 'site'})

Purity

A block is pure if its output is deterministic given the same inputs. Pure blocks can be cached safely. Impure blocks (usingrandom, shuffle, or similar) must be re-rendered each time.

Purity Meaning Cacheable?
"pure" Deterministic output Yes
"impure" Uses non-deterministic functions No
"unknown" Cannot determine statically Treat as impure

Cache Scope

Cache scope tells you how broadly a block's output can be reused:

Scope Meaning Example
"site" Same output for every page Navigation, footer
"page" Varies per page but stable for a given page Content, title
"none" Cannot cache (impure block) Random quotes
"unknown" Cannot determine Mixed dependencies

Landmarks

Kida detects HTML5 landmark elements (<nav>, <main>, <header>, <footer>, <aside>) in block output and uses them to infer the block's role:

>>> meta["nav"].emits_landmarks
frozenset({'nav'})
>>> meta["nav"].inferred_role
'navigation'

Context Validation

validate_context()is designed for build systems, SSR frameworks, and testing pipelines that need to catch missing variables before rendering:

template = env.get_template("email.html")
missing = template.validate_context(user_context)
if missing:
    raise ValueError(f"Missing template variables: {missing}")
result = template.render(**user_context)

This runs dependency analysis (cached after first call) and compares required top-level variable names against the provided context keys plus environment globals. It returns a sorted list of missing names, or an empty list if everything is present.

Dotted Context Contracts

Frameworks that know more than top-level keys can compare a route or handler contract against Kida's dotted dependency paths:

from kida.analysis import check_context_contract

issues = check_context_contract(
    template,
    provided={"page.title", "page.author.name"},
    globals={"csrf_token"},
    optional={"flash.message"},
)

for issue in issues:
    print(issue.code, issue.path, issue.message)

provided, globals, and optionalcan be dotted-path iterables or nested mappings. This checker is deliberately route-agnostic: frameworks supply the contract shape, and Kida reports only whether template dependency paths are covered. Enablecheck_extra=Truewhen a narrow contract should also warn about provided paths the template does not read.

K-CTX-001 and K-CTX-002 are registered in the public ErrorCodeenum; ContextContractIssue.coderemains the same string-valued field for backward compatibility.

Literal Attribute Extraction

Framework adapters and CI reports can inspect literal HTML attributes without parsing Kida's AST themselves:

from kida.analysis import extract_literal_attributes

attrs = extract_literal_attributes(
    template,
    prefixes=("data-", "hx-"),
)

for attr in attrs:
    print(attr.tag, attr.name, attr.value, attr.lineno)

This reports only attributes visible in static template text. Dynamic attributes such as{{ attrs }}, xmlattr, or helper-generated markup are intentionally not inferred. Kida provides the generic facts; frameworks decide whether a literaldata-*, hx-*, id, or other attribute has route-specific meaning.

Escape Audit

Useaudit_escaping()to inventory where a template outputs escaped values, where autoescape is disabled, and where filters intentionally produce trusted markup:

from kida.analysis import audit_escaping

for finding in audit_escaping(template):
    print(finding.code, finding.kind, finding.expression, finding.message)

Findings are static diagnostics only. They do not change rendering and do not prove user input was sanitized.| safefindings include the optional reason=text when present; missing reasons get a suggestion so code review and CI reports can point at explicit trust boundaries. CodesK-ESC-001through K-ESC-005 are registered in the public ErrorCodeenum without changing the existing string-valuedEscapeAuditFinding.codefield.

Privacy Lint

Uselint_privacy()to catch likely private data exposure before templates or report fixtures are published:

from kida.analysis import lint_privacy

for finding in lint_privacy(template):
    print(finding.code, finding.kind, finding.path, finding.message)

The first version is intentionally narrow. It reports sensitive-looking context paths, secret-like string literals without echoing their values,| safeon sensitive-looking values, broad debug context output, and dynamic template names that a framework policy cannot statically allowlist. CodesK-PRI-001through K-PRI-005 are registered in the public ErrorCodeenum; findings retain their existing string-valuedcodefield.

Call-Site Validation

Kida can validate{% def %}call sites at compile time, catching parameter errors before any template is rendered:

from kida import Environment

env = Environment(validate_calls=True)

template = env.from_string("""
    {% def button(text: str, url: str, style="primary") %}
        <a href="{{ url }}" class="btn btn-{{ style }}">{{ text }}</a>
    {% end %}

    {{ button(text="Save", urll="/save") }}
""")
# UserWarning: Call to 'button' at <string>:6 — unknown params: urll; missing required: url

What It Checks

Issue Example
Unknown params Callingbutton(labl="X") when param is label
Missing required Callingbutton() when texthas no default
*args / **kwargsrelaxation Definitions with*args or **kwargssuppress unknown-param warnings

Programmatic API

For build systems and CI pipelines, use theBlockAnalyzerdirectly:

from kida import Environment, DictLoader
from kida.analysis import BlockAnalyzer

env = Environment(
    loader=DictLoader({"page.html": src}),
    preserve_ast=True,
)
template = env.get_template("page.html")

analyzer = BlockAnalyzer()
issues = analyzer.validate_calls(template._optimized_ast)

for issue in issues:
    if not issue.is_valid:
        print(f"{issue.def_name} at line {issue.lineno}: "
              f"unknown={issue.unknown_params}, "
              f"missing={issue.missing_required}")

CallValidation

Field Type Description
def_name str Name of the called{% def %}
lineno int Line number of the call site
col_offset int Column offset of the call site
unknown_params tuple[str, ...] Keyword args not in the definition
missing_required tuple[str, ...] Required params not provided
duplicate_params tuple[str, ...] Params passed more than once; parser-produced ASTs reject these before validation
Property Description
is_valid Trueif no issues were found

Configuration

UseAnalysisConfigto customize analysis behavior for your framework:

from kida import AnalysisConfig
from kida.analysis import BlockAnalyzer

config = AnalysisConfig(
    # Variables indicating page-specific scope
    page_prefixes=frozenset({"post.", "post", "article.", "article"}),
    # Variables indicating site-wide scope
    site_prefixes=frozenset({"settings.", "settings", "global."}),
    # Additional functions your framework guarantees are pure
    extra_pure_functions=frozenset({"asset_url", "t", "current_lang"}),
    # Filters that produce non-deterministic output
    extra_impure_filters=frozenset({"random_choice"}),
)

analyzer = BlockAnalyzer(config=config)

Kida ships with a default config (DEFAULT_CONFIG) that includes common SSG pure functions likeasset_url, t, canonical_url, etc.

Historical 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
check_context_contract() (template, provided, ...) -> list[ContextContractIssue] Dotted context contract diagnostics
extract_literal_attributes() (template_or_ast, names=..., prefixes=...) -> list[LiteralAttribute] Literal HTML attributes with source locations
audit_escaping() (template_or_ast, include_output_sites=True) -> list[EscapeAuditFinding] Static escape and trusted-markup findings
lint_privacy() (template_or_ast) -> list[PrivacyFinding] Sensitive path, secret literal, and broad-output findings
block_metadata() () -> dict[str, BlockMetadata] Per-block analysis results
template_metadata() () -> TemplateMetadata | None Full template analysis
is_cacheable() (block_name: str | None) -> bool Cache safety check
list_blocks() () -> list[str] Block names in template

BlockMetadata

Field Type Description
name str Block identifier
modifiers tuple[BlockModifierMetadata, ...] Ordered literal framework metadata with source locations
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)

BlockMetadata.get_modifier(name)returns the matching immutable BlockModifierMetadata or None. Each modifier exposes name, its typed scalarvalue, and the one-based lineno / zero-based col_offsetwhere its name begins.

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

Filters should be deterministic with no side effects. This allows Kida to optimize them at compile time.

# ✅ 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

The @pureDecorator

Mark your filter with@pure to enable compile-time evaluation. When all inputs are statically known (via static_context), the partial evaluator executes the filter during compilation and replaces it with a constant — eliminating the filter call entirely at render time.

from kida import Environment, pure

@pure
def clean(value):
    return value.strip().lower()

env = Environment()
env.add_filter("clean", clean)

With static_context, the filter is evaluated at compile time:

# "  HELLO  " | clean is folded to "hello" during compilation
tpl = env.from_string(
    '{{ name | clean }}',
    static_context={"name": "  HELLO  "},
)
tpl.render()  # "hello" — no filter call at render time

Without @pure, filters still work normally at render time — the decorator only enables the compile-time optimization. See Compiler Internals for more on how pure filters interact with f-string coalescing and partial evaluation.

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

Contract Status

The framework-author surface below is treated as the documented integration contract:

  • imports exported fromkida.__all__
  • names exported from the publickida.diagnosticsmodule
  • AdviceContext and AdviceFactValue from kida.analysis
  • Extension and ExtensionDiagnosticContext from kida.extensions
  • ErrorCodenames and values
  • Environment(...)constructor parameters
  • Templaterender, block-render, streaming, and metadata methods
  • metadata dataclass fields forBlockMetadata, BlockModifierMetadata, DefParamInfo, DefMetadata, TemplateMetadata, and TemplateStructureManifest
  • render-capture records, manifests, diffs, freeze-cache analysis, and search manifest helpers exported fromkida
  • loader constructor behavior documented on this page
  • sandbox, render context, capture, and manifest objects exported fromkida

Snapshot tests guard those contracts. Any change to the stable surface should be deliberate and land with docs and changelog updates when behavior changes.

Internals remain internal even when they are visible to Python: underscored attributes, generated template namespace entries, parser/compiler node shapes, cache implementation details, and helper functions outsidekida.__all__.

Programmatic Diagnostics

kida.diagnosticsexposes immutable, surface-neutral findings for editors, framework adapters, codemods, and other Python tooling. It is a public module, but its names are deliberately not copied into the rootkidanamespace.

from kida.diagnostics import DiagnosticOptions, diagnose_source

report = diagnose_source(
    unsaved_editor_text,
    name="pages/account.html",
    environment=env,
    options=DiagnosticOptions(
        validate_calls=True,
        typed=True,
        a11y=True,
    ),
)

for finding in report.diagnostics:
    start = finding.span.start
    print(finding.code, finding.message, start.line if start else None)

diagnose_source()parses and compiles the supplied text directly so compiler warnings are included alongside analysis findings. The unsaved buffer is not added to the environment's template or bytecode caches. A supplied Environmentcontributes lexer/parser configuration, extensions, autoescape selection, and loader-backed imported component metadata. Resolving imports may use that environment's existing thread-safe template cache; registries are not mutated.

Usediagnose_directory() for the same collection and options as kida check:

from kida.diagnostics import DiagnosticOptions, diagnose_directory

report = diagnose_directory(
    "templates",
    options=DiagnosticOptions(strict=True, lint_fragile_paths=True),
)

For explicit multi-root tooling, use the public kida.inspectioninventory:

from pathlib import Path

from kida.diagnostics import DiagnosticOptions
from kida.inspection import TemplateRoot, diagnose_roots, inspect_components

roots = (
    TemplateRoot("framework", Path("framework/templates")),
    TemplateRoot("app", Path("app/templates")),
)

report = diagnose_roots(
    roots,
    options=DiagnosticOptions(validate_calls=True),
)
catalog = inspect_components(roots)

Logical names are always namespace/relative/path; no root-order override is inferred.ComponentRecordpreserves the owner, logical template identifier, resolved source path, and immutableDefMetadata. ComponentInspectionalso returns structured configuration/load diagnostics and apartialflag.

Adapters that need custom filters, globals, tests, or extensions can pass a preconfiguredEnvironmentwhose loader resolves the same prefixed names:

report = diagnose_roots(roots, environment=adapter_environment)
catalog = inspect_components(roots, environment=adapter_environment)

Kida reads that environment without mutating its registries. This keeps adapter configuration programmatic and avoids ambient discovery or a Kida config-file surface.

Adapters can pass immutable, exact-spanAdviceContextrecords to advise_encapsulation_roots(..., context=...). Kida recognizes only the framework-neutral role, visibility, preservation, response-boundary, and consumer-context facts documented in Static Analysis. Unknown facts are ignored, and adapter context never changes parsing, rendering, or default diagnostics.

Both functions return a frozenDiagnosticReportcontaining an ordered tuple ofDiagnostic records and a partial flag. partial=Truemeans requested analysis could not finish, such as when malformed source prevents later static checks. Reports intentionally have no CLI exit code or stream-routing policy. Compiler warnings retain their stable code, template path and line, suggestion, and confidence; advisory warnings do not gain asafe_edit.

The public model includesDiagnostic, DiagnosticSeverity, DiagnosticConfidence, SourcePosition, SourceSpan, RelatedLocation, DiagnosticSnippet, and SafeEdit. Lines are one-based, columns are zero-based, and range ends are exclusive.SafeEditrequires an exact range; the type's presence does not mean every finding has a provably safe edit.

Apply proven edits against the exact source snapshot used for diagnosis:

from kida.diagnostics import apply_safe_edits

updated = apply_safe_edits(
    unsaved_editor_text,
    report.diagnostics,
    path="pages/account.html",
)

apply_safe_edits()selects edits for the requested path and ignores advisory findings and findings for other files. Before changing anything, it verifies that every touched source line still matches the diagnostic's captured snippet and that selected ranges do not overlap. It raisesValueErrorfor stale, incomplete, or overlapping edits, leaving the caller's source unchanged.

Kida initially emits safe edits only for--strictunified closers: the exact end keyword in {% end %} becomes endif, endfor, enddef, and so on. Type typo suggestions, fragile paths, component-call findings, and semantic migration advice remain advisory until their complete replacement can be proven.

Framework adapters can normalize caught Kida exceptions without parsing their rendered text:

from kida import TemplateError
from kida.diagnostics import diagnostic_from_exception

try:
    template.render(user=user)
except TemplateError as error:
    finding = diagnostic_from_exception(error)

diagnostic_from_exception()accepts Kida template and lexer exceptions only. It raisesTypeErrorfor unrelated Python exceptions rather than relabeling application failures as Kida diagnostics.

A supplied environment also invokes extension diagnostic hooks registered with that environment. Extensions receive immutable source/AST context and visible component signatures, while Kida validates namespace ownership, category, confidence, location, and safe-edit snapshots. See Extensions for the complete protocol. Directory diagnosis and the CLI do not discover or run these hooks.

Render Capture and Manifests

Build and framework tooling can opt in to compiler-injected capture hooks with Environment(enable_capture=True), then use captured_render()to collect a RenderCapturefor one render. Completed captures can be accumulated in a RenderManifest, compared as a ManifestDiff, inspected through Fragment records andFreezeCache/FreezeCacheStats, or adapted into search output with SearchManifestBuilder, SearchEntry, and default_field_extractor.

get_capture()exposes the active render-local capture to integration helpers. The active value is ContextVar-scoped, while captures, manifests, freeze caches, and builders remain mutable caller-owned objects. See Render Capture and Manifests for the preferred lifecycle, thread-safety rules, runnable workflow, and context-capture privacy boundary.

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 | "html" | "terminal" | "markdown" | Callable True Output escaping mode
auto_reload bool True Check for source changes
preserve_ast bool True Preserve analysis metadata used by introspection and enriched render capture
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
strict_undefined bool True RaiseUndefinedError on missing variable or attribute access (set to Falsefor lenient empty-string fallback)
jinja2_compat_warnings bool True Warn on proven Jinjaif-local setflow that Kida block scope changes
validate_calls bool False Validate{% def %}call sites at compile time
enable_capture bool False Compile opt-in render-capture hooks; activate them per render withcaptured_render()

Constructor contract note: the generated dataclass constructor is part of the snapshot gate. Parameters not documented here are compatibility details rather than recommended framework integration points.

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_overrides names a block; the value is a pre-rendered HTML string. Unknown block names now raise TemplateRuntimeErrorwith did-you-mean suggestions.

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']

Component Introspection

list_defs()

List all{% def %}component names in the template.

names = template.list_defs()
# ['card', 'nav_link', 'badge']
def_metadata()

Return metadata for all{% def %} components in the template. Returns a dict[str, DefMetadata].

meta = template.def_metadata()
card = meta["card"]
print(card.name)              # "card"
print(card.template_name)     # "components/card.html"
print(card.lineno)            # 3
print(card.params)            # (DefParamInfo(name='title', annotation='str', ...), ...)
print(card.slots)             # ('actions', 'footer')
print(card.has_default_slot)  # True
print(card.depends_on)        # frozenset({'site.title'})
print(card.vararg)            # None or '*args' parameter name
print(card.kwarg)             # None or '**kwargs' parameter name

DefMetadata fields:

Field Type Description
name str Component name
template_name str | None Source template
lineno int Line number of{% def %}
params tuple[DefParamInfo, ...] Parameter metadata
slots tuple[str, ...] Named slot names
has_default_slot bool Whether{% slot %}(unnamed) exists
depends_on frozenset[str] Context paths read by the def body outside declared params and locals
vararg str | None Name of*argsparameter, if present
kwarg str | None Name of**kwargsparameter, if present

DefParamInfo fields:

Field Type Description
name str Parameter name
annotation str | None Type annotation (e.g."str", "int")
has_default bool Whether a default value is defined
is_required bool Trueif no default value
warnings

Compile-time warnings collected during template compilation.

for w in template.warnings:
    print(w.code, w.message, w.lineno)

Template 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 }}
warnings list[TemplateWarning] Compile-time warnings (precedence, coercion, migration)

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. All Kida exceptions carry anErrorCode accessible via exc.code.

The same publicErrorCoderegistry includes static-analysis families for privacy (K-PRI-*), context contracts (K-CTX-*), escaping (K-ESC-*), accessibility (K-A11Y-*), template declaration types (K-TYP-*), and fragile paths (K-PATH-001). Analysis records expose stable string codes so existing serialization and comparisons do not need enum-specific handling.

TemplateSyntaxError

Invalid template syntax.

from kida import TemplateSyntaxError

try:
    env.from_string("{% if x %}")  # Missing end
except TemplateSyntaxError as e:
    print(e)

TemplateRuntimeError

Error during template rendering. Includescomponent_stack for errors inside {% def %}components:

from kida import TemplateRuntimeError

try:
    template.render(items=None)
except TemplateRuntimeError as e:
    print(e.code)             # ErrorCode enum value
    print(e.component_stack)  # [(def_name, lineno, template_name), ...]
    print(e.format_compact()) # Formatted error with source snippet

TemplateNotFoundError

Template file not found. Includes caller context (requesting template and line number).

from kida import TemplateNotFoundError

try:
    env.get_template("nonexistent.html")
except TemplateNotFoundError as e:
    print(e)

UndefinedError

Accessing undefined variable or attribute. Thekindfield distinguishes between variable, attribute, and key lookups.

from kida import UndefinedError

try:
    env.from_string("{{ missing }}").render()
except UndefinedError as e:
    print(e.kind)  # "variable", "attribute", or "key"
    print(e)       # "Undefined variable 'missing' in <string>:1"

Under the default strict mode, attribute access errors also include component context:

env = Environment()  # strict_undefined=True by default
# "Undefined attribute 'typo' on User object in page.html:5"

Opt out per-Environment with strict_undefined=Falseif you need empty-string fallback for missing attributes.

SecurityError

Raised bySandboxedEnvironmentwhen a template violates the security policy. Carries error codes K-SEC-001 through K-SEC-005.

Warning Classes

Kida's Python warning categories share one root, so applications can choose a specific policy for component-contract findings or one policy for every Kida warning:

UserWarning
└── KidaWarning
    ├── ComponentWarning
    ├── PrecedenceWarning
    ├── CoercionWarning
    └── MigrationWarning
Class Code Description
KidaWarning Base category for all Python warnings emitted by Kida
ComponentWarning K-CMP-001, K-CMP-002 Invalid component arguments or statically known prop type mismatches
PrecedenceWarning K-WARN-001 | binds tighter than ??
CoercionWarning Silent type coercion in filters (e.g."abc" | float0.0)
MigrationWarning K-WARN-002 Jinjaif-local {% set %}is read after the Kida block or shadows a template-wide binding

Promote only component-contract warnings to errors:

import warnings

from kida import ComponentWarning

warnings.filterwarnings("error", category=ComponentWarning)

Or apply one policy to every current and future Kida warning category:

import warnings

from kida import KidaWarning

warnings.filterwarnings("error", category=KidaWarning)

Python warning filters apply to subclasses, so the second example includes ComponentWarning, PrecedenceWarning, CoercionWarning, and MigrationWarning. TemplateWarningis different: it is the structured record stored inTemplate.warnings, not a subclass of Python's Warning.


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