Jinja2 Migration
Migrate your templates from Jinja2 to Kida.
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+
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
- Full Migration Guide — Step-by-step with verification
- Feature Comparison — Detailed feature matrix
- Thread Safety — Free-threading deep dive
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
- Control Flow — Pattern matching syntax
- Comparison — Kida vs Jinja2
- Performance — Benchmark results
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 tries
getattrfirst regardless of type, so{{ data.items }}resolves to thedict.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: <script>
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
- Filters — Transform output values
- Control Flow — Conditionals and loops
- Filter Reference — All built-in filters
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,
setinside a block modifies the outer variable. In Kida,setis block-scoped. Useletfor template-wide variables orexportto 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. Anexportinside anifinside aforwill 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
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
- Filter Reference — Complete filter list
- Custom Filters — Create your own filters
- Variables — Output expressions
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
- Built-in Filters — All built-in filters
- Custom Filters Tutorial — Step-by-step guide
- Filter Syntax — Using filters in templates