Custom filters, tests and globals

Jinja · Advanced · 12 min

Domain formatting such as money, dates or slugs belongs in reusable filters, not repeated template code.

When the built-ins aren't enough, extend the Environment. All three are plain dicts of callables:

  • env.filters["money"] = format_money makes {{ price|money }} call format_money(price). Extra filter arguments become extra function arguments: {{ price|money("EUR") }} calls format_money(price, "EUR").
  • env.tests["prime"] = is_prime enables {% if n is prime %}.
  • env.globals["now"] = datetime.now makes {{ now() }} available in every template; also good for config values and helper functions.

Register them before loading templates, since filters are looked up when a template is compiled; an unknown filter is a TemplateAssertionError at compile time, not a runtime blank.

Sometimes a filter needs more than its value. Decorators from jinja2 pass extra first arguments:

  • @pass_context: the active Context, giving access to template variables, for example the current locale
  • @pass_eval_context: the evaluation context, mainly to check eval_ctx.autoescape
  • @pass_environment: the Environment

A filter that returns HTML should return Markup, escaping its inputs, so it works correctly with autoescaping on. In Flask, register filters with @app.template_filter() and globals with @app.template_global() or a context processor.

Keep filters pure and fast: they run for every value, on every render.

Example

from decimal import Decimal
from jinja2 import Environment, pass_context
from markupsafe import Markup, escape

def money(value, currency="USD"):
    return f"{Decimal(value):,.2f} {currency}"

@pass_context
def greet(ctx, name):                 # can read template variables
    return f"{ctx.get('greeting', 'Hello')}, {name}"

def badge(text):
    return Markup('<span class="badge">%s</span>') % escape(text)

env = Environment(autoescape=True)
env.filters.update(money=money, greet=greet, badge=badge)
env.tests["even_len"] = lambda s: len(s) % 2 == 0
env.globals["site"] = "Shop"

t = env.from_string("{{ site }}: {{ 1234.5|money('EUR') }} | {{ 'Ann'|greet }} | "
                    "{{ '<b>new</b>'|badge }} | {{ 'abcd' is even_len }}")
print(t.render(greeting="Hi"))
# Shop: 1,234.50 EUR | Hi, Ann | <span class="badge">&lt;b&gt;new&lt;/b&gt;</span> | True

Remember

  • env.filters, env.tests and env.globals are dicts you can extend
  • Extra filter arguments map to extra function parameters
  • @pass_context gives a filter access to template variables
  • Return Markup from filters that produce HTML, escaping inputs

Watch out for

  • Registering a filter after templates using it were compiled, causing "No filter named" errors.

Check yourself

  1. You register env.filters["slug"] = slugify. What does {{ title|slug("_") }} call?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: jinja.palletsprojects.com/en/stable/api/#custom-filters