Jinja2 Templates

Django & Flask · Lesson 6 of 30 7 min read Updated

Returning HTML as a Python string works for one line, but a real page has a header, a menu, a footer and dozens of values from your code. Writing that inside f-strings quickly becomes unreadable and unsafe. Flask solves this with templates: HTML files with small placeholders that the Jinja2 template engine fills in.

In this lesson you will render templates with render_template(), print values with {{ }}, use {% if %} and {% for %}, transform values with filters, share a layout through template inheritance, reuse snippets with macros, link CSS from the static folder and understand why Jinja2 escapes your data automatically.

Your first template

Flask looks for templates in a folder named templates next to your app, and for CSS, images and JavaScript in a folder named static:

Text
pune-tiffin/
  app.py
  static/
    site.css
  templates/
    base.html
    home.html

In the view, call render_template() with the file name and any values the template needs, given as keyword arguments. These values are called the context.

Python
# app.py
from flask import Flask, render_template

app = Flask(__name__)
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True

MENU = [
    {"name": "rajma chawal", "price": 120, "veg": True},
    {"name": "egg curry rice", "price": 140, "veg": False},
    {"name": "veg pulao", "price": 100, "veg": True},
]


@app.route("/")
def home():
    return render_template("home.html", customer="Ananya", menu=MENU)

The two jinja_env lines are optional. They remove the blank lines that {% %} tags would otherwise leave in the HTML, which makes the output easier to read. Browsers ignore those blank lines anyway.

Template inheritance

Every page of a site shares the same outer HTML. Instead of copying it, write it once in a base template and mark the parts that change with {% block %}:

HTML
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>{% block title %}Pune Tiffin{% endblock %}</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='site.css') }}">
</head>
<body>
  <nav><a href="{{ url_for('home') }}">Home</a></nav>
  <main>
    {% block content %}{% endblock %}
  </main>
</body>
</html>

A child template starts with {% extends %} and fills in only the blocks it needs. Anything outside a block in a child is ignored.

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

{% block title %}Today's menu | Pune Tiffin{% endblock %}

{% block content %}
  <h1>Hello, {{ customer }}!</h1>
  {% if menu %}
  <p>We have {{ menu | length }} dishes today.</p>
  {% else %}
  <p>The kitchen is closed today.</p>
  {% endif %}
  <ul>
    {% for dish in menu %}
    <li class="{{ 'veg' if dish.veg else 'non-veg' }}">{{ loop.index }}. {{ dish.name | title }}: Rs {{ dish.price }}</li>
    {% endfor %}
  </ul>
{% endblock %}

Opening / produces this HTML:

Output
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Today's menu | Pune Tiffin</title>
  <link rel="stylesheet" href="/static/site.css">
</head>
<body>
  <nav><a href="/">Home</a></nav>
  <main>
  <h1>Hello, Ananya!</h1>
  <p>We have 3 dishes today.</p>
  <ul>
    <li class="veg">1. Rajma Chawal: Rs 120</li>
    <li class="non-veg">2. Egg Curry Rice: Rs 140</li>
    <li class="veg">3. Veg Pulao: Rs 100</li>
  </ul>
  </main>
</body>
</html>

Here is the template syntax you just used:

SyntaxMeaning
{{ customer }}Print a value
{{ dish.name }}Read a key or attribute (works for dicts and objects)
{% if %}...{% else %}...{% endif %}Choose what to show
{% for x in items %}...{% endfor %}Repeat for each item
loop.indexThe loop counter, starting at 1 (loop.index0 starts at 0)
{{ a if cond else b }}Inline condition
{# note #}A comment that never reaches the browser

url_for() works inside templates too, so links and the CSS file path stay correct even if routes change. If the menu list were empty, the {% else %} branch would print "The kitchen is closed today." instead.

Filters

A filter changes a value before it is printed. Write it after a pipe |, and chain as many as you like. This script uses render_template_string(), which renders a template from a string, handy for quick experiments:

Python
from flask import Flask, render_template_string

app = Flask(__name__)

template = """{{ name | upper }}
{{ city | default("Pune") }}
{{ rating | round(1) }}
{{ areas | join(", ") }}
{{ "%.2f" | format(total) }}
{{ note | truncate(20) }}
{{ items | sort | first }}"""

with app.app_context():
    print(render_template_string(
        template,
        name="meera",
        rating=4.666,
        areas=["Kothrud", "Baner", "Aundh"],
        total=359.5,
        note="Please deliver to the back gate of the society",
        items=["roti", "dal", "salad"],
    ))
Output
MEERA
Pune
4.7
Kothrud, Baner, Aundh
359.50
Please deliver...
dal

city was never passed, so default("Pune") filled the gap. Without that filter, a missing value prints as an empty string, not an error. That is convenient, but it also hides typos, so check the page when something does not show.

Automatic escaping

Visitors type reviews, names and addresses, and some of them type HTML or JavaScript on purpose. Jinja2 in Flask escapes every value in .html templates: characters like <, > and " are turned into harmless codes that the browser shows as text.

Python
@app.route("/review")
def review():
    comment = '<script>alert("hacked")</script> Loved the rajma!'
    return render_template("review.html", comment=comment)

With review.html containing just <p>{{ comment }}</p>, the browser receives:

Output
<p>&lt;script&gt;alert(&#34;hacked&#34;)&lt;/script&gt; Loved the rajma!</p>

The script is shown as text instead of running. This protects you from XSS (cross-site scripting) attacks, which you will study in Security.

Macros: reusable snippets

A macro is like a function inside templates. Put shared macros in their own file and import them where needed:

HTML
<!-- templates/macros.html -->
{% macro price_tag(amount, offer=false) -%}
  <span class="price{{ ' offer' if offer }}">Rs {{ amount }}</span>
{%- endmacro %}

<!-- templates/menu.html -->
{% from "macros.html" import price_tag %}
<ul>
{% for dish in menu %}
  <li>{{ dish.name }} {{ price_tag(dish.price, offer=dish.price < 110) }}</li>
{% endfor %}
</ul>
Output
<ul>
  <li>Poha <span class="price offer">Rs 60</span></li>
  <li>Thali <span class="price">Rs 150</span></li>
</ul>

The - inside {%- and -%} trims whitespace next to that tag, so the span comes out on one line. For whole snippets without parameters, such as a footer, use {% include "footer.html" %}.

Common mistakes

  • TemplateNotFound error. The folder must be named exactly templates (plural) and sit next to the file that creates Flask(__name__).
  • Putting logic in templates. Calculate totals, filter lists and query the database in the view; templates should only display.
  • Hard-coding /static/site.css. Use url_for('static', filename='site.css') so the path keeps working if the app is mounted under a sub-path.
  • Adding | safe to make HTML "work". If escaped tags appear on your page, fix where the HTML comes from instead.

Key points

  • render_template("file.html", **values) renders a file from the templates folder with those values.
  • {{ }} prints, {% %} runs tags such as if, for, block and extends, and {# #} is a comment.
  • Filters (| title, | default, | join, | length) change values before printing.
  • A base template with {% block %} plus {% extends %} in children keeps the layout in one place.
  • Jinja2 escapes values in HTML templates automatically; | safe turns that off, so use it only for trusted HTML.

Exercise

Create templates/orders.html that extends base.html. The view passes orders, a list of dictionaries with customer, plates and paid (True or False). Show a table with one row per order, the word "Paid" or "Due" in the last column, and a final line with the total number of plates. If the list is empty, show "No orders yet". Use the sum filter with attribute='plates' for the total.

Show solution
HTML
{% extends "base.html" %}
{% block title %}Orders | Pune Tiffin{% endblock %}
{% block content %}
  <h1>Today's orders</h1>
  {% if orders %}
  <table>
    <tr><th>Customer</th><th>Plates</th><th>Status</th></tr>
    {% for order in orders %}
    <tr><td>{{ order.customer }}</td><td>{{ order.plates }}</td><td>{{ "Paid" if order.paid else "Due" }}</td></tr>
    {% endfor %}
  </table>
  <p>Total plates: {{ orders | sum(attribute="plates") }}</p>
  {% else %}
  <p>No orders yet</p>
  {% endif %}
{% endblock %}
Output
<h1>Today's orders</h1>
<table>
  <tr><th>Customer</th><th>Plates</th><th>Status</th></tr>
  <tr><td>Rohan</td><td>2</td><td>Paid</td></tr>
  <tr><td>Isha</td><td>3</td><td>Due</td></tr>
</table>
<p>Total plates: 5</p>

This is the <main> part of the page for two sample orders. The sum filter adds up one attribute of every item, so the template does not need a loop for the total.

Quick Quiz

1. Where does Flask look for template files by default?

2. What does {% extends "base.html" %} do?

3. A user's review contains <b>Great</b>. What does {{ review }} show in Flask?

4. Which filter prints a fallback when a value is missing?

Learn Django & Flask with a mentor

Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call