Filters & Tests

Difficulty: Beginner

Jinja2 filters and tests are tools for transforming and checking template variables. Filters modify the output of an expression, while tests check conditions about a value. They keep your templates clean by handling formatting and logic directly in the template layer.

Filters are applied to expressions using the pipe operator (|). The syntax is {{ value|filter }}. Filters can be chained: {{ value|filter1|filter2 }}. Some filters accept arguments: {{ value|truncate(100) }}. Filters never modify the original variable; they return a new transformed value.

Jinja2 provides many built-in filters. The most commonly used include: upper and lower (case conversion), title (title case), capitalize (first letter uppercase), trim (strip whitespace), truncate(length) (shorten text with ellipsis), default(value) (provide a fallback for undefined/empty values), length (count items), sort (sort a list), join(separator) (join list elements), replace(old, new) (string replacement), int and float (type conversion), round(precision) (number rounding), safe (mark as safe HTML), and escape (force HTML escaping).

The default filter is particularly useful. {{ username|default('Anonymous') }} displays 'Anonymous' if username is undefined or empty. This prevents template errors and provides sensible fallback values. You can use default(value, boolean=true) to also catch empty strings and zero values.

Tests are used with the 'is' keyword in Jinja2 expressions. They return true or false and are commonly used in {% if %} statements. Built-in tests include: defined (variable exists), undefined (variable does not exist), none (value is None), true/false (boolean checks), string/number/integer/float (type checks), iterable (can be looped), mapping (is a dict), even/odd (number checks), and divisibleby(num).

Flask allows you to register custom filters and tests. Custom filters are Python functions registered with @app.template_filter('name') or app.jinja_env.filters['name']. Custom tests are registered similarly with @app.template_test('name'). This lets you extend Jinja2 with domain-specific formatting like date formatting, currency display, or markdown rendering.

Filters are evaluated in template rendering, so they should be fast. Avoid putting heavy computation in custom filters. If a transformation requires significant processing, do it in the view function and pass the result to the template.

Code examples

Common Built-in Filters

<!-- String filters -->
{{ 'hello world'|title }}          <!-- Hello World -->
{{ 'HELLO'|lower }}                <!-- hello -->
{{ '  spaces  '|trim }}            <!-- spaces -->
{{ 'Hello World'|replace('World', 'Flask') }}  <!-- Hello Flask -->
{{ 'A long text here...'|truncate(10) }}       <!-- A long ... -->

<!-- Number filters -->
{{ 3.14159|round(2) }}             <!-- 3.14 -->
{{ 42|string }}                    <!-- '42' -->

<!-- List filters -->
{{ [3,1,2]|sort }}                 <!-- [1, 2, 3] -->
{{ ['a','b','c']|join(', ') }}     <!-- a, b, c -->
{{ items|length }}                 <!-- number of items -->

<!-- Default values -->
{{ username|default('Guest') }}    <!-- Guest if undefined -->
{{ bio|default('No bio yet', true) }}  <!-- handles empty strings too -->

Filters transform values using the pipe operator. They can be chained and some accept arguments. The default filter is essential for handling missing or empty values gracefully.

Jinja2 Tests

<!-- Type tests -->
{% if name is defined %}
    <p>Hello, {{ name }}</p>
{% endif %}

{% if value is none %}
    <p>No value provided</p>
{% endif %}

{% if count is number %}
    <p>Count: {{ count }}</p>
{% endif %}

<!-- Number tests -->
{% for i in range(10) %}
    {% if i is even %}
        <span class="even">{{ i }}</span>
    {% else %}
        <span class="odd">{{ i }}</span>
    {% endif %}
{% endfor %}

{% if total is divisibleby(3) %}
    <p>Divisible by 3!</p>
{% endif %}

<!-- String tests -->
{% if email is string %}
    <p>Email: {{ email }}</p>
{% endif %}

Tests check conditions about values using the 'is' keyword. They are used in if statements to check types, properties, and relationships. Use 'is not' for negation.

Custom Filters

from flask import Flask
import datetime

app = Flask(__name__)

@app.template_filter('timeago')
def timeago_filter(dt):
    """Convert a datetime to a human-friendly 'time ago' string."""
    now = datetime.datetime.now()
    diff = now - dt
    seconds = diff.total_seconds()

    if seconds < 60:
        return 'just now'
    elif seconds < 3600:
        return f'{int(seconds // 60)} minutes ago'
    elif seconds < 86400:
        return f'{int(seconds // 3600)} hours ago'
    else:
        return f'{int(seconds // 86400)} days ago'

@app.template_filter('currency')
def currency_filter(value, symbol='
#39;): return f'{symbol}{value:,.2f}' # Usage in templates: # {{ post.created_at|timeago }} # {{ product.price|currency }} # {{ product.price|currency('EUR') }}

Custom filters are Python functions registered with @app.template_filter(). They receive the piped value as the first argument. Additional filter arguments come as function parameters.

Chaining Filters and Advanced Usage

<!-- Chaining multiple filters -->
{{ user.bio|default('No bio')|truncate(150)|title }}

<!-- Filter with conditional -->
{{ items|sort(attribute='name')|join(', ') if items else 'No items' }}

<!-- Using filters in for loops -->
{% for user in users|sort(attribute='created_at', reverse=true) %}
    <div class="user">
        <h3>{{ user.name|title }}</h3>
        <p>{{ user.email|lower }}</p>
        <small>Joined: {{ user.created_at|timeago }}</small>
    </div>
{% endfor %}

<!-- Filters on blocks -->
{% filter upper %}
    This text will be ALL UPPERCASE.
{% endfilter %}

Filters can be chained left to right, each receiving the output of the previous. The sort filter accepts 'attribute' for sorting objects. {% filter %} blocks apply a filter to an entire section of content.

Key points

Concepts covered

Filters, Tests, Custom Filters, Built-in Filters, Pipe Operator