Jinja (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 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.
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:
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:
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:
mkdir templates
<h1>Hello, {{ name }}!</h1>
When you run the script, it should output:
<h1>Hello, World!</h1>
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:
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)
Standalone usage
You can also use Jinja without file-based templates by using the Template
class directly:
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:
<!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:
<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:
<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:
{% 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:
<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:
<!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>© {{ current_year }} My Website</p>
{% endblock %}
</footer>
</body>
</html>
Now, we can create a child template that extends this base:
{% 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.
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:
<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:
{% 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.
{% 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:
{% 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:
pip install Flask requests
Next, we'll create a basic project structure:
mkdir flask_jinja_demo && cd flask_jinja_demo
mkdir static templates
touch app.py
Now, let's create our basic Flask application in 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:
<!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">© {{ 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:
.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:
{% 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 %}
{% 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:
{% 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:
python app.py
* 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:
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:
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:
{% 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.
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:
{% 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:
{% extends "base.html" %}
{% from "macros.html" import post_card %}
{% 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">
{{ post_card(post) }}
</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:
. . .
# 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:
{% 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>
<span class="badge bg-secondary">{{ post.body|readtime }}</span>
<p class="lead">{{ post.body|capitalize }}</p>
</article>
<h3 class="mt-5">
Comments ({{ comments|length }})
{% if comments is popular %}
<span class="badge bg-success">Popular</span>
{% endif %}
</h3>
. . .
Once you reload a post, you'll see the read time and "popular" tag where appropriate:
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!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github