Skip to content

Flask Jinja2 Templates

Jinja2 is Flask’s built-in template engine. Its design was inspired by Django’s template engine, and it extends that concept with additional syntax and powerful features. Flask provides the render_template function to render Jinja2 templates and pass data to them.

Basic Template Usage

1. Configure the template folder when creating the Flask application:

app = Flask(__name__, template_folder='templates')

2. Create a templates/ directory in your project root and add an HTML file:

<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ title }}</h1>
</body>
</html>

3. Render the template from a view function:

from flask import Flask, render_template

app = Flask(import_name=__name__, template_folder='templates')

class Config():
    DEBUG = True

app.config.from_object(Config)

@app.route('/')
def index():
    data = {"title": "My Flask Project"}
    return render_template("index.html", **data)

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

Template Variables

Use {{ }} to output variables (called “variable code blocks”):

<div>{{ title }}</div>
<div>{{ data_list }}</div>
<div>{{ data_list[0] }}</div>
<div>{{ data_list.0 }}</div>          <!-- dot notation for index access -->
<div>{{ data_list[-1] }}</div>
<div>{{ data_dict }}</div>
<div>{{ data_dict['name'] }}</div>
<div>{{ data_dict.name }}</div>       <!-- dot notation for key access -->

Any Python value that can be converted to a string with str() or __str__() can be displayed in a template — strings, numbers, lists, dicts, and custom objects.

Comments — use {# #}; the content is not rendered in the HTML output:

{# {{ name }} #}

Built-in Template Variables

Flask automatically injects several useful variables into every template context:

config

Access the current Flask config object directly:

{{ config.DEBUG }}
{{ config.SQLALCHEMY_DATABASE_URI }}

request

The current HTTP request object:

{{ request.url }}
{{ request.args.name }}
{{ request.headers.Host }}

session

The current session object:

{{ session.name }}
{{ session.new }}

g

The g object stores request-scoped data set in view functions:

{{ g.name }}

url_for()

Generates the URL for a named view function — safer than hardcoding paths:

{{ url_for('home') }}
{{ url_for('index', post_id=1) }}    <!-- /1 -->
{{ url_for('set_session') }}

Flow Control

Control structures use {% %} blocks.

if Statement

{% if name == "root" %}
    <p>Welcome back, administrator!</p>
{% elif name %}
    <p>Welcome, {{ name }}!</p>
{% else %}
    <p>You are not logged in.</p>
{% endif %}

Filters can be used inside conditions:

{% if request.args.get("num") | int % 2 == 0 %}
    <p>Even</p>
{% else %}
    <p>Odd</p>
{% endif %}

for Loop

{% for post in posts %}
    <div>
        <h1>{{ post.title }}</h1>
        <p>{{ post.text | safe }}</p>
    </div>
{% endfor %}

Combine for and if to filter items inline (simulates continue):

{% for post in posts if post.text %}
    <div>
        <h1>{{ post.title }}</h1>
        <p>{{ post.text | safe }}</p>
    </div>
{% endfor %}

Loop Context Variable

Inside a for loop, a special loop variable is available:

VariableDescription
loop.indexCurrent iteration count (1-based)
loop.index0Current iteration count (0-based)
loop.revindexIterations remaining (1-based)
loop.revindex0Iterations remaining (0-based)
loop.firstTrue on the first iteration
loop.lastTrue on the last iteration
loop.lengthTotal number of items
loop.cycle(...)Cycles through its arguments on each iteration

Example — alternate row colors with loop.cycle:

<table border="1" align="center" width="600">
    <tr><th>Index</th><th>ID</th><th>Price</th><th>Title</th></tr>
    {% for book in book_list %}
        {% if loop.index % 2 == 0 %}
            <tr bgcolor="#add8e6">
        {% else %}
            <tr>
        {% endif %}
            <td>{{ loop.index }}</td>
            <td>{{ book.id }}</td>
            <td>{{ book.price }}</td>
            <td>{{ book.title }}</td>
        </tr>
    {% endfor %}
</table>

Filters

Filters transform a variable’s value before display. Use the pipe character (|):

{{ variable | filter_name(arg1, arg2) }}
{{ variable | filter_name }}   <!-- parentheses optional when no arguments -->

Filters can be chained:

{{ "hello world" | reverse | upper }}

Common Built-in Filters

String filters:

{{ '<em>hello</em>' | safe }}           <!-- render HTML as-is, disable escaping -->
{{ 'hello' | capitalize }}              <!-- Hello -->
{{ 'HELLO' | lower }}                   <!-- hello -->
{{ 'hello' | upper }}                   <!-- HELLO -->
{{ 'hello world' | title }}             <!-- Hello World -->
{{ 'olleh' | reverse }}                 <!-- hello -->
{{ '%s is %d' | format('age', 17) }}   <!-- age is 17 -->
{{ '<em>hello</em>' | striptags }}      <!-- hello (strips HTML tags) -->
{{ 'hello every one' | truncate(9) }}   <!-- hello... -->

List filters:

{{ [1,2,3,4,5,6] | first }}    <!-- 1 -->
{{ [1,2,3,4,5,6] | last }}     <!-- 6 -->
{{ [1,2,3,4,5,6] | length }}   <!-- 6 -->
{{ [1,2,3,4,5,6] | sum }}      <!-- 21 -->
{{ [6,2,3,1,5,4] | sort }}     <!-- [1, 2, 3, 4, 5, 6] -->

Block filter:

{% filter upper %}
    a block of text that will all be uppercased
{% endfilter %}

Custom Filters

Method 1 — add_template_filter:

def do_list_reverse(old_list):
    new_list = list(old_list)   # copy to avoid mutating the original
    new_list.reverse()
    return new_list

app.add_template_filter(do_list_reverse, "lrev")

Method 2 — @app.template_filter decorator:

@app.template_filter('lrev')
def do_list_reverse(old_list):
    new_list = list(old_list)
    new_list.reverse()
    return new_list

Usage in template:

<p>{{ user_list }}</p>          <!-- ['xiaoming', 'bob', 'alice'] -->
<p>{{ user_list | lrev }}</p>   <!-- ['alice', 'bob', 'xiaoming'] -->
<p>{{ user_list }}</p>          <!-- ['xiaoming', 'bob', 'alice'] (unchanged) -->

Example — mask a phone number:

@app.template_filter("mobile")
def do_mobile(data, string):
    return data[:3] + string + data[7:]
<td>{{ user.mobile | mobile(string="****") }}</td>
<!-- 131****5678 -->
If a custom filter has the same name as a built-in filter, it will override the built-in one. Choose unique names to avoid accidental conflicts.

Template Inheritance

Template inheritance lets you define a shared base layout and fill in page-specific content in child templates.

Base Template (base.html)

{% block top %}
    <!-- top navigation (default content) -->
{% endblock top %}

{% block content %}
{% endblock content %}

{% block bottom %}
    <!-- footer (default content) -->
{% endblock bottom %}

Child Template

{% extends 'base.html' %}

{% block content %}
    <!-- page-specific content goes here -->
{% endblock content %}

Calling {{ super() }} inside a block includes the parent block’s content before or after your additions.

Rules for template inheritance:

  1. Multiple inheritance is not supported.
  2. The {% extends %} tag should be the first line of the child template.
  3. Do not define multiple blocks with the same name in one template.
  4. Always name the {% endblock %} tag (e.g. {% endblock content %}) to improve readability, especially with nested blocks.

CSRF Protection in Flask

Flask does not include built-in CSRF protection. Use the Flask-WTF extension:

pip install flask_wtf

Step 1 — Set a SECRET_KEY (required for signing CSRF tokens):

class Config(object):
    DEBUG = True
    SECRET_KEY = "strong-random-secret-key"

app.config.from_object(Config)

Step 2 — Initialize CSRFProtect:

from flask_wtf.csrf import CSRFProtect

CSRFProtect(app)

Step 3 — Add the CSRF token to every form:

<form action="{{ url_for('login') }}" method="post">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input type="submit" value="Login">
</form>

Full example:

from flask import Flask, render_template
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__, template_folder='templates')

class Config(object):
    DEBUG = True
    SECRET_KEY = "strong-random-secret-key"

app.config.from_object(Config)
CSRFProtect(app)  # enable global CSRF protection

@app.route("/login", methods=["GET"])
def loginform():
    return render_template("login.html")

@app.route("/dologin", methods=["POST"])
def login():
    # Flask-WTF automatically validates the csrf_token on POST
    return "Login successful"

if __name__ == '__main__':
    app.run(debug=True)
Last updated on