Module 10.2

Jinja Templates

Templates separate presentation from logic using Jinja2. Build dynamic HTML pages by combining Python data with reusable template structures. Master variable interpolation, loops, conditionals, and template inheritance.

50 min
Intermediate
Hands-on
What You'll Learn
  • Render templates with render_template()
  • Pass and display variables in templates
  • Use loops and conditionals in Jinja2
  • Apply filters to transform data
  • Create reusable layouts with inheritance
Contents
01

Introduction to Templates

Templates allow you to separate your application's logic from its presentation. Instead of building HTML strings in Python, you create template files with placeholders that Flask fills in with dynamic data.

Key Concept

What is Jinja2?

Jinja2 is a modern and designer-friendly templating language for Python. It is fast, widely used, and features powerful template inheritance. Flask uses Jinja2 as its default template engine, providing a clean separation between your Python code and HTML templates.

Why it matters: Templating makes your code more maintainable, allows designers to work on HTML independently, and provides automatic escaping to prevent XSS attacks.

Template Rendering Flow
Python Data
Variables, Lists, Dicts
Template
HTML + Jinja2
Final HTML
Rendered Page

The template engine combines your Python data with the template file, replacing placeholders with actual values to produce the final HTML sent to the browser.

Project Structure

Flask looks for templates in a templates folder by default. Create this folder at the same level as your main application file.

my_flask_app/
    app.py              # Main application
    templates/          # Template folder
        index.html
        about.html
        layout.html
    static/             # Static files (CSS, JS, images)
        css/
        js/
        images/

Keep your templates organized in the templates/ folder and static assets in the static/ folder. Flask automatically locates these directories.

02

Rendering Templates

The render_template() function loads a template file, fills in the provided variables, and returns the rendered HTML as a response.

Basic Template Rendering

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    return render_template('index.html')

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

The first argument is the template filename. Additional keyword arguments become variables available in the template.

Passing Multiple Variables

@app.route('/dashboard')
def dashboard():
    user_data = {
        'name': 'Alice',
        'email': 'alice@example.com',
        'posts': 42
    }
    notifications = ['New comment', 'New follower', 'Post liked']
    
    return render_template('dashboard.html',
                           user=user_data,
                           notifications=notifications,
                           is_admin=True)

You can pass strings, numbers, lists, dictionaries, and even objects to templates. Each keyword argument becomes a variable in the template context.

Simple Template Example

<!-- templates/user.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Welcome</title>
</head>
<body>
    <h1>Hello, {{ username }}!</h1>
    <p>Welcome to our Flask application.</p>
</body>
</html>

The {{ username }} placeholder is replaced with the value passed from the Python view function.

03

Jinja2 Syntax

Jinja2 uses special delimiters to distinguish template code from regular HTML. Understanding these three main syntax types is essential for effective templating.

{{ }} Expressions

Output the result of an expression to the template.

<p>Hello, {{ name }}!</p>
<p>Total: {{ price * qty }}</p>
{% %} Statements

Execute control flow like loops and conditionals.

{% if user %}
  <p>Welcome!</p>
{% endif %}
{# #} Comments

Add comments that are not rendered in output.

{# This comment will not
   appear in the HTML #}

Accessing Variables

You can access dictionary keys, object attributes, and list indices using dot notation or bracket notation.

<!-- Dictionary access -->
<p>Name: {{ user.name }}</p>
<p>Email: {{ user['email'] }}</p>

<!-- List access -->
<p>First item: {{ items[0] }}</p>

<!-- Nested access -->
<p>City: {{ user.address.city }}</p>

Dot notation is preferred for readability. Both user.name and user['name'] work identically in Jinja2.

Safe HTML Output

By default, Jinja2 escapes HTML characters to prevent XSS attacks. Use the safe filter for trusted HTML content.

<!-- Escaped (safe) - default behavior -->
{{ user_input }}
<!-- Renders: &lt;script&gt;alert('xss')&lt;/script&gt; -->

<!-- Unescaped (trusted content only) -->
{{ trusted_html | safe }}
Security Warning: Only use the safe filter with trusted content. Never use it with user-provided input.
04

Control Structures

Jinja2 provides powerful control structures for conditional rendering and iteration. These allow you to create dynamic content based on your data.

Conditional Statements

Use if, elif, and else to conditionally render content.

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

{% if score >= 90 %}
    <span class="badge bg-success">A</span>
{% elif score >= 80 %}
    <span class="badge bg-info">B</span>
{% elif score >= 70 %}
    <span class="badge bg-warning">C</span>
{% else %}
    <span class="badge bg-danger">F</span>
{% endif %}

Conditionals let you show different content based on values, user roles, or any other condition from your Python code.

For Loops

Iterate over lists, dictionaries, and other iterables to generate repeated content.

<!-- Simple list iteration -->
<ul>
{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}
</ul>

<!-- Loop with index -->
{% for user in users %}
    <p>{{ loop.index }}. {{ user.name }}</p>
{% endfor %}

The special loop variable provides useful properties like loop.index, loop.first, and loop.last.

Loop Variables

Variable Description
loop.index Current iteration (1-indexed)
loop.index0 Current iteration (0-indexed)
loop.first True if first iteration
loop.last True if last iteration
loop.length Total number of items
<!-- Practical example with loop variables -->
{% for product in products %}
    <div class="{% if loop.first %}first{% endif %}">
        <span>{{ loop.index }} of {{ loop.length }}</span>
        <h3>{{ product.name }}</h3>
        <p>{{ product.price | format_currency }}</p>
    </div>
    {% if not loop.last %}<hr>{% endif %}
{% endfor %}
05

Filters and Functions

Filters modify variables for display without changing the original data. They are applied using the pipe symbol (|) and can be chained together.

Common Built-in Filters

<!-- String filters -->
{{ name | upper }}              {# ALICE #}
{{ name | lower }}              {# alice #}
{{ name | capitalize }}         {# Alice #}
{{ name | title }}              {# Alice Smith #}
{{ text | truncate(50) }}       {# First 50 chars... #}

<!-- Number filters -->
{{ price | round(2) }}          {# 19.99 #}
{{ count | default(0) }}        {# 0 if count is undefined #}

<!-- List filters -->
{{ items | length }}            {# Number of items #}
{{ items | first }}             {# First item #}
{{ items | last }}              {# Last item #}
{{ items | join(', ') }}        {# "a, b, c" #}

Filters transform data for display. Chain multiple filters with | - they execute left to right.

Chaining Filters

<!-- Multiple filters applied in sequence -->
{{ username | lower | replace(' ', '_') }}
{{ description | striptags | truncate(100) }}
{{ price | round(2) | string + ' USD' }}

Filters process data from left to right, so lower runs first, then replace.

url_for Function

Generate URLs dynamically using the endpoint name instead of hardcoding paths.

<!-- Link to routes -->
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('user', username='alice') }}">Profile</a>

<!-- Link to static files -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<img src="{{ url_for('static', filename='images/logo.png') }}">
Best Practice: Always use url_for() for links and static files. It handles URL changes automatically and works across different deployment environments.
06

Template Inheritance

Template inheritance allows you to create a base layout with common elements (header, footer, navigation) and extend it in child templates. This eliminates repetition and ensures consistency.

Base Template (Layout)

<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My Site{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <nav>
        <a href="{{ url_for('home') }}">Home</a>
        <a href="{{ url_for('about') }}">About</a>
    </nav>
    
    <main>
        {% block content %}{% endblock %}
    </main>
    
    <footer>Copyright 2026</footer>
</body>
</html>

The base template defines block placeholders that child templates can override. Common elements like nav and footer appear once.

Child Template

<!-- templates/home.html -->
{% extends 'base.html' %}

{% block title %}Home - My Site{% endblock %}

{% block content %}
    <h1>Welcome to My Site</h1>
    <p>This content replaces the content block.</p>
{% endblock %}

Child templates use extends to inherit from a base and block to provide content for specific sections.

Using super()

Include the parent block's content using super().

{% block content %}
    {{ super() }}  {# Include parent's content #}
    <p>Additional content after parent.</p>
{% endblock %}
07

Practice Exercises

Apply your Jinja2 knowledge with these hands-on exercises covering templates, variables, loops, and inheritance.

Jinja Templates Practice

Task: Create a route that passes a name variable and a template that displays "Hello, [name]!"

Show Solution
# app.py
@app.route('/hello/')
def hello(name):
    return render_template('hello.html', name=name)
<!-- templates/hello.html -->
<h1>Hello, {{ name }}!</h1>

Task: Pass a user dictionary with name, email, and age. Display all fields in the template.

Show Solution
# app.py
@app.route('/profile')
def profile():
    user = {'name': 'Alice', 'email': 'alice@test.com', 'age': 28}
    return render_template('profile.html', user=user)
<!-- templates/profile.html -->
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
<p>Age: {{ user.age }}</p>

Task: Pass a list of products and display them in an unordered list with name and price.

Show Solution
# app.py
@app.route('/products')
def products():
    items = [
        {'name': 'Laptop', 'price': 999},
        {'name': 'Mouse', 'price': 29},
        {'name': 'Keyboard', 'price': 79}
    ]
    return render_template('products.html', products=items)
<!-- templates/products.html -->
<ul>
{% for product in products %}
    <li>{{ product.name }} - ${{ product.price }}</li>
{% endfor %}
</ul>

Task: Show different content based on whether user.is_admin is True or False.

Show Solution
# app.py
@app.route('/dashboard')
def dashboard():
    user = {'name': 'Admin', 'is_admin': True}
    return render_template('dashboard.html', user=user)
<!-- templates/dashboard.html -->
{% if user.is_admin %}
    <div class="admin-panel">
        <h2>Admin Dashboard</h2>
        <a href="/admin/settings">Settings</a>
    </div>
{% else %}
    <div class="user-panel">
        <h2>User Dashboard</h2>
    </div>
{% endif %}

Task: Display a user's name in uppercase, truncate a long description, and format a price.

Show Solution
<!-- templates/item.html -->
<h1>{{ product.name | upper }}</h1>
<p>{{ product.description | truncate(100) }}</p>
<p>Price: ${{ product.price | round(2) }}</p>
<p>Tags: {{ product.tags | join(', ') }}</p>

Task: Create a base.html with nav, content block, and footer. Create home.html and about.html that extend it.

Show Solution
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My Site{% endblock %}</title>
</head>
<body>
    <nav>
        <a href="{{ url_for('home') }}">Home</a>
        <a href="{{ url_for('about') }}">About</a>
    </nav>
    <main>{% block content %}{% endblock %}</main>
    <footer>2026 My Site</footer>
</body>
</html>
<!-- templates/home.html -->
{% extends 'base.html' %}
{% block title %}Home{% endblock %}
{% block content %}
    <h1>Welcome Home</h1>
{% endblock %}

Task: Create a table of users with row numbers and alternating background colors using loop variables.

Show Solution
<!-- templates/users.html -->
<table>
    <tr><th>#</th><th>Name</th><th>Email</th></tr>
{% for user in users %}
    <tr class="{% if loop.index is odd %}odd{% else %}even{% endif %}">
        <td>{{ loop.index }}</td>
        <td>{{ user.name }}</td>
        <td>{{ user.email }}</td>
    </tr>
{% endfor %}
</table>

Task: Add a CSS file and JavaScript file from the static folder to your base template.

Show Solution
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Site{% endblock %}</title>
    <link rel="stylesheet" 
          href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    {% block content %}{% endblock %}
    <script src="{{ url_for('static', filename='js/main.js') }}">
    </script>
</body>
</html>

Task: Create a macro that generates form input fields with label and use it multiple times.

Show Solution
<!-- templates/forms.html -->
{% macro input_field(name, label, type='text') %}
    <div class="form-group">
        <label for="{{ name }}">{{ label }}</label>
        <input type="{{ type }}" id="{{ name }}" name="{{ name }}">
    </div>
{% endmacro %}

<!-- Usage -->
<form>
    {{ input_field('username', 'Username') }}
    {{ input_field('email', 'Email', 'email') }}
    {{ input_field('password', 'Password', 'password') }}
</form>

Key Takeaways

Separation of Concerns

Templates separate HTML presentation from Python logic, making code maintainable and designer-friendly

Jinja2 Syntax

Use {{ }} for output, {% %} for control flow, and {# #} for comments in your templates

Loops and Conditionals

for loops iterate over data, if statements conditionally render content, loop variables track iteration

Filters Transform Data

Use filters like upper, lower, truncate, and join to format data for display without modifying originals

Template Inheritance

Create base layouts with blocks that child templates extend for consistent, DRY design

url_for Function

Generate URLs dynamically for routes and static files - never hardcode paths in templates

Knowledge Check

Quick Quiz

Test what you've learned about Jinja2 templates

1 What delimiters are used for outputting variables in Jinja2?
2 What Flask function is used to render a template?
3 How do you create a for loop in Jinja2?
4 What does the | symbol do in Jinja2?
5 How do you implement template inheritance in Jinja2?
6 What special variable provides loop information in Jinja2?
Answer all questions to check your score