Flask Templates and HTML Pages
Separate your HTML from your Python
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.
# 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)
<h1>Hello, {{ name }}!</h1>
<ul>
{% for note in notes %}
<li>{{ note }}</li>
{% endfor %}
</ul>
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.
template = "<h1>Hello, {name}!</h1>"
print(template.format(name="Ada"))
<h1>Hello, Ada!</h1>
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.
import html
notes = ["Buy milk", "Fish & chips", "<script>"]
items = "".join(f"<li>{html.escape(note)}</li>" for note in notes)
print(items)
<li>Buy milk</li><li>Fish & chips</li><li><script></li>
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)
items = "".join(f"<li>{tool}</li>" for tool in tools), then wrap it in <ul> tags.
tools = ["Python", "Flask", "Jinja"]
items = "".join(f"<li>{tool}</li>" for tool in tools)
page = f"<ul>{items}</ul>"
print(page)
<ul><li>Python</li><li>Flask</li><li>Jinja</li></ul>
Common mistake: Putting templates in the wrong folder
It’s not obvious Flask looks in a specific place.
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
For one line of HTML an f-string is quick, so it creeps into whole pages.
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
Jinja can do loops and conditionals, so it’s tempting to compute things there.
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?
render_template("page.html", x=...) reads the template and substitutes the values you pass.
Where must template files live?
Flask looks in templates/ by default; that’s where render_template finds files.
Why are templates safer than giant HTML strings built in Python?
Jinja escapes {{ }} values by default, preventing injected markup — hand-built strings don’t.
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"]))
Build lis = "".join(f"<li>{item}</li>" for item in items), then return f"<h2>{title}</h2><ul>{lis}</ul>".
def render_list(title, items):
lis = "".join(f"<li>{item}</li>" for item in items)
return f"<h2>{title}</h2><ul>{lis}</ul>"
print(render_list("Tools", ["Python", "Flask"]))
<h2>Tools</h2><ul><li>Python</li><li>Flask</li></ul>
assert render_list("X", []) == "<h2>X</h2><ul></ul>", "an empty list should still produce empty <ul></ul>"
assert render_list("Tools", ["a"]) == "<h2>Tools</h2><ul><li>a</li></ul>", "wrap the title in <h2> and each item in <li>"
assert render_list("T", ["a", "b"]) == "<h2>T</h2><ul><li>a</li><li>b</li></ul>", "join all items inside one <ul>"
print("✓ Rendered!")