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 asmessage
in the above example){% ... %}
for statements (such asif/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.