Jinja2 Templates
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:
pune-tiffin/
app.py
static/
site.css
templates/
base.html
home.htmlIn 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.
# 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 %}:
<!-- 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.
<!-- 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:
<!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:
| Syntax | Meaning |
|---|---|
{{ 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.index | The 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:
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"],
))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.
@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:
<p><script>alert("hacked")</script> 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:
<!-- 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><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
TemplateNotFounderror. The folder must be named exactlytemplates(plural) and sit next to the file that createsFlask(__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. Useurl_for('static', filename='site.css')so the path keeps working if the app is mounted under a sub-path. - Adding
| safeto 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 thetemplatesfolder with those values.{{ }}prints,{% %}runs tags such asif,for,blockandextends, 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;
| safeturns 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
{% 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 %}<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?
render_template() searches the templates folder of the app (and of blueprints).
2. What does {% extends "base.html" %} do?
Template inheritance lets a child template override named blocks in the base layout.
3. A user's review contains <b>Great</b>. What does {{ review }} show in Flask?
Autoescaping turns < and > into < and >, so the tags show as text.
4. Which filter prints a fallback when a value is missing?
default replaces an undefined value with the text you give it.
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
