Learning Goals
3 min- Create a
templates/folder; render an HTML file withrender_template. - Pass Python variables into templates.
- Use Jinja for loops, ifs, filters and template inheritance.
- Serve static files (CSS, images) from
static/.
Warm-Up · The Folders
5 minmyapp/
├─ app.py
├─ templates/
│ ├─ base.html
│ └─ home.html
└─ static/
└─ style.cssFlask auto-finds templates/ and static/ as long as they sit next to app.py.
Separate logic from presentation. The Python file decides what data to show; the template decides how it looks. Designers can change one without touching the other.
New Concept · Jinja Syntax
14 minrender_template + variables
from flask import Flask, render_template app = Flask(__name__) @app.route("/") def home(): return render_template("home.html", name="Aisyah", hobbies=["chess", "coding", "kuih"])
<!-- templates/home.html -->
<h1>Hi, {{ name }}</h1>
<ul>
{% for h in hobbies %}
<li>{{ h }}</li>
{% endfor %}
</ul>The four Jinja delimiters
{{ value }} print a value
{% tag %} control structure (for, if, block)
{# ... #} comment
| filter transform a value
{{ name | upper }}If / for
{% if user %}
<p>Welcome, {{ user.name }}.</p>
{% else %}
<p>Please log in.</p>
{% endif %}
{% for p in products %}
<li>{{ p.name }} — RM {{ "%.2f"|format(p.price) }}</li>
{% else %}
<li>(no products)</li>
{% endfor %}Template inheritance
<!-- templates/base.html -->
<!doctype html>
<html>
<head>
<title>{% block title %}My App{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav>Home · About</nav>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>
<!-- templates/home.html -->
{% extends "base.html" %}
{% block title %}Home — My App{% endblock %}
{% block content %}
<h1>Welcome</h1>
{% endblock %}Auto-escaping
Jinja escapes HTML by default — {{ user_input }} renders <script>, not a real script. Don't turn this off; it's your XSS shield.
Worked Example · Product Catalog
12 minThree files: app.py, templates/base.html, templates/products.html.
# app.py from flask import Flask, render_template app = Flask(__name__) PRODUCTS = [ {"id": 1, "name": "Roti", "price": 1.50, "stock": 12}, {"id": 2, "name": "Milo", "price": 3.00, "stock": 5}, {"id": 3, "name": "Nasi", "price": 8.00, "stock": 0}, ] @app.route("/") def home(): return render_template("products.html", products=PRODUCTS, total=sum(p["price"] * p["stock"] for p in PRODUCTS)) if __name__ == "__main__": app.run(debug=True)
<!-- templates/base.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}Shop{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header><h1>Roti & Milo Shop</h1></header>
<main>{% block content %}{% endblock %}</main>
<footer>© 2026</footer>
</body>
</html>
<!-- templates/products.html -->
{% extends "base.html" %}
{% block title %}Products — Shop{% endblock %}
{% block content %}
<h2>Today's stock</h2>
<table>
<tr><th>Name</th><th>Price</th><th>Stock</th></tr>
{% for p in products %}
<tr class="{% if p.stock == 0 %}out{% endif %}">
<td>{{ p.name }}</td>
<td>RM {{ "%.2f"|format(p.price) }}</td>
<td>{{ p.stock }}</td>
</tr>
{% endfor %}
</table>
<p>Inventory value: <strong>RM {{ "%.2f"|format(total) }}</strong></p>
{% endblock %}/* static/style.css */
body { font-family: system-ui; max-width: 720px; margin: 2rem auto; }
table { border-collapse: collapse; width: 100%; }
th, td { padding: 6px 10px; border-bottom: 1px solid #eee; }
.out { color: #aaa; text-decoration: line-through; }Run, open http://localhost:5000, see a styled product list. Out-of-stock rows are greyed out via a CSS class. Designers can now restyle without ever opening the Python file.
Try It Yourself
13 minTake a /hello/<name> route and render a template that says "Hi, <name>" in a friendly layout.
Load a CSV at startup. Render it as an HTML table via Jinja.
Add a search box (query parameter q) that filters the products list by name (case-insensitive).
Hint
@app.route("/") def home(): q = request.args.get("q", "").lower() rows = [p for p in PRODUCTS if q in p["name"].lower()] return render_template("products.html", products=rows, q=q)
<!-- in template -->
<form>
<input name="q" value="{{ q or '' }}" placeholder="search">
<button>Go</button>
</form>Mini-Challenge · About-Me Site
8 minThree pages — home, projects, contact — sharing one base.html. The projects page renders a list-of-dicts as cards. Add a small nav in the base layout.
Recap
3 minrender_template + variables = clean separation. Jinja gives you for, if, filters, and inheritance. Use url_for("static", ...) for static assets so paths stay correct in deployment. Tomorrow: forms.
Homework
4 minConvert your Lesson 38 portfolio API into a real website: a base layout + at least 3 page templates that share the same header / footer. Style it lightly with a CSS file. Commit it.