Introduction to Jinja2
Jinja2 is a modern and designer-friendly templating language for Python, modeled after Django's template system but with added flexibility and power. As Flask's default templating engine, Jinja2 allows you to generate dynamic HTML by combining static template files with data provided by your application.
"Templates are not just about replacing variables with their values; they're about expressing your application's interface in a way that separates logic from presentation."
Why Use Templates?
Templates provide several benefits in web application development:
- Separation of Concerns: Keep Python code separate from HTML markup
- Code Reusability: Reuse layout elements across multiple pages
- Maintainability: Easier to maintain and update the UI independently
- Designer Friendly: Allow designers to work with templates without knowing Python
- Security: Automatic escaping helps prevent XSS vulnerabilities
Flask Template Basics
Template Organization
By default, Flask looks for templates in a folder named templates in your application directory:
myapp/
├── app.py
├── static/
│ └── css/
│ └── style.css
└── templates/
├── base.html
├── home.html
├── about.html
└── contact.html
Rendering Templates
To render a template from a Flask route, use the render_template() function:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('home.html', title='Home Page')
@app.route('/about')
def about():
return render_template('about.html', title='About Us')
@app.route('/user/')
def profile(username):
user = get_user(username) # Get user data from database
return render_template('profile.html', user=user, title=f"{user.name}'s Profile")
The render_template() function takes:
- The name of the template file as the first argument
- Any number of keyword arguments that are passed to the template as variables
Basic Template Syntax
A simple Jinja2 template might look like this:
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>Hello, {{ name }}!</h1>
{% if user.is_admin %}
<p>You have admin privileges.</p>
{% else %}
<p>Welcome to our site.</p>
{% endif %}
<h2>Your Skills:</h2>
<ul>
{% for skill in user.skills %}
<li>{{ skill }}</li>
{% endfor %}
</ul>
</body>
</html>
Jinja2 uses three types of delimiters:
{{ ... }}for expressions (variables, functions, etc.){% ... %}for statements (control structures like if/for){# ... #}for comments (not visible in the rendered output)
Auto-Escaping in Jinja2
By default, Jinja2 automatically escapes all variables to prevent XSS attacks. HTML special characters like <, >, & are converted to their HTML entities.
To output raw HTML (use with caution), you can use the |safe filter:
<div>{{ html_content|safe }}</div>
Jinja2 Variables and Expressions
Variable Access
You can access variables in templates in several ways:
{{ foo }}
{{ foo.bar }}
{{ foo['bar'] }}
{{ foo.method() }}
Filters
Filters modify variables and are applied with a pipe (|) symbol:
{{ name|upper }}
{{ name|lower }}
{{ name|title }}
{{ name|trim }}
{{ name|striptags }}
{{ list|join(', ') }}
{{ number|round(2) }}
{{ date|strftime('%Y-%m-%d') }}
{{ text|truncate(100) }}
{{ text|escape }}
{{ html|safe }}
{{ none|default('N/A') }}
Tests
Tests check if a variable meets certain conditions:
{% if name is defined %}
{% if name is none %}
{% if name is string %}
{% if value is divisibleby(3) %}
{% if list is empty %}
{% if user is admin %}
Math Operations
Mathematical operations can be performed directly in templates:
{{ a + b }}
{{ a - b }}
{{ a * b }}
{{ a / b }}
{{ a // b }}
{{ a % b }}
{{ a ** b }}
Comparisons and Logic
Comparison and logical operators work as expected:
{% if a > b %}
{% if a >= b %}
{% if a < b %}
{% if a <= b %}
{% if a == b %}
{% if a != b %}
{% if a and b %}
{% if a or b %}
{% if not a %}
Real-World Example: User Profile Template
Here's a more complete example of a user profile page using Jinja2:
<!DOCTYPE html>
<html>
<head>
<title>{{ user.name }}'s Profile</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header>
<h1>
{{ user.name }}
{% if user.is_premium %}
<span class="badge">Premium</span>
{% endif %}
</h1>
<p>Member since {{ user.join_date|strftime('%B %d, %Y') }}</p>
</header>
<section class="profile-details">
<div class="avatar">
{% if user.avatar %}
<img src="{{ user.avatar }}" alt="{{ user.name }}">
{% else %}
<img src="{{ url_for('static', filename='img/default-avatar.png') }}" alt="Default Avatar">
{% endif %}
</div>
<div class="bio">
{% if user.bio %}
{{ user.bio|urlize }}
{% else %}
<p>No bio provided.</p>
{% endif %}
</div>
<div class="stats">
<p>Posts: {{ user.post_count }}</p>
<p>Comments: {{ user.comment_count }}</p>
<p>Reputation: {{ user.reputation|default(0) }}</p>
</div>
</section>
{% if user.posts %}
<section class="recent-posts">
<h2>Recent Posts</h2>
<ul>
{% for post in user.posts[:5] %}
<li>
<a href="{{ url_for('post', post_id=post.id) }}">{{ post.title }}</a>
<span class="date">{{ post.date|timesince }}</span>
</li>
{% endfor %}
</ul>
{% if user.posts|length > 5 %}
<a href="{{ url_for('user_posts', username=user.username) }}">View all {{ user.posts|length }} posts</a>
{% endif %}
</section>
{% else %}
<p>This user hasn't published any posts yet.</p>
{% endif %}
<footer>
{% if current_user and current_user.id == user.id %}
<a href="{{ url_for('edit_profile') }}" class="button">Edit Profile</a>
{% elif current_user %}
{% if user in current_user.following %}
<form action="{{ url_for('unfollow', username=user.username) }}" method="post">
<button type="submit">Unfollow</button>
</form>
{% else %}
<form action="{{ url_for('follow', username=user.username) }}" method="post">
<button type="submit">Follow</button>
</form>
{% endif %}
{% endif %}
</footer>
</body>
</html>
Control Structures
Conditionals
Conditional statements allow you to render different content based on conditions:
{% if condition %}
<p>This is rendered if condition is true</p>
{% elif another_condition %}
<p>This is rendered if the first condition is false but another_condition is true</p>
{% else %}
<p>This is rendered if both conditions are false</p>
{% endif %}
Loops
Loops allow you to iterate over collections:
{% for item in items %}
<p>{{ item.name }}: {{ item.description }}</p>
{% endfor %}
{# Loop with index #}
{% for item in items %}
<p>{{ loop.index }}. {{ item }}</p>
{% endfor %}
{# Using loop variables #}
{% for item in items %}
{% if loop.first %}<ul>{% endif %}
<li class="{% if loop.odd %}odd{% else %}even{% endif %}">
{{ item }}
{% if not loop.last %} | {% endif %}
</li>
{% if loop.last %}</ul>{% endif %}
{% endfor %}
{# Empty list handling #}
{% for item in items %}
<p>{{ item }}</p>
{% else %}
<p>No items found.</p>
{% endfor %}
Available loop variables:
loop.index: The current iteration (1-indexed)loop.index0: The current iteration (0-indexed)loop.revindex: The number of iterations from the end (1-indexed)loop.revindex0: The number of iterations from the end (0-indexed)loop.first: True if this is the first iterationloop.last: True if this is the last iterationloop.length: The total number of itemsloop.cycle: A helper to cycle through valuesloop.odd: True if the current iteration is oddloop.even: True if the current iteration is evenloop.depth: The nesting level for nested loopsloop.depth0: The nesting level (0-indexed)loop.previtem: The item from the previous iterationloop.nextitem: The item from the next iteration
Assignments
You can set variables within templates:
{% set name = 'John' %}
<p>Hello, {{ name }}!</p>
{% set total = 0 %}
{% for item in cart %}
{% set total = total + item.price %}
{% endfor %}
<p>Total: ${{ total }}</p>
Blocks
Block statements can be used for template inheritance (more on this later):
{% block content %}
<p>Default content</p>
{% endblock %}
Macros
Macros are reusable pieces of template code, similar to functions:
{% macro input(name, value='', type='text', label='') %}
<div class="form-group">
{% if label %}
<label for="{{ name }}">{{ label }}</label>
{% endif %}
<input type="{{ type }}" name="{{ name }}" id="{{ name }}" value="{{ value }}">
</div>
{% endmacro %}
{# Using the macro #}
{{ input('username', label='Username') }}
{{ input('password', type='password', label='Password') }}
{{ input('remember', type='checkbox', label='Remember me') }}
Whitespace Control
Jinja2 preserves whitespace by default. To control whitespace in the output, you can use:
{%- ... %}- Removes whitespace before the block{% ... -%}- Removes whitespace after the block{{- ... }}- Removes whitespace before the expression{{ ... -}}- Removes whitespace after the expression
{% for item in items -%}
{{ item }}
{%- endfor %}
Template Inheritance
Template inheritance is one of Jinja2's most powerful features. It allows you to build a base "skeleton" template that contains common elements of your site and defines blocks that child templates can override.
Base Template
First, create a base template with common elements and defined blocks:
<!DOCTYPE html>
<html>
<head>
{% block head %}
<title>{% block title %}Default Title{% endblock %} - My Website</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% endblock %}
</head>
<body>
<header>
{% block header %}
<nav>
<ul>
<li><a href="{{ url_for('home') }}">Home</a></li>
<li><a href="{{ url_for('about') }}">About</a></li>
<li><a href="{{ url_for('contact') }}">Contact</a></li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('profile') }}">Profile</a></li>
<li><a href="{{ url_for('logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('login') }}">Login</a></li>
<li><a href="{{ url_for('register') }}">Register</a></li>
{% endif %}
</ul>
</nav>
{% endblock %}
</header>
<main>
{% block content %}
{# Main content goes here #}
{% endblock %}
</main>
<footer>
{% block footer %}
<p>© {{ current_year }} My Website. All rights reserved.</p>
{% endblock %}
</footer>
{% block scripts %}
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% endblock %}
</body>
</html>
Child Templates
Then, create child templates that extend the base template and override specific blocks:
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Welcome to My Website</h1>
<p>This is the home page.</p>
<div class="featured-content">
<h2>Featured Articles</h2>
<ul>
{% for article in featured_articles %}
<li>
<a href="{{ url_for('article', article_id=article.id) }}">{{ article.title }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}
{% extends "base.html" %}
{% block title %}About Us{% endblock %}
{% block content %}
<h1>About Us</h1>
<p>We are a company dedicated to...</p>
<h2>Our Team</h2>
<div class="team-members">
{% for member in team_members %}
<div class="member">
<img src="{{ member.photo }}" alt="{{ member.name }}">
<h3>{{ member.name }}</h3>
<p>{{ member.position }}</p>
<p>{{ member.bio }}</p>
</div>
{% endfor %}
</div>
{% endblock %}
{% block scripts %}
{{ super() }} {# Include parent block content #}
<script src="{{ url_for('static', filename='js/about.js') }}"></script>
{% endblock %}
Nested Inheritance
You can create multiple levels of inheritance for more complex layouts:
{% extends "base.html" %}
{% block title %}Admin Panel{% endblock %}
{% block header %}
{{ super() }}
<div class="admin-banner">Admin Panel</div>
{% endblock %}
{% block content %}
<div class="admin-container">
<div class="admin-sidebar">
<ul>
<li><a href="{{ url_for('admin.dashboard') }}">Dashboard</a></li>
<li><a href="{{ url_for('admin.users') }}">Users</a></li>
<li><a href="{{ url_for('admin.posts') }}">Posts</a></li>
<li><a href="{{ url_for('admin.settings') }}">Settings</a></li>
</ul>
</div>
<div class="admin-content">
{% block admin_content %}{% endblock %}
</div>
</div>
{% endblock %}
{% extends "admin/base.html" %}
{% block title %}Admin - Users{% endblock %}
{% block admin_content %}
<h1>User Management</h1>
<table class="user-table">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
<td>{{ user.created_at|strftime('%Y-%m-%d') }}</td>
<td>
<a href="{{ url_for('admin.edit_user', user_id=user.id) }}" class="btn btn-edit">Edit</a>
<a href="{{ url_for('admin.delete_user', user_id=user.id) }}" class="btn btn-delete" data-confirm="Are you sure?">Delete</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
Template Includes
For reusing smaller template fragments, you can use the include statement:
Creating Reusable Fragments
{% macro render_pagination(pagination, endpoint) %}
<div class="pagination">
{% if pagination.has_prev %}
<a href="{{ url_for(endpoint, page=pagination.prev_num) }}">« Previous</a>
{% else %}
<span class="disabled">« Previous</span>
{% endif %}
{% for page in pagination.iter_pages() %}
{% if page %}
{% if page != pagination.page %}
<a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
{% else %}
<span class="current">{{ page }}</span>
{% endif %}
{% else %}
<span class="ellipsis">…</span>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<a href="{{ url_for(endpoint, page=pagination.next_num) }}">Next »</a>
{% else %}
<span class="disabled">Next »</span>
{% endif %}
</div>
{% endmacro %}
{% if messages %}
<div class="flash-messages">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
<button type="button" class="close" data-dismiss="alert">×</button>
</div>
{% endfor %}
</div>
{% endif %}
Including Template Fragments
{% extends "base.html" %}
{% block content %}
<h1>Blog Posts</h1>
{% include "partials/flash_messages.html" %}
<div class="posts-list">
{% for post in posts.items %}
<article class="post">
<h2><a href="{{ url_for('post', post_id=post.id) }}">{{ post.title }}</a></h2>
<div class="meta">
By {{ post.author.name }} on {{ post.created_at|strftime('%B %d, %Y') }}
</div>
<div class="summary">
{{ post.content|truncate(200) }}
<a href="{{ url_for('post', post_id=post.id) }}">Read more</a>
</div>
</article>
{% endfor %}
</div>
{% from "partials/pagination.html" import render_pagination %}
{{ render_pagination(posts, 'posts') }}
{% endblock %}
Include vs. Import
Jinja2 offers two ways to reuse template code:
{% include "template.html" %}- Includes the content of another template directly{% import "macros.html" as macros %}- Imports macros from another template{% from "macros.html" import render_field %}- Imports specific macros
Use include for HTML fragments and import for reusable macros.
Custom Filters and Tests
Flask allows you to create custom Jinja2 filters and tests to extend the template engine's capabilities.
Custom Filters
# app.py
@app.template_filter('strftime')
def _jinja2_filter_datetime(date, fmt=None):
if fmt is None:
fmt = '%Y-%m-%d'
return date.strftime(fmt)
@app.template_filter('timesince')
def _jinja2_filter_timesince(dt, default="just now"):
"""
Returns string representing "time since" e.g. "3 days ago", "5 minutes ago"
"""
now = datetime.utcnow()
diff = now - dt
periods = (
(diff.days / 365, "year", "years"),
(diff.days / 30, "month", "months"),
(diff.days / 7, "week", "weeks"),
(diff.days, "day", "days"),
(diff.seconds / 3600, "hour", "hours"),
(diff.seconds / 60, "minute", "minutes"),
(diff.seconds, "second", "seconds"),
)
for period, singular, plural in periods:
if int(period) > 0:
return f"{int(period)} {singular if int(period) == 1 else plural} ago"
return default
Custom Tests
# app.py
@app.template_test('admin')
def _jinja2_test_admin(user):
return user.role == 'admin'
@app.template_test('image')
def _jinja2_test_image(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif'}
Using Custom Filters and Tests
<p>Posted {{ post.created_at|timesince }}</p>
{% if user is admin %}
<div class="admin-controls">
<a href="{{ url_for('admin.dashboard') }}">Admin Dashboard</a>
</div>
{% endif %}
{% if filename is image %}
<img src="{{ url_for('static', filename='uploads/' + filename) }}" alt="Image">
{% else %}
<a href="{{ url_for('static', filename='uploads/' + filename) }}">Download {{ filename }}</a>
{% endif %}
Real-World Example: Blog Template with Custom Filters
Here's a more complex blog post template using custom filters and tests:
{% extends "base.html" %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
<article class="blog-post">
<header>
<h1>{{ post.title }}</h1>
<div class="meta">
By <a href="{{ url_for('author', username=post.author.username) }}">{{ post.author.name }}</a>
on {{ post.created_at|strftime('%B %d, %Y') }}
{% if post.created_at != post.updated_at %}
(Updated {{ post.updated_at|timesince }})
{% endif %}
{% if post.tags %}
| Tags:
{% for tag in post.tags %}
<a href="{{ url_for('tag', tag=tag) }}" class="tag">{{ tag }}</a>
{%- if not loop.last %}, {% endif %}
{% endfor %}
{% endif %}
</div>
</header>
{% if post.featured_image %}
<div class="featured-image">
<img src="{{ post.featured_image }}" alt="{{ post.title }}">
{% if post.image_caption %}
<figcaption>{{ post.image_caption }}</figcaption>
{% endif %}
</div>
{% endif %}
<div class="content">
{% if post.format == 'markdown' %}
{{ post.content|markdown|safe }}
{% else %}
{{ post.content|safe }}
{% endif %}
</div>
<footer>
<div class="share">
<h3>Share this post</h3>
<a href="{{ post|twitter_share_url }}" class="btn btn-twitter" target="_blank">Twitter</a>
<a href="{{ post|facebook_share_url }}" class="btn btn-facebook" target="_blank">Facebook</a>
<a href="{{ post|linkedin_share_url }}" class="btn btn-linkedin" target="_blank">LinkedIn</a>
</div>
{% if current_user is admin or current_user.id == post.author.id %}
<div class="admin-controls">
<a href="{{ url_for('edit_post', post_id=post.id) }}" class="btn btn-edit">Edit</a>
<a href="{{ url_for('delete_post', post_id=post.id) }}" class="btn btn-delete" data-confirm="Are you sure you want to delete this post?">Delete</a>
</div>
{% endif %}
<div class="author-bio">
<h3>About the Author</h3>
<div class="author-details">
<img src="{{ post.author.avatar|default(url_for('static', filename='img/default-avatar.png')) }}" alt="{{ post.author.name }}">
<div>
<h4>{{ post.author.name }}</h4>
<p>{{ post.author.bio|default('No bio available.')|truncate(150) }}</p>
<a href="{{ url_for('author', username=post.author.username) }}">View all posts</a>
</div>
</div>
</div>
<div class="related-posts">
<h3>Related Posts</h3>
<ul>
{% for related_post in related_posts %}
<li>
<a href="{{ url_for('post', post_id=related_post.id) }}">{{ related_post.title }}</a>
({{ related_post.created_at|timesince }})
</li>
{% else %}
<li>No related posts found.</li>
{% endfor %}
</ul>
</div>
</footer>
</article>
<section class="comments">
<h2>{{ post.comments|length }} Comment{% if post.comments|length != 1 %}s{% endif %}</h2>
{% if current_user.is_authenticated %}
<div class="comment-form">
<h3>Add a Comment</h3>
<form action="{{ url_for('add_comment', post_id=post.id) }}" method="post">
{{ comment_form.csrf_token }}
<div class="form-group">
{{ comment_form.content.label }}
{{ comment_form.content(class="form-control") }}
{% if comment_form.content.errors %}
{% for error in comment_form.content.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
{% endif %}
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
{% else %}
<p><a href="{{ url_for('login', next=request.path) }}">Log in</a> to add a comment.</p>
{% endif %}
<div class="comments-list">
{% for comment in post.comments|sort(attribute='created_at', reverse=true) %}
<div class="comment" id="comment-{{ comment.id }}">
<div class="comment-header">
<img src="{{ comment.author.avatar|default(url_for('static', filename='img/default-avatar.png')) }}" alt="{{ comment.author.name }}" class="avatar">
<span class="name">{{ comment.author.name }}</span>
<span class="date">{{ comment.created_at|timesince }}</span>
</div>
<div class="comment-content">
{{ comment.content|urlize|nl2br }}
</div>
{% if current_user.is_authenticated and (current_user.id == comment.author.id or current_user is admin) %}
<div class="comment-actions">
<a href="{{ url_for('edit_comment', comment_id=comment.id) }}" class="btn-edit">Edit</a>
<a href="{{ url_for('delete_comment', comment_id=comment.id) }}" class="btn-delete" data-confirm="Are you sure?">Delete</a>
</div>
{% endif %}
</div>
{% else %}
<p>No comments yet. Be the first to comment!</p>
{% endfor %}
</div>
</section>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/comments.js') }}"></script>
{% endblock %}
Practical Exercise
Build a Blog Template System
Create a template system for a blog application using Jinja2:
Requirements
- Create a base template with common elements (header, footer, navigation)
- Create templates for the home page, blog post list, single post view, and about page
- Implement template inheritance
- Create reusable components (pagination, post preview, comments section)
- Use filters and conditional logic
Steps
- Set up a Flask application with appropriate routes
- Create a base.html template with necessary blocks
- Implement child templates that extend the base template
- Create partial templates for reusable components
- Add at least one custom filter
- Create some sample data for testing templates
Sample Data
posts = [
{
'id': 1,
'title': 'Getting Started with Flask',
'content': 'Flask is a lightweight web framework for Python...',
'author': {'name': 'John Doe', 'username': 'johndoe'},
'created_at': datetime(2025, 2, 15, 12, 30),
'tags': ['Flask', 'Python', 'Web Development']
},
{
'id': 2,
'title': 'Understanding Jinja2 Templates',
'content': 'Jinja2 is a powerful templating engine...',
'author': {'name': 'Jane Smith', 'username': 'janesmith'},
'created_at': datetime(2025, 3, 10, 15, 45),
'tags': ['Jinja2', 'Templates', 'Flask']
},
# Add more posts...
]
Bonus Challenges
- Add a search feature with a search results template
- Implement a tag cloud using custom filters
- Create an admin dashboard template
- Add a contact form template with validation feedback
Summary
Key Takeaways
- Jinja2 is Flask's default templating engine for generating dynamic HTML
- Templates help separate presentation from logic in your application
- Jinja2 supports variables, expressions, filters, and control structures
- Template inheritance allows for reusable layouts and components
- Includes and macros help reduce code duplication
- Custom filters and tests can extend Jinja2's functionality
- Template organization is important for maintainability
Additional Resources
- Jinja2 Documentation
- Flask Template Documentation
- Jinja2 Template Designer Documentation
- Flask Template Inheritance Patterns
- Jinja2 Tips and Tricks
Next Lecture
In our next lecture, we'll explore Template Inheritance and Includes in more depth, focusing on building a complete site structure with Jinja2.
Practice Activities
Basic Exercises
- Create a base template with header, footer, and navigation
- Implement at least three child templates that extend the base template
- Create a template with a for loop to display a list of items
- Create a template with conditionals to show different content based on variables
- Implement a custom filter and use it in your templates
Advanced Project
Build a portfolio website template system:
- Create a base template with modern, responsive design
- Implement templates for home page, project list, project detail, about page, and contact page
- Create reusable components for project cards, skills section, and contact form
- Use template inheritance for consistent layout
- Implement custom filters for formatting dates and other data
- Add template macros for UI components like buttons, cards, and form fields
- Add conditional rendering based on user authentication status
- Implement different color themes using template variables