To-Do List App - Part 3

In the previous post, we used Flask to attach routes to functions that return content. So far, we have returned strings with text with some HTML markup. For our app, we want to return a complete HTML page. Let’s have a look at what a complete page looks like:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The title of our page</title>
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
</head>
<body>
<!-- page content -->
<p>This is an example page.</p>
</body>
</html>
You don’t need to remember this. Usually, you can just copy an existing template and change it to what we need. The indentation is just for us, so we can keep track of opening and closing tags.
The first line tells the browser that this is an HTML file. The real code
starts with the html opening and closing tag that enclose everything.
Tags can have parameters, so instead of just <html>
we have <html lang="en">
.
This indicates that the language of everything between the opening and closing
text is in English.
Within the html tags we have two sections: a head and a body. The head contains information about the page, while the body contains the actual content.
In the head we first have a (meta) tag that indicates our character encoding. You can just copy this line; it tells the browser how special characters are encoded. We don’t have to close this tag.
We also have a title tag, where you can put the title of your web page. This is the title you will see on your browser tabs.
The stylesheet line indicates the file containing the CSS; in this case we should
also have a file style.css
. We will be using that later to make everything look
nice.
The script tag loads a file with JavaScript. Most web pages have some JavaScript code. For this project we won’t be needing that, so we can remove this line.
Inside the body tag we put the content of our page. The <!-- page content -->
shows you how you can add a comment to your HTML code.
Just like the indentation, the browser will ignore line breaks. So you could have
typed each word of ‘This is an example page.’ on a new line. If you really want
to have these words on separate lines on your page, you need to put them inside
different paragraphs (<p>
) or use breaks (<br>
).
If we want to return an HTML page in our Flask function, we could put the entire
HTML code in a single string and return that. Fortunately, there is an easier
solution. Flask provides a function render_template
that turns a Jinja
template into HTML. It expects the templates in a folder called templates
,
so create that folder in your project, and inside that folder create
a file todolist.html
with this content:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>To-Do List</title>
</head>
<body>
<h1>To-Do List Application</h1>
<ul>
<li>Feed the cat</li>
<li>Walk the dog</li>
<li>Go to sleep</li>
</ul>
</body>
</html>
This is the HTML template without the css and javascript lines, and
with some content added. In the body we now have a header (<h1
>),
followed by an unordered list (<ul>
). This list contains three
items, each wrapped in a list-item tag (<li>
).
You can just open this html file in your browser to preview our first
page. Without our own CSS styling, the browser uses some default styling
to show the title (<h1>
) bigger, and the list items with bullets.
The next step is to return this page from Flask. Open todolist.py
and replace the code by:
# todolist.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def homepage():
return render_template('todolist.html')
We now also import render_template
. Instead of returning a string
in homepage()
, we now call render_template()
and return the result.
The parameter is the name of the template, in our case just an HTML file.
Flask expects the templates in the templates
folder, so we don’t need
to specify the folder name.
If you run the app (flask --app todolist --debug run
) and visit
http://127.0.0.1:5000
you should see the to-do list.
Jinja #
At the moment we’re returning an HTML page with a hardcoded to-do list.
We should get the particular list for a user, so we somehow have to
retrieve the list (for example from a database) and put it in the
HTML page. Remember that render_template
turns a Jinja template into
HTML? So far our template is just plain HTML. The render_template
can take more parameters, that can be used in the template with some
Jinja markup. We haven’t built the database part, so for now let’s
just add some dummy data to our code and pass it on to the template:
# todolist.py
from flask import Flask, render_template
app = Flask(__name__)
dummy_list = [
'Feed the cat',
'Walk the dog',
'Go to sleep'
]
@app.route('/')
def homepage():
return render_template('todolist.html', todolist=dummy_list)
We’ve created a list of three strings, and added a parameter
to render_template
. With these changes, we now have a variable
todolist
available in our template that contains the dummy data.
Now modify our HTML template to the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>To-Do List</title>
</head>
<body>
<h1>To-Do List Application</h1>
<ul>
{% for item in todolist %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</body>
</html>
The difference is in the content of the unordered list. Instead of having
a list of <li>
tags, we use Jinja code to loop over the strings in todolist
,
which now contains our dummy data. This is a list of strings. In Jinja, you
can loop over a list with {% for item in todolist %}
. The loop ends at
{% endfor %}
. You can recognize the Jinja code by the curly braces:
{% %}
for statements, {{ }}
for expressions, and {# #}
for comments.
By enclosing item
in {{
and }}
, the list item does not contain the
literal text ‘item’, but the value of item
, which is the current string
in our to-do list.
<!-- comment -->
) and Jinja
comments ({# comment #}
) in your template. The difference is that HTML
comments stay in the code, so visitors can see them when they view the
source of the web page. Jinja comments are removed during rendering, so
they will not be sent to the browser.
We also got rid of the fixed length of three items. You can just add more
lines to the dummy_list
and they will show up on your web page. (Don’t
forget to restart the Flask server or use debug mode to apply the changes).
For more about Jinja you can have a look at the Jinja documentation .
In the next section we will make our page look a little better, but
there’s one final change we are going to make. Instead of just a short
line, we want to add some more data to our to-do items. Let’s use our
current dummy data as the title for each item. We will also add a description,
a date for when the item needs to be done, and a status to indicate if
the item is done. We can put each item in a simple dict (you can also
use a namedtuple
). Change our dummy data to:
dummy_list = [
{
'title': 'Feed the cat',
'description': 'We have to keep Blacky happy.',
'date': '2025-08-30',
'is_done': True
},
{
'title': 'Walk the dog',
'description': 'We have to keep Fido happy.',
'date': '2025-08-31',
'is_done': False
},
{
'title': 'Go to sleep',
'description': 'We have to keep ourselves happy.',
'date': '2025-08-30',
'is_done': False
}
]
datetime
from the database later, but we can always convert
it to a string before sending it to the template.
Now, if you run the application, you will see that the list contains the string representation of our dict’s. The Jinja template just puts the entire item in each list item. Let’s fix that.
Change the lines:
{% for item in todolist %}
<li>{{ item }}</li>
{% endfor %}
to:
{% for item in todolist %}
<li>
<b>{{ item.title }}</b><br>
{{ item.description }}<br>
<i>{{ item.date }}</i>
{% if item.is_done %}
done
{% else %}
to-do
{% endif %}
</li>
{% endfor %}
Instead of inserting the entire item dict, we include the values one by one.
In Jinja you can use both item['title']
or item.title
to get the ’title’
element from the ‘item’ dict. I find item.title
a bit more readable.
I’ve added some markup to distinguish the different elements: the title
is now bold (<b>
), the description is on a new line (<br>
), and the
date is also on a new line and in italics (<i>
). After the date, I
display either ‘done’ or ’to-do’, depending on the boolean value of
the is_done
value. The if-else of Jinja is pretty straightforward.
Note that the indentation and new lines are just to make the template
readable. You could also write {% if item.is_done %}done{% else %}to-do{% endif %}
on a single line. Also note that ‘done’ or ’to-do’ is displayed after the
date on our web page, since there is no <br>
after the date.
The current markup is just to distinguish the elements. Now that we can insert data from our Flask app into the web page, it’s time to make our page look a little better. In the next section we will look at CSS. After a short look at what CSS is, we will be using a CSS framework to make a responsive page that looks good on both screens and mobile.