Créer une application todo avec Flask

Dans ce guide, je te montre comment construire une application todolist en partant de zéro avec Flask

Publié le par Thibault Houdon (mis à jour le )
paceTemps de lecture estimé : 60 minutes

Flask est un micro-framework développé avec Python et maintenu par la team PalletsProjects.

C'est aussi eux qui sont derrière des librairies bien connues comme Click qui te permet de créer des interfaces en ligne de commandes.

Flask est intéressant car il va te permettre de construire les fondations de ton application web très rapidement sans t'imposer quoi que ce soit !

Personnellement, c'est une librairie que j'aime beaucoup. Elle est très accessible et facile à prendre en main. Cela permet de bien intégrer certains concepts de base avant de se plonger dans une librairie plus complexe comme Django.

👉 Je trouve que c'est juste parfait si tu débutes dans le développement web !

Aujourd'hui, on va voir les fondamentaux de Flask au travers d'une todo-app qu'on va développer en partant de zéro.

C'est un projet que j'adore réaliser à chaque fois que je découvre une nouvelle techno, tu verras que c'est très instructif.

On y va !

Un peu de configuration

Tout ce que je montre dans cet article est réalisé sous macOS. Donc si tu es sur Windows ou Linux, tu devras adapter certaines commandes que j'utilise dans le terminal.Aller, le premier truc qu'on va faire, c'est créer un nouveau répertoire pour notre application, créer un nouvel environnement virtuel et y installer flask.

mkdir todo_app && cd todo_app
python -m venv env
source env/bin/activate
pip install flask

Tu vas ensuite ouvrir ça dans ton éditeur de code préféré, pour ma part ce sera sur Visual Studio Code.

On va créer un fichier app.py à l'intérieur de notre dossier todo_app et y écrire notre première méthode :

# todo_app/app.py

from flask import Flask

app = Flask(__name__) # Crée une instance de la classe Flask, c'est notre application 

@app.route("/")
def index(): # Méthode appelée quand on se rend sur la route "/"
    return "Hello World!"

Pour lancer l'application, il faut d'abord indiquer à Flask ce qu'il doit exécuter en exportant ces deux variables d'environnement :

export FLASK_APP=app.py
export FLASK_ENV=development

Et pour lancer le serveur, c'est très simple :

flask run

Tu n'as plus qu'à cliquer sur l'adresse de ton serveur local qui s'affiche dans ton terminal.

Si tout s'est bien passé de ton côté, tu devrais pouvoir lire 'Hello World' sur ton écran 👍

La structure de notre application

Notre application sera très simple.

Le but étant de te faire découvrir Flask, je ne vais pas t'embêter avec des notions trop avancées.

Cela pourra faire l'objet d'un autre article si celui-ci a du succès !

Du coup, on aura les quatre fonctionnalités de base de toute application :

  • Ajouter un élément
  • Afficher des éléments
  • Mettre à jour un élément
  • Supprimer un élément

C'est ce qu'on appelle un CRUD (Create, Read, Update, Delete) et c'est tout ce dont on va avoir besoin aujourd'hui.

Pour stocker nos données, on va utiliser SQLite3 qui est déjà disponible dans la librairie standard de Python et aussi une extension Flask qui s'appelle flask-sqlalchemy pour gérer nos interactions avec la base de données.

Du coup, on retourne rapidement dans le terminal pour installer ça (n'oublie pas d'activer ton environnement virtuel) :

pip install flask_sqlalchemy 

Une fois que c'est fait, on va dans notre fichier app.py pour définir le modèle qui représentera notre base de données :

# todo_app/app.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__) # Crée un instance de la classe Flask, c'est notre app
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db' # Nom de la bdd
db = SQLAlchemy(app) # Lie notre app à SQLAlchemy

class Task(db.Model): # Modèle
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)

@app.route('/') 
def index(): # Méthode appelée lorsqu'on se rend sur la route '/'
    return 'Hello World!'

J'ai importé SQLAlchemy depuis flask_sqlalchemy et également le module datetime.

from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

Ensuite j'ai indiqué le nom de la base de données que je veux créer et je l'ai assigné à une variable de configuration SQLALCHEMY_DATABASE_URI dans le dictionnaire app.config.

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db' # Nom de la bdd

Derrière, j'ai lié mon application à SQLAlchemy (db = SQLAlchemy(app)) et j'ai créé une classe Task qui hérite de db.Model dans laquelle j'ai défini tous les champs qui vont composer ma table.

  • Un champ id pour stocker l'identifiant de la tâche.
  • Un champ name pour le nom.
  • Un champ created_at pour stocker la date de création de chaque tâche.
class Task(db.Model): # Modèle
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)

Le modèle est écrit, il ne reste plus qu'à créer la base de données !

Pour ça, ré-ouvre ton terminal, lance python et exécute cette commande :

>>> from app import db, Task
>>> db.create_all()

La méthode create_all() va créer la table en se basant sur la définition écrite dans notre modèle Task, plutôt sympa !

Pour vérifier que tout a fonctionné correctement, on peut aller jeter un oeil dans sqlite3 :

sqlite3 todo.db
sqlite> .table
task
sqlite> .schema task
CREATE TABLE task (
        id INTEGER NOT NULL, 
        name VARCHAR(50) NOT NULL, 
        created_at DATETIME NOT NULL, 
        PRIMARY KEY (id)
);

Tout semble ok, on va donc ajouter quelques tâches d'exemple pour peupler notre table !

On retourne dans l'interpréteur python :

>>> from app import db, Task
>>> task1 = Task(name='Apprendre Python')
>>> task2 = Task(name='Faire les courses')
>>> task3 = Task(name='Sortir le chien')
>>> db.session.add(task1)
>>> db.session.add(task2)
>>> db.session.add(task3)
>>> db.session.commit()

On continue !

Afficher des éléments

Maintenant qu'on a fait ça, on aimerait bien pouvoir lire ce qu'il y a dans notre base de données et les afficher sur l'écran de l'utilisateur.

Pour ça, on va interroger notre base via notre modèle Task pour récupérer toutes les tâches puis envoyer tout ça vers un template HTML.

Je t'explique juste après !

# todo_app/app.py
from flask import render_template

@app.route('/')
def index():
    tasks = Task.query.order_by(Task.created_at).all()
    return render_template('index.html', tasks=tasks)

Je fais une requête vers ma base de donnée avec la méthode query, je précise que je veux les récupérer par ordre de création avec order_by() puis que je veux tout récupérer avec la méthode all() :

tasks = Task.query.order_by(Task.created_at).all()

Ensuite j'utilise la fonction render_template de Flask pour envoyer les données vers un template (n'oublie pas de l'importer).

Le premier paramètre doit correspondre au nom du template et ce qui vient derrière, c'est toutes les données que tu veux transmettre :

return render_template('index.html', tasks=tasks)

Pour le coup, on veut juste envoyer notre liste de tâches !

Maintenant, on va créer ce template en HTML ! Dans ton répertoire todo_app, crée un nouveau dossier templates puis un fichier index.html à l'intérieur de ce dossier.

Tu dois obligatoirement appelé ton dossier 'templates', c'est une des rares conventions de Flask. Je te laisse créer ce template rapidement avec le style qui te convient ! Ou tu peux utiliser le mien si tu préfères :

<!-- todo_app/templates/index.html -->

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo App</title>
</head>

<body>
<main>
    <div id="tasks">
        {% if tasks %}
            {% for task in tasks %}
                <div id="task">
                    <p>{{ task.name }}</p>
                </div>
            {% endfor %}
        {% else %}
            <p style="font-size: 16px; text-align: center;">Super, vous n'avez plus rien à faire ✌️</p>
        {% endif %}
    </div>
</main>
</body>

</html>

'utilise le moteur de templating Jinja qui est installé avec Flask pour pouvoir implémenter un peu de logique en Python dans mon template.

Dans mon cas, je fais une boucle for pour itérer sur ma liste de tâches et j'affiche simplement le nom de chaque tâche. Si ma liste est vide j'affiche un message d'information.

Ce que tu dois retenir quant à l'utilisation de Jinja, c'est :

  • {{ ... }} → Pour afficher des données que tu as envoyé dans la vue avec la fonction render_template.
  • {% ... %} → Pour créer des boucles, structures conditionnelles, bref la logique de ton code.

À ce stade, tu devrais pouvoir afficher la liste de toutes les tâches dans ton navigateur, essaie un peu pour voir si tout fonctionne correctement !

Ajouter un élément

On rentre dans le vif du sujet !

Pour ajouter un élément dans notre todo_app, on va avoir besoin d'un formulaire dans lequel l'utilisateur pourra rentrer sa tâche.

<!-- todo_app/templates/index.html -->

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo App</title>
</head>

<body>
<main>
    <form action="{{ url_for('index') }}" , method="POST">
        <input type="text" name="name" id="task" placeholder="Faire les courses...">
        <input type="submit" value="Add">
    </form>
    <div id="tasks">
        {% if tasks %}
            {% for task in tasks %}
                <div id="task">
                    <p>{{ task.name }}</p>
                </div>
            {%endfor %}
        {% else %}
            <p style="font-size: 16px; text-align: center;">Super, vous n'avez plus rien à faire ✌️</p>
        {% endif %}
    </div>
</main>
</body>

</html>

Le truc important à noter ici, c'est ce qu'il y a dans l'attribut action de mon formulaire :

{{ url_for('index') }}

Quand j'écris ça, j'indique à Flask quelle route utiliser pour traiter les données du formulaire. Pour le reste, c'est un formulaire tout ce qu'il y a de plus classique !

url_for va appelé notre vue index (vu qu'on lui a passé la chaîne de caractères 'index'). C'est dans cette vue que l'on va traiter les données du formulaire.

Je te conseille d'ailleurs de toujours utiliser url_for pour appeler tes vues ! N'écris jamais les routes en dur.

Et du coup, du côté de notre fichier app.py :

from flask import Flask, request, redirect, url_for, render_template

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        name = request.form.get('name')
        task = Task(name=name)
        db.session.add(task)
        db.session.commit()
        return redirect(url_for('index'))
    else:
        tasks = Task.query.order_by(Task.created_at).all()
    return render_template('index.html', tasks=tasks)

Il se passe pas mal de choses ici !

Je dois d'abord préciser les méthodes HTTP autorisées sur ma route '/'. Comme on reçoit les données d'un formulaire, je dois pouvoir récupérer du GET mais aussi du POST. Je l'indique donc dans le décorateur au paramètre methods :

@app.route('/', methods=['GET', 'POST'])

Ensuite, je vais récupérer les informations du formulaire grâce à request :

name = request.form.get('name')

C'est une instance de la classe Request contenue dans Flask et qui nous permet de récupérer des données entrantes comme les arguments, la route utilisée ou dans notre cas les données envoyées par un formulaire.

Après ça, je vais créer cette tâche dans ma base de données en passant par mon modèle Task :

task = Task(name=name)

Il ne faut pas oublier d'ajouter notre objet à la session puis de faire un db.session.commit() pour enregistrer la tâche dans la base de données :

db.session.add(task)
db.session.commit()

Enfin, il ne me reste plus qu'à rediriger l'utilisateur vers la page d'accueil en utilisant la fonction redirect et url_for que tu connais déjà.

Je te laisse ajouter quelques tâches !

Styliser le formulaire

Avant de passer à la suite, je vais te donner accès à ma feuille de style pour que notre application ressemble à quelque chose !

Pour cela, tu dois créer un dossier static à la racine de ton projet (comme on l'avait fait pour le dossier templates).

Ensuite, tu peux créer un nouveau fichier style.css dans ce dossier static.

Le dossier static est destiné à contenir tous les fichiers statiques justement : feuilles de style, images, etc..

On y stocke aussi les fichiers Javacript Tu dois obligatoirement appeler ce fichier static, c'est un autre convention de Flask. Tu es libre de faire ce que tu veux, c'est juste au cas où 🙂

body {
    background-color: #F7FAFC;
    display: flex;
    justify-content: center;
    margin-top: 10%;
    font-family: Arial, Helvetica, sans-serif;
}

main {
    display: flex;
    flex-direction: column;
    background-color: white;
    height: 100%;
}

form {
    display: flex;
}

input[type=text] {
    padding: 16px 32px;
    box-sizing: border-box;
    width: 100%;
    font-size: 16px;
}

input::placeholder {
    font-size: 16px;
}

input:focus {
    outline: none;
}

input[type="submit"] {
    background-color: #1E1E2D;
    border: none;
    color: white;
    padding: 16px 32px;
    text-decoration: none;
    cursor: pointer;
    font-size: 16px;
}

#tasks {
    display: flex;
    flex-direction: column;
    width: 100%;
}

#task {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: baseline;
    padding-left: 20px;
    padding-right: 20px;

}

a {
    color: #1E1E2D;
    font-size: 16px;
    font-weight: 600;
    text-decoration: none;
}

p {
    font-size: 16px;
}

Tu vois, j'ai personnalisé un peu les input de mon formulaire et j'ai joué avec flexbox pour centrer mes différents éléments !

Une fois que tu as écrit le code CSS, il faut lier tes templates à ce fichier CSS pour que le style s'applique.

Pour ça, ajoute la ligne suivante dans le fichier index.html, au niveau de la balise <head>:

<link rel="stylesheet" href="{{ url_for('static', filename='style.css')}}">

On utilise la fonction url_for() pour dire à Flask : "Hé, voilà le dossier static qui contient mes feuilles de style. Je veux que tu appliques les styles du fichier style.css."

Avec ça, ta todo app devrait ressembler à ça :

Mettre à jour un élément

Pour mettre un jour notre liste de tâches, nous allons créer une nouvelle route dans le fichier app.py.

@app.route('/update/', methods=['GET', 'POST'])
def update(id):
    task = Task.query.get_or_404(id)
    if request.method == 'POST':
        task.name = request.form.get('name')
        db.session.commit()
        return redirect(url_for('index'))
    else:
        return render_template('update.html', task=task)

Petite nouveauté sur cette route, les urls dynamiques !

Si tu regardes au niveau de mon décorateur :

@app.route('/update/', methods=['GET', 'POST'])

Tu peux ajouter des sections de variables à une URL en marquant les sections avec <nom_variable>.</nom_variable>

Tu peux ainsi récupérer dans ta fonction le <nom_variable> comme un argument.</nom_variable>

En option, tu peux utiliser un convertisseur pour spécifier le type d'argument comme convertisseur:nom_variable. Ici, je précise que mon id doit être un nombre avec int : <int:id>.

Pour le reste, ça doit commencer à te parler. On récupère la tâche dans notre base de données grâce à l'id justement puis on met à jour le nom de la tâche avec les données reçues du formulaire.

Du côté de notre template, on va juste ajouter un lien qui appelera cette route :

<!-- todo_app/templates/index.html -->

<main>
    <form action="{{ url_for('index') }}" , method="POST">
        <input type="text" name="name" id="task" placeholder="Faire les courses...">
        <input type="submit" value="Add">
    </form>
    <div id="tasks">
        {% if tasks %}
            {% for task in tasks %}
                <div id="task">
                    <p>{{ task.name }}</p>
                    <!-- Lien pour mettre à jour une tâche -->
                    <a href="{{ url_for('update', id=task.id) }}">Update</a>
                </div>
            {% endfor %}
        {% else %}
             <p style="font-size: 16px; text-align: center;">Super, vous n'avez plus rien à faire ✌️</p>
        {% endif %}
    </div>
</main>

Là encore, on utilise url_for. On précise le nom de la route ('update') et on passe l'id de notre tâche au paramètre id de notre route :

<a href="{{ url_for('update', id=task.id) }}">Update</a>

Du ménage dans nos templatesAvant de créer notre dernier template update.html, on va faire du ménage et séparer les choses. Ça va me permettre de te parler un peu d'héritage entre les templates.

D'abord, on va créer un fichier base.html dans le dossier templates :

<!-- todo_app/templates/base.html -->

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css')}}">
    <title>Todo App</title>
</head>

<body>
    {% block main %}

    {% endblock main %}
</body>

</html>

Ce fichier sera le squelette principal de mon application et tu verras que tous mes autres templates vont l'utiliser.

Pour que ça fonctionne, je dois créer un block ! Cela indique à Flask qu'il doit remplir dynamiquement cette partie.

Du coup, du côté du fichier index.html qu'on utilise depuis le début, je peux enlever quelques parties :

<!-- todo_app/templates/index.html -->

{% extends 'base.html' %}
{% block main %}
<main>
    {% include './partials/_form.html' %}
    <div id="tasks">
        {% if tasks %}
        {% for task in tasks %}
        <div id="task">
            <p>{{ task.name }}</p>
            <div>
                <a href="{{ url_for('update', id=task.id) }}">Update</a>
            </div>
        </div>
        {% endfor %}
        {% else %}
        <p style="font-size: 16px; text-align: center;">Super, vous n'avez plus rien à faire ✌️</p>
        {% endif %}
    </div>
</main>
{% endblock main %}

J'utilise extends pour dire que je veux hériter du template base.html. Ensuite j'indique le nom du block que je veux utiliser et je mets mon code html à l'intérieur.

Tu as aussi sans doute remarquer que le formulaire avait disparu !

C'est parce que je l'ai lui aussi mis dans un fichier à part que j'appelle dans ce template là avec include :

{% include './partials/_form.html' %}

Dans ton dossier templates, crée un nouveau dossier partial puis un fichier _form.html.

Tu n'es pas obligé d'appeler ce dossier et ce fichier comme ça, c'est juste une convention personnelle que je m'impose pour m'y retrouver !

<!-- todo_app/templates/partials/_form.html -->

<form action="{{ url_for('index') }}" , method="POST">
    <input type="text" name="name" id="task" placeholder="Faire les courses...">
    <input type="submit" value="Add">
</form>

Maintenant il ne reste plus qu'à créer notre template update.html qui va fortement ressembler au formulaire du fichier index.html:

{% extends 'base.html' %}
{% block main %}
<main>
    <form action="{{ url_for('update', id=task.id) }}" , method="POST">
        <input type="text" name="name" id="task" value="{{ task.name }}">
        <input type="submit" value="Update">
    </form>
</main>
{% endblock main %}

On pourrait faire en sorte d'utiliser un seul formulaire pour l'ajout et la mise à jour mais cela impliquerait que je te parle de la notion de contexte d'application en Flask.

C'est une notion trop avancé pour ce tutoriel mais si jamais ça t'intéresses, je te conseille d'aller lire la documentation officielle, elle est très bien faite et ça pourrait être un bon exercice pour toi !

Supprimer un élément

Pour supprimer un élément de notre todo list, on va procéder quasiment de la même façon :

@app.route('/delete/')
def delete(id):
    task = Task.query.get_or_404(id)
    db.session.delete(task)
    db.session.commit()
    return redirect(url_for('index'))

Ici, pas besoin de préciser de méthode HTTP puisqu'on va appeler cette route depuis un lien, donc en GET. On va récupérer notre tâche encore une fois grâce à l'id passé en paramètre puis supprimer notre tâche de la base de données grâce à la méthode delete.

On ajoute un lien dans notre template index.html :

<!-- todo_app/templates/index.html -->

<main>
    <form action="{{ url_for('index') }}" , method="POST">
        <input type="text" name="name" id="task" placeholder="Faire les courses...">
        <input type="submit" value="Add">
    </form>
    <div id="tasks">
        {% if tasks %}
            {% for task in tasks %}
                <div id="task">
                    <p>{{ task.name }}</p>
                    <a href="{{ url_for('update', id=task.id) }}">Update</a>
                    <a href="{{ url_for('delete', id=task.id) }}">Delete</a>
               </div>
            {% endfor %}
       {% else %}
           <p style="font-size: 16px; text-align: center;">Super, vous n'avez plus rien à faire ✌️</p>
       {% endif %}
    </div>
</main>

Et normalement tout fonctionne !

Tu devrais maintenant pouvoir ajouter, supprimer et mettre à jour des éléments 👍

Débrief

Bravo à toi d'être arrivé jusque là 👏

J'espère que tu as pris plaisir à découvrir Flask et que tu vas l'utiliser dans tes futurs projets !

Alors évidemment qu'il y aurait des dizaines de choses à améliorer dans cette application mais au moins je t'ai montré les bases essentielles pour utiliser ce micro-framework et c'est ce que je voulais.

Pour continuer ton apprentissage, je te conseille fortement de commencer par aller lire la documentation, en particulier les parties QuickStart et Tutorial. Ça va te permettre de revoir et de consolider ce qu'on a vu ensemble !

Après ça, crée toi quelques projets pour être à l'aise et maîtriser les fondamentaux, c'est ultra important.

Ensuite, tu pourras commencer à t'intéresser à des concepts plus avancés comme les Blueprints ou le Contexte d'Application. Tu verras que ces concepts te permettront de faire passer ton application au niveau supérieur ! 🚀

Pour terminer, quelques ressources en vrac qui m'ont bien servi :

Amuse toi bien et à la prochaine !