# Jinja Templating in Python: A Practical Guide

[Jinja](https://jinja.palletsprojects.com/) (specifically Jinja2, its current
major version) was created by Armin Ronacher, the same developer behind the
Flask web framework.

It draws inspiration from Django's templating system but offers more flexibility
and powerful features. The name "Jinja" comes from a Japanese shrine, reflecting
the elegant and simple design philosophy behind the engine.

Today, Jinja is widely used across the Python ecosystem, particularly in:

- Web frameworks like Flask and Django.
- Configuration management tools like [Ansible](https://betterstack.com/community/guides/linux/ansible-getting-started/) and
  SaltStack.
- Static site generators like Pelican.
- Documentation generators like Sphinx.
- Email templating systems.
- Report generation tools.

What makes Jinja particularly popular is its balance of simplicity and power. It
provides a straightforward syntax for basic operations while offering advanced
features for complex requirements.

Additionally, Jinja is designed with security in mind, protecting against common
vulnerabilities like cross-site scripting (XSS) attacks through automatic
escaping.

This guide will walk you through everything you need to know about Jinja, from
basic setup to advanced usage patterns, with practical examples along the way.

[ad-logs]

## Setting up Jinja

Before diving into Jinja's features, let's get everything set up properly. Jinja
is a Python package, so you'll need Python installed on your system.

The simplest way to install Jinja is via pip, Python's package manager:

```command
pip install Jinja2
```

This will install the latest stable version of Jinja2. If you're using a virtual
environment (which is recommended), make sure to activate it before running this
command.

### Basic environment setup

To use Jinja in a Python script, you need to import it and set up an
environment. Here's a minimal example:

```python
[label basic_setup.py]
from jinja2 import Environment, FileSystemLoader

# Set up the environment
# The FileSystemLoader tells Jinja where to look for template files
env = Environment(loader=FileSystemLoader('templates'))

# Load a template
template = env.get_template('hello.html')

# Render the template with some variables
output = template.render(name='World')

print(output)
```

In this example, we're creating a Jinja environment that loads templates from a
directory called `templates` relative to our script. We then load a specific
template file, provide some data to it, and render the result.

For this to work, you'll need to create a directory called `templates` and
within it, a file called `hello.html` with the following content:

```command
mkdir templates
```

```html
[label templates/hello.html]
<h1>Hello, {{ name }}!</h1>
```

When you run the script, it should output:

```html
[output]
<h1>Hello, World!</h1>
```

![Jinja Hello World!](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/60e1e7cc-f88e-4646-b8c7-109a64680200/lg1x =1442x272)

### Integration with Python applications

While the above example shows standalone usage, Jinja is often integrated into
larger applications. Many web frameworks already include Jinja or similar
templating engines.

For example, in Flask, Jinja is built-in and can be used with minimal
configuration:

```python
[label app.py]
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def hello():
   return render_template('hello.html', name='World')

if __name__ == '__main__':
   app.run(debug=True)
```

![Flash Hello world with Jinja2](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/6a726989-b1c1-4f57-c8c4-6f634ef23d00/md1x =1204x393)

### Standalone usage

You can also use Jinja without file-based templates by using the `Template`
class directly:

```python
from jinja2 import Template

# Create a template from a string
template_string = 'Hello, {{ name }}!'
template = Template(template_string)

# Render the template
output = template.render(name='World')
print(output)
```

This approach is useful for simpler applications or when templates come from a
database or other non-file sources.

## Understanding the core concepts

Now that you have Jinja set up, let's explore its core concepts and syntax.
Understanding these fundamentals will give you a solid foundation for using
Jinja effectively.

### Template basics and syntax

Jinja templates are text files with embedded expressions and statements. These
elements are enclosed in specific delimiters:

- `{{ ... }}` for expressions (variables or expressions that output a value).
- `{% ... %}` for control structures like conditionals and loops.
- `{# ... #}` for comments.

Here's a simple template demonstrating these elements:

```html
<!DOCTYPE html>
<html>
<head>
   <title>{{ page_title }}</title>
</head>
<body>
   {# This is a comment that won't appear in the output #}
   <h1>Welcome to {{ site_name }}</h1>

   {% if user %}
       <p>Hello, {{ user.name }}!</p>
   {% else %}
       <p>Hello, guest!</p>
   {% endif %}

   <h2>Our Products</h2>
   <ul>
   {% for product in products %}
       <li>{{ product.name }} - ${{ product.price }}</li>
   {% endfor %}
   </ul>
</body>
</html>
```

### Variables and expressions

Variables in Jinja templates are passed when rendering the template. They can be
simple values or complex objects. You can access attributes and items using dot
notation or square brackets:

```html
<p>{{ user.name }}</p>
<p>{{ user['name'] }}</p>
<p>{{ user.profile.email }}</p>
<p>{{ array[0] }}</p>
```

Jinja also supports expressions within the `{{ }}` delimiters, so you can write
something like:

```html
<p>{{ user.age + 5 }}</p>
<p>{{ 'Hello, ' + user.name }}</p>
<p>{{ user.is_active ? 'Active' : 'Inactive' }}</p>
```

### Control structures

Jinja provides several control structures to help you create dynamic content.
Let's look at some of the most common ones.

The `if` statement allows you to conditionally display content:

```jinja
{% if user.is_admin %}
   <p>Welcome, Administrator!</p>
{% elif user.is_authenticated %}
   <p>Welcome back, {{ user.name }}!</p>
{% else %}
   <p>Welcome, new visitor!</p>
{% endif %}
```

The `for` loop iterates over sequences like lists and dictionaries:

```jinja
<ul>
{% for item in items %}
   <li>{{ item.name }} - {{ item.description }}</li>
{% else %}
   <li>No items found.</li>
{% endfor %}
</ul>
```

The `else` block in a `for` loop is executed if the sequence is empty.

### Template inheritance and reuse

One of Jinja's most powerful features is template inheritance, which allows you
to build a base "skeleton" template that contains common elements and defines
blocks that child templates can override.

Let's create a base template:

```html
[label templates/base.html]
<!DOCTYPE html>
<html>
<head>
   <title>{% block title %}Default Title{% endblock %}</title>
   {% block extra_head %}{% endblock %}
</head>
<body>
   <header>
       <h1>My Website</h1>
       <nav>
           {% block navigation %}
           <ul>
               <li><a href="/">Home</a></li>
               <li><a href="/about">About</a></li>
               <li><a href="/contact">Contact</a></li>
           </ul>
           {% endblock %}
       </nav>
   </header>

   <main>
       {% block content %}
       <p>Default content</p>
       {% endblock %}
   </main>

   <footer>
       {% block footer %}
       <p>&copy; {{ current_year }} My Website</p>
       {% endblock %}
   </footer>
</body>
</html>
```

Now, we can create a child template that extends this base:

```text
[label templates/page.html]
{% extends "base.html" %}

{% block title %}About Us{% endblock %}

{% block content %}
<h2>About Our Company</h2>
<p>We are a leading provider of widgets and gadgets.</p>

<h3>Our Team</h3>
<ul>
   {% for member in team_members %}
   <li>{{ member.name }} - {{ member.position }}</li>
   {% endfor %}
</ul>
{% endblock %}
```

In this example, the child template inherits all the structure from the base
template but overrides specific blocks to customize content.

![Flask template inheritance demo](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/3ce4d7cf-90d9-4fdd-3cec-e60315d1e900/orig =1531x555)

### Filters and tests

Jinja provides filters and tests to modify variables and test conditions.
Filters are applied using the pipe symbol (`|`) and can transform the output of
an expression:

```jinja
<p>{{ name|upper }}</p>
<p>{{ description|truncate(100) }}</p>
<p>{{ date|date("Y-m-d") }}</p>
<p>{{ tags|join(", ") }}</p>
```

Tests check if a variable matches certain criteria and are used with the `is`
keyword:

```jinja
{% if number is divisibleby(3) %}
   <p>{{ number }} is divisible by 3.</p>
{% endif %}

{% if value is defined %}
   <p>Value: {{ value }}</p>
{% else %}
   <p>Value is not defined.</p>
{% endif %}

{% if items is iterable %}
   <ul>
   {% for item in items %}
       <li>{{ item }}</li>
   {% endfor %}
   </ul>
{% endif %}
```

### Macros and functions

Macros are similar to functions in programming languages and allow you to create
reusable template fragments.

```jinja
[label macros.html]
{% macro input_field(name, value='', type='text') %}
   <div class="form-group">
       <label for="{{ name }}">{{ name|capitalize }}</label>
       <input type="{{ type }}" id="{{ name }}" name="{{ name }}" value="{{ value }}">
   </div>
{% endmacro %}

<form method="post">
   {{ input_field('username') }}
   {{ input_field('email', type='email') }}
   {{ input_field('password', type='password') }}
   <button type="submit">Submit</button>
</form>
```

You can also import macros from other templates:

```jinja
[label form.html]
{% import 'macros.html' as forms %}

<form method="post">
   {{ forms.input_field('username') }}
   {{ forms.input_field('email', type='email') }}
   <button type="submit">Submit</button>
</form>
```

## Building a Flask application with Jinja

Now that you understand the core concepts of Jinja, let's build a practical
Flask application that demonstrates these concepts in action. We'll create a
simple app that fetches data from a free API and displays it using Jinja
templates.

First, let's set up our Flask project with the necessary dependencies:

```command
pip install Flask requests
```

Next, we'll create a basic project structure:

```command
mkdir flask_jinja_demo && cd flask_jinja_demo
```

```command
mkdir static templates
```

```command
touch app.py
```

Now, let's create our basic Flask application in `app.py`:

```python
[label app.py]
from flask import Flask, render_template, abort
import requests
import json
from datetime import datetime

app = Flask(__name__)

# API endpoint we'll use
API_URL = "https://jsonplaceholder.typicode.com"

@app.context_processor
def inject_globals():
   """Add variables to all templates."""
   return {
       'current_year': datetime.now().year,
       'app_name': 'Post Explorer'
   }

@app.route('/')
def home():
   try:
       # Fetch posts from the API
       response = requests.get(f"{API_URL}/posts")
       posts = response.json()[:10]  # Limit to first 10 posts

       return render_template('home.html', posts=posts)
   except Exception as e:
       print(f"Error: {e}")
       abort(500)

@app.route('/post/<int:post_id>')
def post_detail(post_id):
   try:
       # Fetch post details
       post_response = requests.get(f"{API_URL}/posts/{post_id}")
       post = post_response.json()

       # Fetch comments for this post
       comments_response = requests.get(f"{API_URL}/posts/{post_id}/comments")
       comments = comments_response.json()

       # Fetch author (user) details
       user_response = requests.get(f"{API_URL}/users/{post['userId']}")
       user = user_response.json()

       return render_template('post_detail.html',
                             post=post,
                             comments=comments,
                             user=user)
   except Exception as e:
       print(f"Error: {e}")
       abort(404)

@app.errorhandler(404)
def page_not_found(e):
   return render_template('404.html'), 404

@app.errorhandler(500)
def server_error(e):
   return render_template('500.html'), 500

if __name__ == '__main__':
   app.run(debug=True)
```

In this setup:

- We've created a Flask application with routes for a home page and post detail
  page.
- We're using the JSONPlaceholder API to fetch blog posts and related data.
- We've added error handlers for 404 and 500 errors.
- We're using a context processor to inject global variables into all templates.

Now, let's create our base template that will serve as the foundation for all
pages. We'll create a file called `base.html` in the `templates` folder:

```html
[label templates/base.html]
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>{% block title %}{{ app_name }}{% endblock %}</title>
   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
   <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
   {% block extra_css %}{% endblock %}
</head>
<body>
   <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
       <div class="container">
           <a class="navbar-brand" href="{{ url_for('home') }}">{{ app_name }}</a>
           <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
               <span class="navbar-toggler-icon"></span>
           </button>
           <div class="collapse navbar-collapse" id="navbarNav">
               <ul class="navbar-nav">
                   <li class="nav-item">
                       <a class="nav-link" href="{{ url_for('home') }}">Home</a>
                   </li>
               </ul>
           </div>
       </div>
   </nav>

   <div class="container mt-4">
       {% block content %}{% endblock %}
   </div>

   <footer class="footer mt-5 py-3 bg-light">
       <div class="container text-center">
           <span class="text-muted">&copy; {{ current_year }} {{ app_name }}. Powered by JSONPlaceholder API.</span>
       </div>
   </footer>

   <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
   {% block extra_js %}{% endblock %}
</body>
</html>
```

Next, let's create a simple CSS file in the static directory:

```css
[label static/css/style.css]
.post-card {
   transition: transform 0.3s ease;
   margin-bottom: 20px;
}

.post-card:hover {
   transform: translateY(-5px);
   box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}

.comment {
   border-left: 3px solid #0d6efd;
   padding-left: 15px;
   margin-bottom: 15px;
}

.user-info {
   border-radius: 5px;
   padding: 15px;
   background-color: #f8f9fa;
   margin-bottom: 20px;
}
```

Now, let's create our error page templates:

```text
[label templates/404.html]
{% extends "base.html" %}

{% block title %}Page Not Found - {{ app_name }}{% endblock %}

{% block content %}
<div class="text-center">
   <h1 class="display-1">404</h1>
   <p class="lead">Oops! The page you're looking for doesn't exist.</p>
   <a href="{{ url_for('home') }}" class="btn btn-primary">Go Home</a>
</div>
{% endblock %}
```

```text
[label templates/500.html]
{% extends "base.html" %}

{% block title %}Server Error - {{ app_name }}{% endblock %}

{% block content %}
<div class="text-center">
   <h1 class="display-1">500</h1>
   <p class="lead">Sorry, something went wrong on our end. Please try again later.</p>
   <a href="{{ url_for('home') }}" class="btn btn-primary">Go Home</a>
</div>
{% endblock %}
```

Finally, let's create our home page template, which will display a list of posts
from the API:

```html
[label templates/home.html]
{% extends "base.html" %}

{% block title %}Home - {{ app_name }}{% endblock %}

{% block content %}
<h1>Latest Posts</h1>
<p class="lead">Explore the latest content from our community</p>

<div class="row">
   {% for post in posts %}
   <div class="col-md-6">
       <div class="card post-card">
           <div class="card-body">
               <h5 class="card-title">{{ post.title|capitalize }}</h5>
               <p class="card-text">{{ post.body|truncate(100) }}</p>
               <a href="{{ url_for('post_detail', post_id=post.id) }}" class="btn btn-primary">Read More</a>
           </div>
           <div class="card-footer text-muted">
               Post #{{ post.id }}
           </div>
       </div>
   </div>
   {% else %}
   <div class="col-12">
       <div class="alert alert-info">
           No posts found. Please try again later.
       </div>
   </div>
   {% endfor %}
</div>
{% endblock %}
```

In this template:

- We're extending the `base` template.
- We're overriding the title block to include the app name (which comes from our
  context processor).
- We're iterating through the `posts` passed from our Flask route.
- We're using the `capitalize` filter to capitalize the post title.
- We're using the `truncate` filter to limit the length of the post body.
- We're using the `url_for` function to generate the URL for the post detail
  page.

You may now start the application on port 5000 by running:

```command
python app.py
```

```text
[output]
 * Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 628-170-989
```

Visiting `http://localhost:5000` in your browser will yield the following
results:

![Home page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/49f2f194-8ef9-4862-5bdf-b885a6a7b900/md2x =1785x1223)

At the moment, if you click any of the posts, you'll see a 404 page due to the
template not being set up yet:

![404 page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/56fdbd70-1aae-4e9e-939b-c12348f87700/lg2x =1785x574)

In the following section, we'll create the post layout template and learn more
about template inheritance.

## Creating the post template

Next, let's create our post detail template, which will display the details of a
single post along with comments:

```html
[label templates/post_detail.html]
{% extends "base.html" %}

{% block title %}{{ post.title|capitalize }} - {{ app_name }}{% endblock %}

{% block content %}
<nav aria-label="breadcrumb">
   <ol class="breadcrumb">
       <li class="breadcrumb-item"><a href="{{ url_for('home') }}">Home</a></li>
       <li class="breadcrumb-item active">Post #{{ post.id }}</li>
   </ol>
</nav>

<div class="row">
   <div class="col-md-8">
       <article>
           <h1>{{ post.title|capitalize }}</h1>
           <p class="lead">{{ post.body|capitalize }}</p>
       </article>

       <h3 class="mt-5">Comments ({{ comments|length }})</h3>

       {% if comments %}
           {% for comment in comments %}
           <div class="comment">
               <h5>{{ comment.name|capitalize }}</h5>
               <p class="text-muted">{{ comment.email }}</p>
               <p>{{ comment.body|capitalize }}</p>
           </div>
           {% endfor %}
       {% else %}
           <p class="alert alert-info">No comments yet.</p>
       {% endif %}
   </div>

   <div class="col-md-4">
       <div class="user-info">
           <h4>Author Information</h4>
           <p><strong>Name:</strong> {{ user.name }}</p>
           <p><strong>Username:</strong> {{ user.username }}</p>
           <p><strong>Email:</strong> {{ user.email }}</p>
           <p><strong>Website:</strong> <a href="http://{{ user.website }}" target="_blank">{{ user.website }}</a></p>
           <p><strong>Company:</strong> {{ user.company.name }}</p>
       </div>

       <div class="card mt-3">
           <div class="card-header">Post Stats</div>
           <ul class="list-group list-group-flush">
               <li class="list-group-item">Post ID: {{ post.id }}</li>
               <li class="list-group-item">User ID: {{ post.userId }}</li>
               <li class="list-group-item">Comments: {{ comments|length }}</li>
               <li class="list-group-item">Title Length: {{ post.title|length }} characters</li>
               <li class="list-group-item">Body Length: {{ post.body|length }} characters</li>
           </ul>
       </div>
   </div>
</div>
{% endblock %}

{% block extra_js %}
<script>
   document.addEventListener('DOMContentLoaded', function() {
       // Just a simple example of how you might use JavaScript with your template
       console.log('Post detail page loaded for post #{{ post.id }}');
   });
</script>
{% endblock %}
```

In this template, we're extending the base template to maintain a consistent
layout across our application. We've implemented more complex control structures
by utilizing nested conditionals and loops to display the post content and
comments dynamically based on available data.

Throughout the template, we access various properties of the `post`, `user`, and
`comments` objects to display the relevant information in appropriate places.
We've enhanced the presentation by applying filters like `capitalize` to format
text properly and `length` to calculate and display metadata about the content.

Additionally, we've included some JavaScript functionality within the `extra_js`
block that was defined in our base template, demonstrating how to incorporate
client-side functionality specific to this page.

When you visit a `/post` route now, each post will now render correctly.

![Post page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/d51fa238-4c84-4a8c-fce2-1287f8350900/md2x =1789x1495)

## Creating reusable components with macros

Now let's create some reusable components using macros. We'll create a file
called `macros.html` in the templates folder:

```html
[label templates/macros.html]
{% macro post_card(post, show_body=true) %}
<div class="card post-card h-100">
   <div class="card-body">
       <h5 class="card-title">{{ post.title|capitalize }}</h5>
       {% if show_body %}
       <p class="card-text">{{ post.body|truncate(100) }}</p>
       {% endif %}
       <a href="{{ url_for('post_detail', post_id=post.id) }}" class="btn btn-primary">Read More</a>
   </div>
   <div class="card-footer text-muted">
       Post #{{ post.id }}
   </div>
</div>
{% endmacro %}
```

The `post_card` macro serves as a reusable component that displays a post in a
consistent card format throughout our application.

It takes a `post` object and an optional `show_body` parameter that determines
whether to display the post's content. For example, you might show the body on
the home page but hide it in search results or related posts sections.

Go ahead and update our home page to use the post card macro:

```text
[label templates/home.html]
{% extends "base.html" %}
[highlight]
{% from "macros.html" import post_card %}
[/highlight]

{% block title %}Home - {{ app_name }}{% endblock %}

{% block content %}
<h1>Latest Posts</h1>
<p class="lead">Explore the latest content from our community</p>

<div class="row">
   {% for post in posts %}
   <div class="col-md-6">
[highlight]
    {{ post_card(post) }}
[/highlight]
   </div>
   {% else %}
   <div class="col-12">
       <div class="alert alert-info">
           No posts found. Please try again later.
       </div>
   </div>
   {% endfor %}
</div>
{% endblock %}
```

There will be no change to how the home page is rendered visually, but your
templates will be more modular and easier to maintain.

In this manner, you can reuse components across multiple templates without
duplicating code.

## Using filters and tests

Now let's enhance our application by creating custom filters and tests. We'll
add these to our `app.py` file:

```python
[label app.py]
. . .
# Custom filters
@app.template_filter('readtime')
def readtime_filter(text):
   """Estimates reading time for text content."""
   words_per_minute = 200
   word_count = len(text.split())
   minutes = max(1, round(word_count / words_per_minute))
   return f"{minutes} min read"

# Custom tests
@app.template_test('popular')
def is_popular(comments):
   """Test if a post is popular based on comment count."""
   return len(comments) >= 3
```

This code extends Jinja's functionality by adding custom template utilities:

- A custom filter called `readtime` that calculates estimated reading time for
   text by:

   - Assuming a reading speed of 200 words per minute.
   - Counting words in the provided text.
   - Calculating and rounding minutes needed to read.
   - Returning a formatted string like "3 min read".
   - Ensuring at least 1 minute is shown even for very short content.

- A custom test called `popular` that determines if content is popular by:

   - Checking if a post has 3 or more comments.
   - Returning `True` if it meets this threshold, `False` otherwise.

These extensions can be used in templates like `{{ post.body|readtime }}` and
`{% if comments is popular %}Popular!{% endif %}`, enhancing template
expressiveness without cluttering presentation logic.

You can see them in action by modifying your `post_detail.html` file as follows:

```html
[label post_detail.html]
{% extends "base.html" %}

{% block title %}{{ post.title|capitalize }} - {{ app_name }}{% endblock %}

{% block content %}
<nav aria-label="breadcrumb">
   <ol class="breadcrumb">
       <li class="breadcrumb-item"><a href="{{ url_for('home') }}">Home</a></li>
       <li class="breadcrumb-item active">Post #{{ post.id }}</li>
   </ol>
</nav>

<div class="row">
   <div class="col-md-8">
       <article>
           <h1>{{ post.title|capitalize }}</h1>
[highlight]
           <span class="badge bg-secondary">{{ post.body|readtime }}</span>
[/highlight]
           <p class="lead">{{ post.body|capitalize }}</p>
       </article>

[highlight]
      <h3 class="mt-5">
        Comments ({{ comments|length }})
        {% if comments is popular %}
        <span class="badge bg-success">Popular</span>
        {% endif %}
      </h3>
[/highlight]

. . .
```

Once you reload a post, you'll see the read time and "popular" tag where
appropriate:

![Post using filters and tests](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/c56a39b8-aef3-4a4d-03f5-bf9195918600/orig =1788x777)

## Final thoughts

Jinja templating is a powerful tool that strikes a balance between simplicity
and functionality. It allows you to separate your presentation logic from your
business logic, making your code more maintainable and your templates more
reusable.

In this guide, we've explored Jinja's core concepts and built a practical Flask
application that demonstrates these concepts in action.

Thanks for reading!
