Custom Filters

Create custom template filters

2 min read 483 words

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