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:
| Variable | Description |
|---|---|
loop.index | Current iteration count (1-based) |
loop.index0 | Current iteration count (0-based) |
loop.revindex | Iterations remaining (1-based) |
loop.revindex0 | Iterations remaining (0-based) |
loop.first | True on the first iteration |
loop.last | True on the last iteration |
loop.length | Total 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_listUsage 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 -->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:
- Multiple inheritance is not supported.
- The
{% extends %}tag should be the first line of the child template. - Do not define multiple blocks with the same name in one template.
- 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_wtfStep 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)