DataLens

Templates with Jinja2

Move HTML out of Python into templates, fill them with data, loop and branch inside them, share a base layout, and let auto-escaping keep you safe.

Level: Intermediate · 11 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Building HTML with f-strings works for a heading. For a real page (a table of orders, a navigation bar, a footer on every page), Python strings full of HTML become unreadable and dangerous. Templates solve this: HTML files with placeholders, filled in by Flask using the Jinja2 template engine.

The split is clean: Python decides what data to show; the template decides how it looks.

Your first template

Templates live in a folder named templates/ next to your app:

shop/
├── app.py
└── templates/
    └── product.html

templates/product.html (this module includes it):

<h1>{{ name }}</h1>
<p>Price: Rp {{ "{:,}".format(price) }}</p>

app.py:

from flask import Flask, render_template

app = Flask(__name__)


@app.route("/products/<int:product_id>")
def product(product_id):
    return render_template("product.html", name="Ceramic Mug", price=55000)


client = app.test_client()
print(client.get("/products/6").get_data(as_text=True))
<h1>Ceramic Mug</h1>
<p>Price: Rp 55,000</p>

Jinja syntax in one table

SyntaxDoesExample
{{ ... }}print a value{{ customer.name }}
{% ... %}logic: loops, ifs, blocks{% for o in orders %} ... {% endfor %}
{# ... #}a comment, not in the output{# shown to developers only #}
`\` filtertransform a value`{{ name\upper }}, {{ items\length }}`

Loops and conditions

For quick experiments (and on this site), render_template_string takes the template as a string instead of a file. The syntax is identical:

from flask import Flask, render_template_string

app = Flask(__name__)

ORDERS_PAGE = """
<h1>Orders ({{ orders|length }})</h1>
{% if orders %}
<ul>
{% for o in orders %}
  <li>#{{ o.id }}: {{ o.customer }}, Rp {{ "{:,}".format(o.total) }}{% if o.total > 500000 %} <b>big</b>{% endif %}</li>
{% endfor %}
</ul>
{% else %}
<p>No orders yet.</p>
{% endif %}
"""


@app.route("/orders")
def orders():
    data = [
        {"id": 38, "customer": "Dimas Pratama", "total": 90000},
        {"id": 33, "customer": "Kartika Rahma", "total": 735000},
    ]
    return render_template_string(ORDERS_PAGE, orders=data)


client = app.test_client()
print(client.get("/orders").get_data(as_text=True))
<h1>Orders (2)</h1>

<ul>

  <li>#38: Dimas Pratama, Rp 90,000</li>

  <li>#33: Kartika Rahma, Rp 735,000 <b>big</b></li>

</ul>

The blank lines come from the {% %} tags; browsers ignore them. (app.jinja_env.trim_blocks = True removes them if you care.)

o.id works for both object attributes and dict keys: Jinja tries both.

Filters

Templates often need a value tweaked just for display: a name in capitals, a list's length, a fallback when something is missing, a long note cut short. Jinja's filters do that inside the template. You write the value, a |, and the filter's name:

from flask import Flask, render_template_string

app = Flask(__name__)
with app.app_context():
    print(render_template_string("{{ name|upper }} | {{ name|title }} | {{ items|length }} | {{ missing|default('n/a') }}",
                                 name="ayu lestari", items=[1, 2, 3]))
    print(render_template_string("{{ price|round(1) }} | {{ tags|join(', ') }} | {{ note|truncate(12) }}",
                                 price=12.345, tags=["coffee", "gift"], note="Fragile, handle with care"))
AYU LESTARI | Ayu Lestari | 3 | n/a
12.3 | coffee, gift | Fragile,...

Auto-escaping: safety for free

Here's a question that matters more than it looks: what happens if a customer types HTML, or even a script, into their name, and your template shows that name on a page?

from flask import Flask, render_template_string

app = Flask(__name__)
with app.app_context():
    print(render_template_string("<p>Hello {{ name }}</p>", name="<script>alert('hi')</script>"))
<p>Hello &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</p>

Jinja escaped the < and >, so the browser shows the text instead of running the script. Without escaping, anyone who can type into a form (a review, a name, an address) could inject JavaScript into your pages; this is called cross-site scripting (XSS). Templates rendered by Flask escape by default. Building HTML with f-strings doesn't, which is the main reason to stop doing it.

Key idea

Put data into HTML with {{ }} in templates, never with string formatting. Escaping is automatic, so user input can't become code.

Template inheritance: one layout, many pages

Every page shares a header and footer. Write them once in a base template with blocks that pages fill in:

templates/base.html (included in this module):

<!doctype html>
<html>
<head><title>{% block title %}Toko Kita{% endblock %}</title></head>
<body>
  <nav><a href="/">Home</a> · <a href="/about">About</a></nav>
  <main>{% block content %}{% endblock %}</main>
  <footer>© Toko Kita</footer>
</body>
</html>

templates/about.html:

{% extends "base.html" %}
{% block title %}About · Toko Kita{% endblock %}
{% block content %}<h1>About us</h1><p>Coffee and desk gear since 2023.</p>{% endblock %}
from flask import Flask, render_template

app = Flask(__name__)


@app.route("/about")
def about():
    return render_template("about.html")


html = app.test_client().get("/about").get_data(as_text=True)
print(html)
<!doctype html>
<html>
<head><title>About · Toko Kita</title></head>
<body>
  <nav><a href="/">Home</a> · <a href="/about">About</a></nav>
  <main><h1>About us</h1><p>Coffee and desk gear since 2023.</p></main>
  <footer>© Toko Kita</footer>
</body>
</html>

{% extends %} says "start from base.html"; each {% block %} replaces the base's block of the same name. Change the navigation once in base.html, and every page updates.

Keep logic in Python

Jinja can do maths and call methods, but resist it. Compute totals, filter lists and format money in the view (or a helper function), and pass ready-to-show values to the template. A template full of logic is as hard to test as a 300-line function.

Why this matters for clean code

Templates separate presentation from logic, the web version of the "data prep → display" split you've used in every track. Views become short (load data, call render_template), templates are readable HTML a designer could edit, and auto-escaping removes a whole class of security bugs.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Serving static files →