Coming from Jinja2? This track walks you through syntax differences, migration steps, and verification—so you can switch with confidence.

Tip

Duration: ~30 min | Prerequisite: Jinja2 experience, Python 3.14+

1

Coming from Jinja2

Quick cheat sheet for Jinja2 users switching to Kida

Coming from Jinja2

A quick reference for Jinja2 users. For a full migration guide, see Migrate from Jinja2.

What Stays the Same

Most Jinja2 syntax works in Kida unchanged:

{{ variable }}
{{ user.name }}
{{ items | join(", ") }}
{% if condition %}...{% end %}
{% for item in items %}...{% end %}
{% extends "base.html" %}
{% block content %}...{% end %}
{% include "partial.html" %}
{# comments #}

What's Different

Block Endings

Jinja2 uses tag-specific endings. Kida uses a unified{% end %}:

{# Jinja2 #}
{% if user %}...{% endif %}
{% for x in items %}...{% endfor %}
{% block nav %}...{% endblock %}

{# Kida #}
{% if user %}...{% end %}
{% for x in items %}...{% end %}
{% block nav %}...{% end %}

Pipeline Operator

Kida adds|>for readable, left-to-right filter chains:

{# Jinja2 — filters read right-to-left #}
{{ title | truncate(50) | upper | escape }}

{# Kida — pipeline reads left-to-right #}
{{ title |> escape |> upper |> truncate(50) }}

Both | and |>work in Kida. Use whichever you prefer.

Pattern Matching

Kida adds{% match %} for cleaner branching (like Python's match):

{# Jinja2 — chained if/elif #}
{% if status == "active" %}
    <span class="green">Active</span>
{% elif status == "pending" %}
    <span class="yellow">Pending</span>
{% elif status == "inactive" %}
    <span class="red">Inactive</span>
{% else %}
    <span>Unknown</span>
{% endif %}

{# Kida — pattern matching #}
{% match status %}
    {% case "active" %}<span class="green">Active</span>
    {% case "pending" %}<span class="yellow">Pending</span>
    {% case "inactive" %}<span class="red">Inactive</span>
    {% case _ %}<span>Unknown</span>
{% end %}

Scoping Keywords

Kida provides explicit scoping for variable assignment:

{# Jinja2 #}
{% set name = "Alice" %}

{# Kida — three scoping options #}
{% set name = "Alice" %}      {# Block-scoped: doesn't leak out #}
{% let name = "Alice" %}      {# Template-wide: visible everywhere #}
{% export name = "Alice" %}   {# Exports to template scope from inner blocks #}
{% promote name = "Alice" %}  {# Alias for export — same behavior #}

Nullish Assignment

Kida adds??= to assign only when a variable is undefined or None:

{# Jinja2 — verbose default pattern #}
{% if title is not defined %}{% set title = "Untitled" %}{% endif %}

{# Kida — one line #}
{% let title ??= "Untitled" %}

Null-Safe Filter Chains

Kida adds?| (optional filter) and ?|>(safe pipeline) for None-safe filtering:

{# Jinja2 — errors if value is None #}
{{ value | upper }}

{# Kida — skips filter on None, provides fallback #}
{{ value ?| upper ?? "N/A" }}
{{ user?.name ?|> upper ?|> trim ?? "Anonymous" }}

Built-in Caching

Kida has native block-level caching (no extensions needed):

{% cache "sidebar" %}
    {# Expensive rendering cached automatically #}
    {% for item in nav_items %}
        <a href="{{ item.url }}">{{ item.title }}</a>
    {% end %}
{% end %}

API Comparison

Operation Jinja2 Kida
Create env jinja2.Environment() kida.Environment()
File loader jinja2.FileSystemLoader(...) kida.FileSystemLoader(...)
Dict loader jinja2.DictLoader(...) kida.DictLoader(...)
Load template env.get_template(name) env.get_template(name)
From string env.from_string(src) env.from_string(src)
Render template.render(**ctx) template.render(**ctx)
Add filter env.filters["name"] = fn env.add_filter("name", fn)
Add test env.tests["name"] = fn env.add_test("name", fn)
Stream render template.generate(**ctx) template.render_stream(**ctx)
Async render N/A template.render_stream_async(**ctx)
Render block N/A template.render_block(name, **ctx)
Template metadata N/A template.template_metadata(), meta.regions()

What Kida Adds

Feature Description
Pipeline|> Left-to-right filter chains
Safe pipeline?|> None-propagating filter chains
Optional filter?| Skip filter when value is None
Nullish assign??= Assign only if undefined/None
{% match %} Pattern matching for cleaner branching
{% region %} Parameterized blocks forrender_block()and OOB (no Jinja2 equivalent)
{% yield %} Explicit slot forwarding in nested macro composition
{% cache %} Built-in block-level output caching
{% let %} / {% export %} / {% promote %} Explicit variable scoping
Native async {% async for %}, {{ await expr }}
Streaming render_stream()yields chunks for HTMX/SSE
Block rendering render_block(), render_with_blocks()for fragments and layout composition
Static analysis template_metadata(), depends_on(), regions()for framework integration
Free-threading True parallelism on Python 3.14t
T-string tag k(t"Hello {name}!")for inline templates

See Also

2

Migrate from Jinja2

Rewrite Jinja2 templates for Kida

Migrate from Jinja2

Guide for rewriting Jinja2 templates in Kida syntax.

:::note[Not a Drop-In Replacement] Kida is not compatible with Jinja2. Templates must be rewritten using Kida syntax. This guide helps you translate common patterns. :::

Prerequisites

  • Python 3.14+
  • Familiarity with Jinja2 template syntax

Step 1: Install Kida

pip install kida

Step 2: Update Imports

Before (Jinja2):

from jinja2 import Environment, FileSystemLoader
from markupsafe import Markup

After (Kida):

from kida import Environment, FileSystemLoader
from kida import Markup  # Built-in, no markupsafe

Step 3: Update Environment Creation

Before (Jinja2):

env = Environment(
    loader=FileSystemLoader('templates'),
    autoescape=select_autoescape(),
    extensions=['jinja2.ext.do'],
)

After (Kida):

env = Environment(
    loader=FileSystemLoader('templates'),
    autoescape=True,
    cache_size=400,
    fragment_ttl=300.0,
)

Note: Kida does not use Jinja2-style extensions. For side effects, use {% set _ = expr %}.

Step 4: Rewrite Block Endings

Convert specific endings to unified{% end %}:

Jinja2:

{% if condition %}
    content
{% endif %}

{% for item in items %}
    {{ item }}
{% endfor %}

{% block content %}
    ...
{% endblock %}

Kida:

{% if condition %}
    content
{% end %}

{% for item in items %}
    {{ item }}
{% end %}

{% block content %}
    ...
{% end %}

Step 5: Update Custom Filters

Before (Jinja2):

def format_money(value, currency='$'):
    return f'{currency}{value:,.2f}'

env.filters['money'] = format_money

After (Kida):

def format_money(value, currency='$'):
    return f'{currency}{value:,.2f}'

env.add_filter('money', format_money)

# Or use the decorator
@env.filter()
def format_money(value, currency='$'):
    return f'{currency}{value:,.2f}'

Step 6: Update Custom Tests

Before (Jinja2):

def is_prime(n):
    return n > 1 and all(n % i for i in range(2, n))

env.tests['prime'] = is_prime

After (Kida):

env.add_test('prime', is_prime)

# Or use the decorator
@env.test()
def is_prime(n):
    return n > 1 and all(n % i for i in range(2, n))

API Mapping

Jinja2 Kida
Environment Environment
FileSystemLoader FileSystemLoader
DictLoader DictLoader
Template.render() Template.render()
Template.render_async() Template.render_async()
Markup(markupsafe) Markup(built-in)
env.filters[name] = func env.add_filter(name, func)
env.tests[name] = func env.add_test(name, func)
env.globals[name] = value env.add_global(name, value)

Syntax Translation

Jinja2 Kida
{% endif %} {% end %}
{% endfor %} {% end %}
{% endblock %} {% end %}
{% endmacro %} {% end %}
{{ x \| filter }} {{ x \|> filter }}(pipeline)
{% if %}...{% elif %}...{% endif %} {% match %}...{% case %}...{% end %}
N/A {% cache key %}...{% end %}

New Features

After migrating, explore Kida-only features:

Pipeline Operator

{{ title |> escape |> upper |> truncate(50) }}

Pattern Matching

{% match status %}
{% case "active" %}
    ✓ Active
{% case "pending" %}
    ⏳ Pending
{% case _ %}
    Unknown
{% end %}

Block Caching

{% cache "sidebar-" ~ user.id %}
    {{ render_sidebar(user) }}
{% end %}

Verification

After migration, verify templates render correctly:

from kida import Environment

env = Environment()
template = env.from_string("Hello, {{ name }}!")
result = template.render(name="World")
assert result == "Hello, World!"
print("✅ Migration successful!")

Common Issues

Markup Import

Error:ImportError: cannot import name 'Markup' from 'markupsafe'

Fix: Import from Kida:

from kida import Markup  # Not from markupsafe

Filter Registration

Error:TypeError: 'FilterRegistry' object does not support item assignment

Fix: Useadd_filter():

env.add_filter('name', func)  # Not env.filters['name'] = func

Block Endings

Error:TemplateSyntaxError: Unexpected tag 'endif'

Fix: Use unified{% end %}:

{% if x %}...{% end %}  # Not {% endif %}

See Also

3

Variables

Output expressions and variable access in Kida templates

Variables

Basic Output

Use double braces to output expressions:

{{ name }}
{{ user.email }}
{{ items[0] }}
{{ 1 + 2 }}

Attribute Access

Access object attributes with dot notation:

{{ user.name }}
{{ page.metadata.title }}

For dictionary keys:

{{ data.key }}
{{ data["key-with-dashes"] }}

Dict-Safe Resolution

For dict objects, dot notation resolves to dictionary keys first, then falls back to attributes. This means{{ data.items }} returns data["items"] (your data), not the dict.itemsmethod:

{# data = {"items": ["a", "b"], "keys": ["x", "y"]} #}

{{ data.items }}    {# → ["a", "b"] — the key, not dict.items() #}
{{ data.keys }}     {# → ["x", "y"] — the key, not dict.keys() #}

For non-dict objects (dataclasses, custom classes), dot notation uses getattr first, then falls back to subscript. This is the safe default for objects with __getitem__.

Jinja2 difference: Jinja2 always triesgetattr first regardless of type, so {{ data.items }} resolves to the dict.itemsmethod. Kida handles this correctly for dicts.

Index Access

Access sequence items by index:

{{ items[0] }}
{{ items[-1] }}  {# Last item #}
{{ matrix[0][1] }}

HTML Escaping

By default, output is HTML-escaped for security:

{{ "<script>" }}

Output: &lt;script&gt;

Mark Content as Safe

Use thesafefilter for trusted HTML:

{{ html_content | safe }}

Or with an optional reason for code review:

{{ cms_block | safe(reason="sanitized by bleach") }}

Pipelines

Chain filters with the pipe operator:

{{ name | upper }}
{{ title | escape | truncate(50) }}

Use the pipeline operator |>for improved readability:

{{ title |> escape |> upper |> truncate(50) }}

Both syntaxes are equivalent:

{# Traditional pipe #}
{{ items | sort(attribute='date') | first }}

{# Pipeline operator #}
{{ items |> sort(attribute='date') |> first }}

Safe Pipeline

Use?|>for None-propagating pipelines. If the value is None, the entire chain short-circuits instead of erroring:

{# Without ?|> — errors if user.name is None #}
{{ user.name |> upper |> trim }}

{# With ?|> — safely returns None, then ?? provides fallback #}
{{ user?.name ?|> upper ?|> trim ?? "Anonymous" }}

This composes naturally with optional chaining (?.) and null coalescing (??).

Optional Filter

Use?|to skip a filter when the value is None:

{# Errors if value is None #}
{{ value | upper }}

{# Safely skips — returns None, then ?? provides fallback #}
{{ value ?| upper ?? "N/A" }}

{# Chain multiple optional filters #}
{{ config?.debug ?| string ?? "unset" }}

?| works like |but checks for None before applying the filter. Use it anywhere you'd write| default("") | filter— it's cleaner and doesn't swallow falsy values like0 or "".

Default Values

Handle missing or None values:

{{ user.nickname | default("Anonymous") }}
{{ count | default(0) }}

Shorthand dalias:

{{ missing | d("fallback") }}

Expressions

Full Python expressions are supported:

{{ price * 1.1 }}
{{ "Hello, " + name }}
{{ items | length > 0 }}
{{ value if condition else fallback }}

String Literals

Use single or double quotes:

{{ "Hello" }}
{{ 'World' }}
{{ "It's fine" }}
{{ 'Say "hi"' }}

String Concatenation

Kida supports both+ and ~for string building:

  • +adds when both operands are numeric, otherwise it concatenates as strings.
  • ~always coerces both sides to strings, which makes it the clearest choice for dynamic URLs, paths, and IDs.
{{ "Hello, " + name }}
{{ count + " items" }}
{{ "/path/" ~ id ~ "/action" }}
hx-post="{{ '/chains/' ~ chain_id ~ '/add-step' }}"

Static analysis tools (e.g. Chirp's contract checker) treat these concatenated attribute values as dynamic and skip route validation for such URLs.

Method Calls

Call methods on objects:

{{ name.upper() }}
{{ items.count(x) }}
{{ text.split(',')[0] }}

Global Functions

Built-in functions available in all templates:

{{ range(10) }}
{{ len(items) }}
{{ dict(a=1, b=2) }}

Available globals: range, dict, list, set, tuple, len, str, int, float, bool, abs, min, max, sum, sorted, reversed, enumerate, zip, map, filter.

See Also

4

Control Flow

Conditionals, loops, and pattern matching in Kida templates

Control Flow

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

See Also

5

Filters

Transform values in template expressions

Filters

Filters transform values in template expressions using the pipe syntax.

Basic Usage

{{ name | upper }}
{{ title | truncate(50) }}
{{ items | join(", ") }}

Chaining Filters

{{ name | lower | capitalize }}
{{ text | striptags | truncate(100) }}

Pipeline Operator

Use|>for improved readability:

{{ title |> escape |> upper |> truncate(50) }}

Safe Pipeline (?|>)

None-propagating pipeline — if the value is None, all subsequent filters are skipped:

{{ user?.name ?|> upper ?|> trim ?? "Anonymous" }}

Optional Filter (?|)

Skip a single filter when the value is None:

{{ value ?| upper ?? "N/A" }}
{{ config?.debug ?| string ?? "unset" }}

Unlike | default("") | upper, ?| preserves falsy values (0, "", False) and only short-circuits onNone.

Common Filters

String Filters

Filter Description Example
upper Uppercase {{ "hello" \| upper }}HELLO
lower Lowercase {{ "HELLO" \| lower }}hello
capitalize Capitalize first {{ "hello" \| capitalize }}Hello
title Title case {{ "hello world" \| title }}Hello World
trim Strip whitespace {{ " hi " \| trim }}hi
truncate Shorten text {{ text \| truncate(50) }}
replace Replace text {{ s \| replace("a", "b") }}
striptags Remove HTML {{ html \| striptags }}

Collection Filters

Filter Description Example
first First item {{ items \| first }}
last Last item {{ items \| last }}
length Item count {{ items \| length }}
sort Sort items {{ items \| sort }}
reverse Reverse order {{ items \| reverse }}
unique Remove duplicates {{ items \| unique }}
join Concatenate {{ items \| join(", ") }}
take First N items {{ items \| take(5) }}
skip Skip N items {{ items \| skip(10) }}

CSS / HTML Filters

Filter Description Example
classes Join CSS classes, drop falsy {{ ["btn", "", "lg"] \| classes }}btn lg
escape HTML escape {{ html \| escape }}
safe Mark as safe {{ trusted \| safe }}
striptags Remove tags {{ html \| striptags }}

Number Filters

Filter Description Example
abs Absolute value {{ -5 \| abs }}5
decimal Fixed decimal places {{ 3.14159 \| decimal(2) }}3.14
round Round number {{ 3.7 \| round }}4
int Convert to int {{ "42" \| int }}42
float Convert to float {{ "3.14" \| float }}3.14

Utility Filters

Filter Description Example
default Fallback value {{ x \| default("N/A") }}
tojson JSON encode {{ data \| tojson }}
pprint Pretty print {{ data \| pprint }}
debug Debug output {{ items \| debug }}

Filter Arguments

Filters can accept arguments:

{{ text | truncate(50) }}
{{ text | truncate(50, killwords=true) }}
{{ text | truncate(50, end="...") }}
{{ items | sort(attribute="date", reverse=true) }}

Sorting with Attributes

Sort objects by attribute:

{% for post in posts | sort(attribute="date") %}
    {{ post.title }}
{% end %}

{# Multiple attributes #}
{% for page in pages | sort(attribute="weight,title") %}
    {{ page.title }}
{% end %}

{# Reverse order #}
{% for post in posts | sort(attribute="date", reverse=true) %}
    {{ post.title }}
{% end %}

Grouping

Group items by attribute:

{% for group in posts | groupby("category") %}
    <h2>{{ group.grouper }}</h2>
    {% for post in group.list %}
        {{ post.title }}
    {% end %}
{% end %}

Filtering Items

{# Select items matching condition #}
{{ items | selectattr("is_active") }}
{{ items | selectattr("status", "eq", "published") }}

{# Reject items matching condition #}
{{ items | rejectattr("is_draft") }}

Safe HTML

Mark content as trusted HTML:

{{ html_content | safe }}

{# With reason for code review #}
{{ cms_block | safe(reason="sanitized by bleach library") }}

See Also

6

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

✓ Track Complete