Flask Templates and HTML Pages

Separate your HTML from your Python

Intermediate 12 min

In this lesson

A Flask template is an HTML file with blanks that Python fills in when it builds a page — keeping your markup and your logic apart, instead of returning whole pages as messy Python strings. This lesson shows how render_template() and Jinja placeholders work, and you’ll build the list-rendering logic yourself.

Explain it like I’m 5

A template is a fill-in-the-blank web page. Python fills in the blanks — your name, your list of notes — just before sending the finished page to the browser.

Why templates exist

Instead of building HTML with string concatenation in your view, you keep the HTML in a file under a templates/ folder and call render_template(), passing in the variables it needs. The view stays about data; the template stays about presentation.

Example
# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def home():
    notes = ["Buy milk", "Walk the dog"]
    return render_template("index.html", name="Ada", notes=notes)
The view passes data; the template decides how it looks.
Example · templates/index.html
<h1>Hello, {{ name }}!</h1>
<ul>
  {% for note in notes %}
    <li>{{ note }}</li>
  {% endfor %}
</ul>
Jinja fills {{ }} with values and loops with {% %}.

Filling placeholders is just substitution

Under the hood, filling a placeholder is the same idea as Python’s own string formatting: take a slot and drop a value into it. Seeing it in plain Python demystifies what Jinja does on the server.

Example
template = "<h1>Hello, {name}!</h1>"
print(template.format(name="Ada"))
Output
<h1>Hello, Ada!</h1>
Jinja's {{ name }} works like this fill-in-the-blank.

Rendering a list, and staying safe

To show many items you build one <li> per element. Real templates also auto-escape values, turning characters like < and & into safe entities so a user can’t inject HTML. You can see that escaping with the standard html module.

Example
import html

notes = ["Buy milk", "Fish & chips", "<script>"]
items = "".join(f"<li>{html.escape(note)}</li>" for note in notes)
print(items)
Output
<li>Buy milk</li><li>Fish &amp; chips</li><li>&lt;script&gt;</li>
Auto-escaping turns risky characters into safe entities.

Build an HTML unordered list from the tools list: wrap each tool in <li> tags, join them, then surround the result with <ul></ul>.

tools = ["Python", "Flask", "Jinja"]
items = ""  # TODO: join "<li>tool</li>" for each tool
page = f"<ul>{items}</ul>"
print(page)

Common mistake: Putting templates in the wrong folder

Why it happens:

It’s not obvious Flask looks in a specific place.

How to fix it:

Templates must live in a folder named templates/ beside your app, or render_template() can’t find them and raises a TemplateNotFound error.

Common mistake: Building large HTML pages with Python f-strings

Why it happens:

For one line of HTML an f-string is quick, so it creeps into whole pages.

How to fix it:

Move anything beyond a tiny fragment into a template. Big HTML strings in Python are hard to read, easy to break, and skip auto-escaping.

Common mistake: Mixing too much logic into templates

Why it happens:

Jinja can do loops and conditionals, so it’s tempting to compute things there.

How to fix it:

Do the real work in Python and pass finished values to the template. Keep templates focused on display, with only simple loops and ifs.

What does render_template() do?

Where must template files live?

Why are templates safer than giant HTML strings built in Python?

Mini exercise (medium)

Write render_list(title, items) that returns an HTML fragment: an <h2> with the title, followed by a <ul> of the items as <li> elements. An empty list should still produce <ul></ul>.

Do it here. Finish the code below and press Run to try your answer right in the browser — no setup needed.

def render_list(title, items):
    lis = ""  # TODO: join "<li>item</li>" for each item
    # TODO: return an <h2> title followed by a <ul> of the items
    return ""

print(render_list("Tools", ["Python", "Flask"]))

What to learn next

You learned why templates beat giant HTML strings: render_template() loads a file from templates/ and fills its Jinja {{ }} placeholders, {% for %} loops repeat markup, and auto-escaping keeps user text from injecting HTML. You wrote render_list() to build a titled list fragment.

Pages get interactive when visitors can send you data. Handling Forms in Flask covers reading request.form, GET vs POST, and validating input you should never trust. When you’re ready, continue to the next lesson.