Migrate from Jinja2

Rewrite Jinja2 templates for Kida

5 min read 916 words

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-templates

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: Keep Matching Block Endings

No migration edit is required for matching explicit closers on constructs Kida supports. The following Jinja source is valid in Kida unchanged:

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

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

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

Unified {% end %}remains canonical Kida style, so you may normalize the same template later as an optional style cleanup:

{% 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 %} Accepted unchanged;{% end %}is the canonical alternative
{% endfor %} Accepted unchanged;{% end %}is the canonical alternative
{% endblock %} Accepted unchanged;{% end %}is the canonical alternative
{% macro name(...) %} {% def name(...) %} (Kida has no {% macro %}keyword)
{% endmacro %} {% end %}
{{ x \| filter }} {{ x \|> filter }}(pipeline)
{% if %}...{% elif %}...{% endif %} {% match %}...{% case %}...{% end %}
N/A {% cache key %}...{% end %}

Audit assignments that cross anifblock

Jinja2 lets an assignment inside{% if %}update the surrounding scope; Kida keeps{% set %} block-scoped. kida checkreports K-WARN-002 when it can prove that such a binding is read after the block:

{% if user %}
  {% set greeting = "Hello " ~ user.name %}
{% endif %}
{{ greeting }}

Use {% let greeting = ... %}when the name is first introduced and should be template-wide. Use{% export greeting = ... %}when updating an existing template-wide binding. Assignments do not escape{% for %}in either engine; anif-local assignment read later in the same iteration can still be reported with loop-local restructuring advice. K-WARN-002 is advisory only because control flow, shadowing, and dynamic values can make an automatic rewrite unsound.

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

Matching explicit closers are accepted. If Kida reports a block-ending error, check that the closer matches the open construct. Both of these are valid:

{% if x %}...{% endif %}
{% if x %}...{% end %}  {# canonical Kida style #}

Do not blanket-rewrite endif, endfor, or endblockduring migration.

See Also