What is Jinja?

Introduction

Jinja is the most popular template engine for Python projects and is used in projects like Flask, Django, and Ansible; it has recently gained popularity also for interaction with databases in combination with SQL, partly thanks to its use in popular tools such as dbt.
A template engine is a software that generates dynamic content by replacing special placeholders in a template with actual data when run.

Using a template you can separate the structure of your document from its varying parts (input data): this means that you can reuse the same structure without restarting from scratch.
For example, a marketing team could create a template for a promotional email that can be customized with different products or messages depending on the audience.

Here at PushMetrics, we see Jinja as a good fit to create automation workflows, so we made it a first-class citizen of our product by:

  • enabling you to use (and preview!) Jinja templates in SQL, Email, and Slack blocks
  • adding logic blocks where you can provide input parameters and write conditions/loops to drive the behavior of your workflow according to your data

Using these tools you can easily create workflows such as KPI Alerts or sending alerts to Slack.

Template syntax


Hello {{ name }}!

The above example introduces the first element in Jinja's template syntax: variables. Variables are identifiers surrounded by curly braces, as in {{ variable_name }}. You can think of them as placeholders: when you render a template, they are replaced with an actual value.

Next to variables, Jinja provides control structures to make the template more dynamic.

Below is a minimal template that illustrates their usage:


Hello {{ name }},

{% if message %}
  There is a new message for you: "{{ message }}".
{% else %}
  There is no new message for you.
{% endif %}

Rendering this template will replace {{ name }} with the value of the corresponding variable (e.g. Jane) and will then evaluate the if statement (the control structure): if the message variable contains a non-empty value (e.g. Hey, how are you?), it will output:


Hello Jane,

There is a new message for you: "Hey, how are you?".

otherwise, it will output:


Hello Jane,

There is no new message for you.

Jinja templates use the following delimiters:

  • {{ ... }} for expressions and variables (such as message in the above example)
  • {% ... %} for statements (such as if/else/endif)
  • {%- ... -%} same as the above, but stripping whitespaces before and/or after the block
  • {# ... #} for comments

Key features

Jinja is particularly powerful because it offers a lot of flexibility while also being easy to use. A shortlist of its key features includes:

  • control structures
  • filters
  • tests
  • macros
  • template inheritance

Below is a quick overview of the aforementioned features: follow-up articles will describe them more in detail.

Control structures

Jinja provides a set of control structures that allow you to conditionally display content, loop through data, and more.

Conditionals

Conditionals use the if/elif/else syntax, as in the following example:


{% if user.age < 10 %}
  Child
{% elif user.age < 18 %}
  Teenager
{% else %}
  Adult
{% endif %}

Note: both the elif and else branches are optional. There can be multiple elif branches, as in the above example, but at most one else branch.

Conditionals must always end with an endif statement. For more details, see the Jinja conditionals article.

Tests

Jinja also offers a builtin set of tests, that can be used to test a variable against an expression. The expression may check the value of the variable or its type.

Tests are written in the following form:


{% if variable is test_name %}
  # ...
{% endif %}

Here is an example:


{% if user.age is odd %}
  # ...
{% endif %}

where is is a keyword and odd is a builtin test provided by Jinja.

This test will look at the value of user.age and check whether it is an odd number of not.

Here is another example:


{% if user.address is defined %}
  # ...
{% endif %}

This test checks if the user has an attribute called address and, if so, if that attribute has a non-empty value.

A more in-depth explanation of tests is at the end of the Jinja conditionals article.

Loops

Loops use the for control structure, optionally followed by an else branch as in the following example, to perform some action if the input list was empty:


{% for user in users %}
  {{ user.username }}
{% else %}
  No users found
{% endfor %}

If the users list contains any users, Jinja will output each user's username; if not, it will output No users found.

Loops must always end with an endfor statement.

Loops will be explained more in detail in the Jinja Loops article.

Filters

Filters are a way to perform basic transformations to variables before they are rendered. You add them to variables using a pipe (|) character followed by the filter name and any arguments, if required. There are many built-in filters provided by Jinja and it's also possible to create custom filters.

Below are a few simple examples:

  • Capitalizing the first letter of a string

{{ "hello world"|capitalize }}

output:


Hello world
  • Sorting elements in a list in reverse order

{% for num in [ 42, 99, 7 ]|sort(reverse=true) %}
  {{ num }}
{% endfor %}

output:


99
42
7
  • Getting the maximum value in a list of numbers

{{ [ 42, 99, 7 ]|max }}

output:


99

It's important to note that multiple filters can be used in sequence. For example:


{{ [ 42.1, 99.9, 7.5 ]|max|round }}

will select the maximum value (99.9) and then round it up, producing 100.0 as output.

The Jinja Filters article provides an overview (with examples) of several built-in filters.

Macros

You can use macros to define custom functions that can then be called multiple times within the template, reducing code duplication and improving maintainability.

You define macros using the macro keyword followed by its definition. A macro can take arguments and return values, just like a regular function in programming languages. You can then call a macro in a template by using its name (e.g. my_macro(...)); you can also call a macro from other macros using the call keyword and the name of the macro (e.g. call my_macro(...)).

As an example, the following macro generates a WHERE clause for an SQL query based on a dictionary of filter criteria, returning a string containing the WHERE clause.


{% macro where_clause(filters) -%}
  {% set ns = namespace(clauses = []) -%}
  {% for field, value in filters.items() -%}
    {% set clause = field + " = " + value -%}
    {% set ns.clauses = ns.clauses + [clause] -%}
  {% endfor -%}
  WHERE {{ " AND ".join(ns.clauses) }}
{%- endmacro -%}

SELECT * FROM users {{ where_clause({"name": "John", "age": "25"}) }};

Rendering this template would generate the following SQL query:


SELECT * FROM users WHERE name = John AND age = 25;

Note: the above snippet makes use of the namespace object, that is only available in Jinja 2.10+.

Macros are explained in more detail in the Jinja Macros article.

Template inheritance

Using template inheritance you can create a base template with common content and structure, and then inherit from it in child templates to add specific content or variations. This can save you a lot of time and effort in content generation, as the base template only needs to be defined once.

This is an example of parent template (note the use of block/endblock to declare named parts of the template where children can inject their own content):


# parent.txt

WITH customer_names AS (
{% block cte %}{% endblock %}
)
SELECT *
FROM customer_names;

and this is an example of template extending the above one and defining some content for the cte block declared by the parent:


# child.txt

{% extends 'parent.txt' %}
{% block cte %}
SELECT first_name, last_name
FROM customers
{% endblock %}

when you render child.txt, the output will look like this:


WITH customer_names AS (
SELECT first_name, last_name
FROM customers
)
SELECT *
FROM customer_names;

For more details, see the Template Inheritance article.