Control Flow

Conditionals, loops, and pattern matching in Kida templates

5 min read 991 words

Conditionals

if / elif / else

{% if user.is_admin %}
    <span class="badge">Admin</span>
{% elif user.is_moderator %}
    <span class="badge">Mod</span>
{% else %}
    <span class="badge">User</span>
{% end %}

Note: Kida uses unified {% end %}to close all blocks.

Inline Conditionals

{{ "Active" if is_active else "Inactive" }}

Boolean Operators

{% if user and user.is_active %}
{% if not disabled %}
{% if a or b %}
{% if (a and b) or c %}

Comparison Operators

{% if count > 0 %}
{% if status == "active" %}
{% if value in allowed_values %}
{% if item is defined %}

Loops

for Loop

{% for item in items %}
    <li>{{ item.name }}</li>
{% end %}

Loop with else

Theelseblock runs when the sequence is empty:

{% for item in items %}
    <li>{{ item.name }}</li>
{% else %}
    <li>No items found</li>
{% end %}

Loop Context

Access loop state via theloopvariable:

{% for item in items %}
    {{ loop.index }}      {# 1-based index #}
    {{ loop.index0 }}     {# 0-based index #}
    {{ loop.first }}      {# True on first iteration #}
    {{ loop.last }}       {# True on last iteration #}
    {{ loop.length }}     {# Total items #}
    {{ loop.revindex }}   {# Reverse index (1-based) #}
    {{ loop.revindex0 }}  {# Reverse index (0-based) #}
{% end %}

Unpacking

{% for key, value in data.items() %}
    {{ key }}: {{ value }}
{% end %}

{% for x, y, z in coordinates %}
    Point: ({{ x }}, {{ y }}, {{ z }})
{% end %}

Filtering Items

{% for user in users if user.is_active %}
    {{ user.name }}
{% end %}

Pattern Matching

Kida adds{% match %}for cleaner branching:

{% match status %}
{% case "active" %}
    ✓ Active
{% case "pending" %}
    ⏳ Pending
{% case "error" %}
    ✗ Error: {{ error_message }}
{% case _ %}
    Unknown status
{% end %}

Match with Guards

{% match user %}
{% case {"role": "admin"} %}
    Full access
{% case {"role": "user", "verified": true} %}
    Standard access
{% case _ %}
    Limited access
{% end %}

Variables

Kida has three scoping keywords. Understanding the difference is critical when migrating from Jinja2.

let

Template-wide variable — visible everywhere after assignment:

{% let name = "Alice" %}
{% let items = [1, 2, 3] %}
{% let total = price * quantity %}
{{ name }}  → Alice

This is the closest equivalent to Jinja2's {% set %}.

set

Block-scoped variable — does not leak out of{% if %}, {% for %}, or other blocks:

{% let x = "outer" %}
{% if true %}
    {% set x = "inner" %}
    {{ x }}  → inner
{% end %}
{{ x }}  → outer   {# set did not modify the outer variable #}

Coming from Jinja2? This is the biggest scoping difference. In Jinja2,set inside a block modifies the outer variable. In Kida,setis block-scoped. Use let for template-wide variables or exportto push values out of blocks.

export / promote

Promotes a variable to the template (outermost) scope — it escapes all nested blocks, not just the immediate parent:

{% let total = 0 %}
{% for item in items %}
    {% export total = total + item.price %}
{% end %}
{{ total }}  → sum of all prices

promote is an alias for export— use whichever reads better:

{% for item in items %}
    {% promote first_item ??= item %}
{% end %}
{{ first_item }}  → first item in the list

This replaces Jinja2's namespace()pattern for accumulating values inside loops.

Note:export / promotealways writes to the template scope, even from deeply nested blocks. Anexport inside an if inside a forwill escape both.

Nullish Assignment (??=)

Use??=to assign only when a variable is undefined or None. Works with all three scoping keywords:

{# Set defaults without overwriting existing values #}
{% let title ??= "Untitled" %}
{% let show_sidebar ??= true %}

{# In loops — capture only the first value #}
{% for item in items %}
    {% promote winner ??= item %}
{% end %}
{{ winner }}  → first item (??= is a no-op after that)

{# With context variables — respect what the caller passed #}
{% let page_class ??= "default" %}

??= checks for both undefined and None. It does not treat falsy values like0, "", or False as missing — only Noneand truly undefined variables.

Whitespace Control

Trim whitespace with-:

{%- if true -%}
    trimmed
{%- end -%}
  • {%-trims whitespace before the tag
  • -%}trims whitespace after the tag
  • {{-trims whitespace before the expression
  • -}}trims whitespace after the expression

Whitespace Trim vs Negative Numbers

The-modifier must appear immediately after the delimiter with no space. A space before-makes it a unary minus (negative number):

{{-5}}     {# Whitespace trim + outputs 5 #}
{{ -5 }}   {# Outputs -5 (negative number) #}
{{ - 5 }}  {# Also outputs -5 #}

When outputting a negative literal, always include a space after {{:

{# Correct — negative number #}
{{ -5 }}

{# Surprising — trims whitespace and outputs 5, not -5 #}
{{-5}}

The same applies to block tags:

{%- set x = -5 %}   {# Trims whitespace, assigns -5 #}
{% set x = -5 %}     {# No trim, assigns -5 #}

See Also