Build a to-do app with Flask


Flask is a really cool Python framework for building web applications. One of its major selling points is how simple it is to get started on building apps with it. In this tutorial, we will build a simple Flask CRUD application, and add realtime functionality to it using the superpowers Pusher grants us.

Our CRUD app is a simple realtime to-do list app that can find use in distributed teams, for example to manage their deliverables.

Here is what the final app will look like:



To follow along properly, basic knowledge of Python, Flask and JavaScript (ES6 syntax) is needed. You will also need the following installed:

Virtualenv is great for creating isolated Python environments, so we can install dependencies (like Flask) in an isolated environment, and not pollute our global packages directory. To install virtualenv:

    pip install virtualenv

Setup and Configuration

Installing Flask

Now, we can create our project folder, activate a virtual environment in it, and install Flask. To activate a virtual environment:

1mkdir realtime-todo
2    cd realtime-todo
3    virtualenv .venv
4    source .venv/bin/activate

To install Flask:

    pip install flask

Setting up Pusher

We will be using Pusher to power our realtime updates. Head over to and register for a free account, if you don’t already have one. Then create an app on the dashboard, and copy out the app credentials (App ID, Key, Secret and Cluster). It is super straight-forward.

We also need to install the Pusher Python Library to communicate with Pusher on the backend:

    pip install pusher

File and Folder Structure

We will keep things super simple and will only create a couple of files. Here’s the file/folder structure used:

1├── realtime-todo
2       ├──
3       ├── static
4       └── templates

The static folder will contain the static files to be used as per Flask standards, and the templates folder will contain the HTML templates for the app. is the main entrypoint for our app and will contain all our server-side code.

Building Our App Backend

Next, we will write code to show a simple view and create endpoints for adding, updating and deleting our to-do's. We will not be persisting the data to a database, but will instead use Pusher events to broadcast data to all users subscribed to our channel.


1# ./
3    from flask import Flask, render_template, request, jsonify
4    from pusher import Pusher
5    import json
7    # create flask app
8    app = Flask(__name__)
10    # configure pusher object
11    pusher = Pusher(
12      app_id='YOUR_APP_ID',
13      key='YOUR_APP_KEY',
14      secret='YOUR_APP_SECRET',
15      cluster='YOUR_APP_CLUSTER',
16      ssl=True
17    )
19    # index route, shows index.html view
20    @app.route('/')
21    def index():
22      return render_template('index.html')
24    # endpoint for storing todo item
25    @app.route('/add-todo', methods = ['POST'])
26    def addTodo():
27      data = json.loads( # load JSON data from request
28      pusher.trigger('todo', 'item-added', data) # trigger `item-added` event on `todo` channel
29      return jsonify(data)
31    # endpoint for deleting todo item
32    @app.route('/remove-todo/<item_id>')
33    def removeTodo(item_id):
34      data = {'id': item_id }
35      pusher.trigger('todo', 'item-removed', data)
36      return jsonify(data)
38    # endpoint for updating todo item
39    @app.route('/update-todo/<item_id>', methods = ['POST'])
40    def updateTodo(item_id):
41      data = {
42        'id': item_id,
43        'completed': json.loads('completed', 0)
44      }
45      pusher.trigger('todo', 'item-updated', data)
46      return jsonify(data)
48    # run Flask app in debug mode

In the code block above, after importing the needed modules and objects and initialising a Flask app, we initialise and configure Pusher. Remember to replace YOUR_APP_ID and similar values with the actual values gotten from the Pusher dashboard for your app. With this pusher object, we can then trigger events on whatever channels we define.

A clear example of this is seen in the addTodo() procedure, where we trigger an item-added event on the todo channel with the trigger method. The trigger method has the following syntax:

    pusher.trigger('a_channel', 'an_event', {'some': 'data'})

You can find the docs for the Pusher Python library here, to get more information on configuring and using Pusher in Python.

In the code above, we also created an index route which is supposed to show our app view by rendering the index.html template. In the next step, we will create this view and start communicating with our Python backend.

Creating Our App View

Now, we create our main app view in ./templates/index.html. This is where the interface for our app will live.

First we will pull CSS for TodoMVC apps to take advantage of some pre-made to-do list app styles, and store the folder in the ./static folder.

Next, we can write the basic markup for the view:

1<!-- ./templates/index.html -->
2    <html>
3    <head>
4      <!-- link to the Todo MVC index.css file -->
5      <link rel="stylesheet" href="/static/todomvc-app-css/index.css">
6      <title>Realtime Todo List</title>
7    </head>
9    <body>
10      <section class="todoapp">
11        <header class="header">
12          <h1>Todos</h1>
13          <input class="new-todo" placeholder="What needs to be done?" 
14            autofocus="" onkeypress="addItem(event)">
15        </header>
17        <section class="main">
18          <ul class="todo-list"></ul>
19        </section>
21        <footer class="footer"></footer>  
22      </section>
23    </body>
24    </html>

In the above markup, notice we added an addItem() function to be called onkeypress for the .new-todo input. In the following steps we will define this function, as well as other JavaScript functions to handle the basic app functions and interact with our Python backend.

Creating, Removing and Updating To-do Items

Now, we can add the JavaScript code to interact with the to-do items. Whenever an item is to be added, removed or updated, we will make API calls to our backend to affect those changes. We will do this with the simple and intuitive Fetch API:

1<!-- ./templates/index.html -->
2    <html>
3      <!-- // ... -->
4      <script>
5        // function that makes API call to add an item
6        function addItem(e) {
7          // if enter key is pressed on the form input, add new item
8          if (e.which == 13 || e.keyCode == 13) {
9            let item = document.querySelector('.new-todo');
10            fetch('/add-todo', {
11              method: 'post',
12              body: JSON.stringify({ 
13                id: `item-${}`,
14                value: item.value,
15                completed: 0
16              })
17            })
18            .then(resp => {
19              // empty form input once a response is received
20              item.value = ""
21            });
22          }
23        }
25        // function that makes API call to remove an item
26        function removeItem(id) {
27          fetch(`/remove-todo/${id}`);
28        }
30        // function that makes API call to update an item 
31        // toggles the state of the item between complete and
32        // incomplete states
33        function toggleComplete(elem) {
34          let id =,
35              completed = (elem.dataset.completed == "1" ? "0" : "1");
36          fetch(`/update-todo/${id}`, {
37            method: 'post',
38            body: JSON.stringify({ completed })
39          });
40        }
42        // helper function to append new ToDo item to current ToDo list
43        function appendToList(data) {
44          let html = `
45            <li id="${}">
46              <div class="view">
47                <input class="toggle" type="checkbox" onclick="toggleComplete(this)" 
48                  data-completed="${data.completed}" data-id="${}">
49                <label>${data.value}</label>
50                <button class="destroy" onclick="removeItem('${}')"></button>
51              </div>
52            </li>`;
53          let list = document.querySelector(".todo-list")
54          list.innerHTML += html;
55       };
56      </script>
57    </body>
58    </html>

Note: The JavaScript Fetch API is great for making AJAX requests, although it requires a polyfill for older browsers. A great alternative is axios.

In the above block of code, we define 4 functions to help us interact with the items on our to-do list. The addItem() function makes a POST API call to add a new item to the to-do list, with the value from our input field, we also try to mock a unique ID for each item by assigning them a value of item-${} (ideally this would be implemented by our data store, but it is beyond the scope of this tutorial). Lastly, we assign an initial state of 0 to the completed property for each item, this is to show that the item is just added, and has not yet been completed.

The removeItem() function makes a request to delete an item, while the toggleComplete() function makes a request to update the completed property of an item. An appendToList() helper function is also defined to update our to-do list with new items, this helper function will be used in the next step when we start listening for events.

Listening For Events

In this step we will listen for events from Pusher, and update our app view based on the data received. Updating index.html:

1<!-- ./templates/index.html -->
2    <html>
3      <!-- .// -->
4      <script src=""></script>
6      <script>
7        // Enable pusher logging for debugging - don't include this in production
8        Pusher.logToConsole = true;
10        // configure pusher
11        const pusher = new Pusher('YOUR_APP_KEY', {
12          cluster: 'eu', // gotten from Pusher app dashboard
13          encrypted: true // optional
14        });
16        // subscribe to `todo` public channel, on which we'd be broadcasting events
17        const channel = pusher.subscribe('todo');
19        // listen for item-added events, and update todo list once event triggered
20        channel.bind('item-added', data => {
21          appendToList(data);
22        });
24        // listen for item-removed events
25        channel.bind('item-removed', data => {
26          let item = document.querySelector(`#${}`);
27          item.parentNode.removeChild(item);
28        });
30        // listen for item-updated events
31        channel.bind('item-updated', data => {
32          let elem = document.querySelector(`#${} .toggle`);
33          let item = document.querySelector(`#${}`);
34          item.classList.toggle("completed");
35          elem.dataset.completed = data.completed;
36          elem.checked = data.completed == 1;
37        });
39        // ...
40      </script>
41    </body>
42    </html>

The first thing we have to do here is to include the pusher-js library to help us communicate with the Pusher service. Next, we initialise the Pusher service by passing in our App Key, and some other options — for a full list of configuration options, you can check the docs here.

After successfully initialising Pusher and assigning it to the pusher object we can then subscribe to the channel from which we want to receive events, in our case that’s the public todo channel:

    const channel = pusher.subscribe('todo');

Note: Pusher provides various types on channels, including Public, Private and Presence channels. Read about them here.

Finally, we bind the various events we’re listening for on the channel. The bind() method has the following syntax - channel.bind(event_name, callback_function)

Optionally, we can add a loader to the page, which would show whenever a request is made. The final index.html file would look like this, and our app should be ready now!

To run our app:


And here is what the demo looks like:



In this tutorial, we have learned how to build a Python Flask project from scratch and add realtime functionality to it using Pusher and Vanilla JavaScript. The entire code for this tutorial is hosted on GitHub.

There are many other use cases for adding realtime functionality to Python applications. Do you have any more improvements, suggestions or use cases? Let us know in the comments!