Flask Jinja Templates: Layouts, Variables, and Loops
Returning a string from a route is enough to prove that an application is running. Once a page needs a title, navigation, a data list, and styling, however, writing HTML inside a function quickly becomes awkward. Flask uses Jinja to separate Python data from HTML presentation.
The route remains responsible for fetching and preparing data. The template receives that data and decides how it appears. This simple split has a large effect: Python code does not disappear among HTML tags, and the presentation can change without disturbing the application flow.
Why Does Flask Look for templates?
By default, render_template("tasks.html") looks inside a templates/ folder next to the application file. Arrange this example as follows:
# File: folder-structure.txt
flask-first-route/
├── app.py
└── templates/
├── base.html
└── tasks.html
The folder name matters. If tasks.html sits outside templates/, Flask raises a TemplateNotFound error.
Pass Data from the Route
The /tasks route prepares a list of dictionaries. This shape gives Jinja more information than a list of strings because every task has a title and completion status.
# File: app.py
from flask import Flask, render_template
app = Flask(__name__)
@app.get("/tasks")
def tasks():
items = [
{"title": "Write outline", "done": True},
{"title": "Record demo", "done": False},
{"title": "Publish post", "done": False},
]
return render_template("tasks.html", tasks=items)
The tasks=items argument makes a variable named tasks available to the template. The name on the left belongs to Jinja, while the value on the right comes from Python. They can share the same name, but using different names here makes the handoff visible.
Build a Reusable Layout
base.html stores the frame shared by every page. The {% block content %} section gives child templates a place to insert their own content.
<!-- File: templates/base.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}Flask Tasks{% endblock %}</title>
</head>
<body>
<nav><a href="/tasks">Tasks</a></nav>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>
Now tasks.html can extend the layout instead of repeating the doctype, <head>, navigation, and <main> element.
<!-- File: templates/tasks.html -->
{% extends "base.html" %}
{% block title %}Task List{% endblock %}
{% block content %}
<h1>Tasks</h1>
{% if tasks %}
<ul>
{% for task in tasks %}
<li>
{{ task.title }}
{% if task.done %}<strong>Done</strong>{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>There are no tasks yet.</p>
{% endif %}
{% endblock %}
Recognize Jinja Syntax
Two kinds of delimiters appear frequently:
{{ ... }}renders a value, such as{{ task.title }}.{% ... %}runs presentation logic such asif,for,extends, orblock.
During the loop, each dictionary in tasks becomes the task variable. Jinja lets dictionary keys use dot notation, so task.title retrieves the "title" key.
The {% if tasks %} condition handles an empty list. Without the else branch, the page would show only a heading and could look broken. A short empty-state message makes the page state explicit.
Jinja also escapes rendered variables by default. If a task title contains a tag such as <script>, the tag is displayed as text rather than executed by the browser. Avoid applying the safe filter to user input unless the value has already been sanitized.
Keep Logic in the Right Place
Templates can decide whether a label appears or how a collection is repeated. Database queries, price calculations, and business rules should stay in Python. A useful rule is that logic needed by an API or another process should not exist only in a template.
To practice, empty the items list, add a priority field, and display a priority label for every task. Then create about.html and make it extend base.html as well. If a navigation change only needs to happen once in base.html, the shared layout is working as intended.
Related Articles
Keep reading within the same topic.
Flask Routing: URLs, Requests, and Responses
Learn Flask routing through URLs, path parameters, query strings, HTTP methods, and status codes for clear requests and responses.
Flask Tutorial: Setup and First Route
Set up Flask in a virtual environment, create app.py, run the development server, and follow a request through your first browser route.
Flask CRUD App: Build with MySQL, Auth, and Caching
Build a Flask CRUD app with MySQL, authentication, testing, caching, environment setup, and deployment-ready project structure.
Flask Tutorial: Build a Python Website from Scratch
Learn how to build a simple Python web application with Flask, routes, templates, form handling, and a clean starter structure for beginners.