6. Templates with Jinja2

Returning a string is enough to prove Flask is running, but real pages need HTML. Flask uses Jinja2 templates for that job: HTML files with small placeholders that Python fills in before the page is sent to the browser.

Why templates exist

You could return HTML directly from a route:

app.py
@app.route("/")
def home():
    return "<h1>Music Time Machine</h1>"

That becomes awkward quickly. HTML belongs in HTML files. Python routes should prepare data, choose a template, and let the template describe the page.

Render a template

Import render_template(), then return it from your route:

app.py
from flask import Flask, render_template

app = Flask(__name__)


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

Flask looks for home.html inside the templates/ folder:

Project layout
music-time-machine/
├── app.py
└── templates/
    └── home.html

Create the template:

templates/home.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Music Time Machine</title>
</head>
<body>
  <h1>Music Time Machine</h1>
  <p>Your Flask page is rendering from a template.</p>
</body>
</html>

Now the route returns a complete HTML page, but the HTML lives in the right place.

Pass data into a template

Templates become useful when Python passes values into them. Add keyword arguments to render_template():

app.py
@app.route("/")
def home():
    return render_template(
        "home.html",
        username="Alex",
        track_count=247
    )

Inside the template, use double curly braces to display those values:

templates/home.html

Welcome, {{ username }}

You have {{ track_count }} tracks in your database.

Jinja2 replaces {{ username }} and {{ track_count }} before the browser receives the page. The browser sees plain HTML, not Jinja2 syntax.

Use conditionals

Templates can choose what to show. Use {% if %}, {% else %}, and {% endif %} for conditional content:

templates/home.html
{% if track_count > 0 %}
  

Your database has music data.

{% else %}

No tracks yet. Run a snapshot first.

{% endif %}

This pattern is important for empty states. The capstone uses it to show a helpful message when the database has no snapshots yet.

Loop over lists

Pass a list from Python:

app.py
@app.route("/")
def home():
    tracks = [
        {"name": "Track One", "artist_name": "Artist A"},
        {"name": "Track Two", "artist_name": "Artist B"},
    ]
    return render_template("home.html", tracks=tracks)

Then loop over it in the template:

templates/home.html
    {% for track in tracks %}
  • {{ track.name }} by {{ track.artist_name }}
  • {% endfor %}

Jinja2 lets you use dot access, such as track.name, for dictionary keys. That keeps templates readable when you pass structured data into them.

Use simple filters

Filters transform a value before it is displayed. A filter starts with a pipe character:

templates/home.html

Total tracks: {{ tracks|length }}

Artist: {{ artist_name|default("Unknown artist") }}

The |length filter counts items. The |default() filter provides fallback text when a value is missing.

Automatic escaping

Jinja2 escapes values by default. If a variable contains HTML-like text, Jinja2 turns the angle brackets into harmless characters before the browser sees them.

Security: automatic escaping

Automatic escaping helps prevent cross-site scripting, often shortened to XSS. If user-controlled data contains <script>, Jinja2 displays it as text instead of letting the browser run it as code.

Be careful with the |safe filter. It disables escaping for that value. Do not use it on data a user can control.

At this point, you can render HTML files, pass Python values into them, show different content with conditionals, and loop over lists. The next page builds on that by sharing one layout across multiple templates.