40  TP récapitulatif — Gestionnaire de tâches ToDo

Ce Travail Pratique mobilise l’ensemble des notions du niveau Avancé : compréhensions, *args/**kwargs, lambdas, POO, exceptions, fichiers JSON, complexité, et même match/case. Vous allez construire un vrai petit programme qu’on peut réutiliser.

🎯 Objectifs pédagogiques
  • Concevoir une application orientée objet complète avec classes et héritage.
  • Gérer proprement les erreurs avec des exceptions personnalisées.
  • Persister les données en JSON.
  • Utiliser les compréhensions et match/case à bon escient.
  • Produire du code documenté, testable, extensible.

Temps estimé : 2 h 30.

40.1 Contexte

Vous devez créer un gestionnaire de tâches (style ToDo-list) avec :

  • Des tâches ayant un titre, une priorité, un statut, une date de création, une échéance optionnelle.
  • Une liste de tâches qui les regroupe, offre des filtres, du tri, des statistiques.
  • Une persistance JSON pour sauvegarder et recharger entre deux sessions.
  • Des exceptions personnalisées pour les cas d’erreur.

Vous allez avancer progressivement en 5 parties.

Méthode
  1. Un fichier .py pour travailler (todo.py).
  2. Avancez partie par partie, testez après chaque.
  3. Ne copiez pas les solutions avant d’avoir essayé.
  4. Utilisez votre mémoire : quand vous bloquez, retournez au chapitre concerné, pas à Google.

Partie 1 — La classe Tache

Consignes

Créez une classe Tache avec @dataclass :

  • titre (str) — obligatoire.
  • priorite (str) — "basse", "moyenne" ou "haute", défaut "moyenne".
  • statut (str) — "à faire", "en cours" ou "terminée", défaut "à faire".
  • date_creation (str au format ISO) — généré automatiquement à la création.
  • echeance (str ou None) — date optionnelle au format "JJ/MM/AAAA".

Ajoutez :

  • Une méthode est_terminee() renvoyant un booléen.
  • Une méthode __str__ affichant "⏳ [haute] Appeler le plombier" (avec un emoji selon le statut : ⏳ à faire, 🔄 en cours, ✅ terminée).

Exceptions à créer

  • PrioriteInvalideError si la priorité n’est pas dans les 3 valeurs acceptées.
  • StatutInvalideError si le statut n’est pas valide.

Ces validations se feront dans une méthode __post_init__ (appelée par dataclass après __init__).

from dataclasses import dataclass, field
from datetime import datetime


class PrioriteInvalideError(Exception):
    """Levée quand la priorité n'est pas dans ('basse', 'moyenne', 'haute')."""
    pass


class StatutInvalideError(Exception):
    """Levée quand le statut n'est pas valide."""
    pass


PRIORITES_VALIDES = ("basse", "moyenne", "haute")
STATUTS_VALIDES = ("à faire", "en cours", "terminée")
EMOJIS = {"à faire": "⏳", "en cours": "🔄", "terminée": "✅"}


@dataclass
class Tache:
    titre: str
    priorite: str = "moyenne"
    statut: str = "à faire"
    date_creation: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
    echeance: str | None = None

    def __post_init__(self):
        if self.priorite not in PRIORITES_VALIDES:
            raise PrioriteInvalideError(
                f"Priorité '{self.priorite}' invalide. Valeurs : {PRIORITES_VALIDES}"
            )
        if self.statut not in STATUTS_VALIDES:
            raise StatutInvalideError(
                f"Statut '{self.statut}' invalide. Valeurs : {STATUTS_VALIDES}"
            )

    def est_terminee(self):
        return self.statut == "terminée"

    def __str__(self):
        emoji = EMOJIS[self.statut]
        echeance_str = f" (échéance {self.echeance})" if self.echeance else ""
        return f"{emoji} [{self.priorite}] {self.titre}{echeance_str}"


# Tests
t1 = Tache("Appeler le plombier", priorite="haute")
t2 = Tache("Faire les courses", echeance="25/04/2026")
t3 = Tache("Rendre le rapport", statut="terminée", priorite="basse")

print(t1)
print(t2)
print(t3)
print(f"Est terminée ? {t3.est_terminee()}")
⏳ [haute] Appeler le plombier
⏳ [moyenne] Faire les courses (échéance 25/04/2026)
✅ [basse] Rendre le rapport
Est terminée ? True

Tests des exceptions

try:
    t = Tache("Test", priorite="extrême")
except PrioriteInvalideError as e:
    print(f"Refusé : {e}")

try:
    t = Tache("Test", statut="oublié")
except StatutInvalideError as e:
    print(f"Refusé : {e}")
Refusé : Priorité 'extrême' invalide. Valeurs : ('basse', 'moyenne', 'haute')
Refusé : Statut 'oublié' invalide. Valeurs : ('à faire', 'en cours', 'terminée')

Points à retenir

  • @dataclass génère __init__, __repr__, __eq__ automatiquement.
  • __post_init__ est appelée après __init__ — idéale pour les validations.
  • field(default_factory=...) pour générer la date à chaque création (et non au chargement).
  • Python 3.10 : str | None pour indiquer un type optionnel.
  • Les exceptions personnalisées héritent d’Exception et portent un nom finissant par Error.

Partie 2 — La classe ListeTaches

Consignes

Créez une classe ListeTaches qui gère une collection de tâches.

Attributs et constructeur

  • taches : liste de Tache (vide par défaut).

Méthodes de base

  • ajouter(tache) : ajoute une tâche à la fin.
  • supprimer(index) : retire la tâche à cet indice. Lève IndexError si hors limites.
  • __len__() : nombre de tâches.
  • __str__() : affiche toutes les tâches numérotées.

Méthodes de consultation

  • filtrer_par_statut(statut) : liste des tâches avec ce statut.
  • filtrer_par_priorite(priorite) : idem pour la priorité.
  • taches_terminees() / taches_en_cours() / taches_a_faire() : raccourcis.
from dataclasses import dataclass, field
from datetime import datetime


class PrioriteInvalideError(Exception): pass
class StatutInvalideError(Exception): pass

PRIORITES_VALIDES = ("basse", "moyenne", "haute")
STATUTS_VALIDES = ("à faire", "en cours", "terminée")
EMOJIS = {"à faire": "⏳", "en cours": "🔄", "terminée": "✅"}


@dataclass
class Tache:
    titre: str
    priorite: str = "moyenne"
    statut: str = "à faire"
    date_creation: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
    echeance: str | None = None

    def __post_init__(self):
        if self.priorite not in PRIORITES_VALIDES:
            raise PrioriteInvalideError(f"Priorité '{self.priorite}' invalide")
        if self.statut not in STATUTS_VALIDES:
            raise StatutInvalideError(f"Statut '{self.statut}' invalide")

    def est_terminee(self):
        return self.statut == "terminée"

    def __str__(self):
        emoji = EMOJIS[self.statut]
        ech = f" (échéance {self.echeance})" if self.echeance else ""
        return f"{emoji} [{self.priorite}] {self.titre}{ech}"


class ListeTaches:
    def __init__(self):
        self.taches = []              # IMPORTANT : dans __init__, pas en classe !

    def ajouter(self, tache):
        self.taches.append(tache)

    def supprimer(self, index):
        if not 0 <= index < len(self.taches):
            raise IndexError(f"Indice {index} hors limites (0 à {len(self.taches)-1})")
        return self.taches.pop(index)

    def __len__(self):
        return len(self.taches)

    def __str__(self):
        if not self.taches:
            return "(liste vide)"
        lignes = [f"{i}. {t}" for i, t in enumerate(self.taches)]
        return "\n".join(lignes)

    def filtrer_par_statut(self, statut):
        return [t for t in self.taches if t.statut == statut]

    def filtrer_par_priorite(self, priorite):
        return [t for t in self.taches if t.priorite == priorite]

    def taches_terminees(self):
        return self.filtrer_par_statut("terminée")

    def taches_en_cours(self):
        return self.filtrer_par_statut("en cours")

    def taches_a_faire(self):
        return self.filtrer_par_statut("à faire")


# Tests
liste = ListeTaches()
liste.ajouter(Tache("Appeler le plombier", priorite="haute"))
liste.ajouter(Tache("Faire les courses", echeance="25/04/2026"))
liste.ajouter(Tache("Rendre le rapport", statut="en cours", priorite="basse"))
liste.ajouter(Tache("Répondre à Bob", statut="terminée"))

print(liste)
print(f"\nTotal : {len(liste)} tâches")

print("\nÀ faire :")
for t in liste.taches_a_faire():
    print(f"  {t}")

print("\nPriorité haute :")
for t in liste.filtrer_par_priorite("haute"):
    print(f"  {t}")
0. ⏳ [haute] Appeler le plombier
1. ⏳ [moyenne] Faire les courses (échéance 25/04/2026)
2. 🔄 [basse] Rendre le rapport
3. ✅ [moyenne] Répondre à Bob

Total : 4 tâches

À faire :
  ⏳ [haute] Appeler le plombier
  ⏳ [moyenne] Faire les courses (échéance 25/04/2026)

Priorité haute :
  ⏳ [haute] Appeler le plombier

Points à retenir

  • Attribut d’instance self.taches = [] dans __init__ (jamais en attribut de classe mutable !).
  • Compréhensions dans les filtres — bien plus lisible qu’une boucle classique.
  • __len__ fait fonctionner len(liste).
  • __str__ permet print(liste) de façon lisible.
  • raise IndexError avec un message explicite pour aider le débogage.

Partie 3 — Changement de statut et tri

Consignes

Ajoutez à Tache :

  • marquer_en_cours() : passe le statut à "en cours".
  • marquer_terminee() : passe à "terminée".
  • reouvrir() : repasse à "à faire".

Ajoutez à ListeTaches :

  • trier_par_priorite() : renvoie une nouvelle liste triée par priorité (haute en premier).
  • trier_par_date_creation() : par date de création (plus ancienne en premier).
  • statistiques() : renvoie un dict avec nb_total, nb_a_faire, nb_en_cours, nb_terminees, pourcentage_termine.

Pour le tri par priorité, il faut définir un ordre personnalisé (haute > moyenne > basse). On utilise un dict de rang :

from dataclasses import dataclass, field
from datetime import datetime


class PrioriteInvalideError(Exception): pass
class StatutInvalideError(Exception): pass

PRIORITES_VALIDES = ("basse", "moyenne", "haute")
STATUTS_VALIDES = ("à faire", "en cours", "terminée")
EMOJIS = {"à faire": "⏳", "en cours": "🔄", "terminée": "✅"}
ORDRE_PRIORITES = {"haute": 0, "moyenne": 1, "basse": 2}


@dataclass
class Tache:
    titre: str
    priorite: str = "moyenne"
    statut: str = "à faire"
    date_creation: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
    echeance: str | None = None

    def __post_init__(self):
        if self.priorite not in PRIORITES_VALIDES:
            raise PrioriteInvalideError(f"Priorité '{self.priorite}' invalide")
        if self.statut not in STATUTS_VALIDES:
            raise StatutInvalideError(f"Statut '{self.statut}' invalide")

    def est_terminee(self):
        return self.statut == "terminée"

    def marquer_en_cours(self):
        self.statut = "en cours"

    def marquer_terminee(self):
        self.statut = "terminée"

    def reouvrir(self):
        self.statut = "à faire"

    def __str__(self):
        emoji = EMOJIS[self.statut]
        ech = f" (échéance {self.echeance})" if self.echeance else ""
        return f"{emoji} [{self.priorite}] {self.titre}{ech}"


class ListeTaches:
    def __init__(self):
        self.taches = []

    def ajouter(self, tache):
        self.taches.append(tache)

    def supprimer(self, index):
        if not 0 <= index < len(self.taches):
            raise IndexError(f"Indice {index} hors limites")
        return self.taches.pop(index)

    def __len__(self):
        return len(self.taches)

    def __str__(self):
        if not self.taches:
            return "(liste vide)"
        return "\n".join(f"{i}. {t}" for i, t in enumerate(self.taches))

    def filtrer_par_statut(self, statut):
        return [t for t in self.taches if t.statut == statut]

    def filtrer_par_priorite(self, priorite):
        return [t for t in self.taches if t.priorite == priorite]

    def taches_terminees(self):
        return self.filtrer_par_statut("terminée")

    def taches_en_cours(self):
        return self.filtrer_par_statut("en cours")

    def taches_a_faire(self):
        return self.filtrer_par_statut("à faire")

    def trier_par_priorite(self):
        return sorted(self.taches, key=lambda t: ORDRE_PRIORITES[t.priorite])

    def trier_par_date_creation(self):
        return sorted(self.taches, key=lambda t: t.date_creation)

    def statistiques(self):
        n = len(self.taches)
        if n == 0:
            return {"nb_total": 0, "nb_a_faire": 0, "nb_en_cours": 0,
                    "nb_terminees": 0, "pourcentage_termine": 0.0}
        nb_term = len(self.taches_terminees())
        return {
            "nb_total": n,
            "nb_a_faire": len(self.taches_a_faire()),
            "nb_en_cours": len(self.taches_en_cours()),
            "nb_terminees": nb_term,
            "pourcentage_termine": round(nb_term / n * 100, 1),
        }


# Tests
liste = ListeTaches()
liste.ajouter(Tache("Répondre à Alice", priorite="haute"))
liste.ajouter(Tache("Courses", priorite="basse"))
liste.ajouter(Tache("Rapport", priorite="moyenne", statut="en cours"))
liste.ajouter(Tache("Facture", priorite="haute", statut="terminée"))

# Changer statut
liste.taches[1].marquer_terminee()

print("Tri par priorité :")
for t in liste.trier_par_priorite():
    print(f"  {t}")

print("\nStatistiques :")
for k, v in liste.statistiques().items():
    print(f"  {k} = {v}")
Tri par priorité :
  ⏳ [haute] Répondre à Alice
  ✅ [haute] Facture
  🔄 [moyenne] Rapport
  ✅ [basse] Courses

Statistiques :
  nb_total = 4
  nb_a_faire = 1
  nb_en_cours = 1
  nb_terminees = 2
  pourcentage_termine = 50.0

Points à retenir

  • Tri avec clé personnalisée via un dict d’ordres.
  • sorted retourne une nouvelle liste — on ne modifie pas l’original.
  • Les statistiques retournent un dict pour plus de flexibilité qu’un tuple.

Partie 4 — Persistance JSON

Consignes

Ajoutez à ListeTaches :

  • sauver(chemin) : écrit la liste en JSON (une liste de dicts).
  • charger(chemin) (méthode de classe) : charge et renvoie une nouvelle ListeTaches depuis un fichier JSON. Gère FileNotFoundError en renvoyant une liste vide.

Indice : dataclasses.asdict(tache) convertit une dataclass en dict, pratique pour JSON.

import json
from dataclasses import dataclass, field, asdict
from datetime import datetime
from pathlib import Path


class PrioriteInvalideError(Exception): pass
class StatutInvalideError(Exception): pass

PRIORITES_VALIDES = ("basse", "moyenne", "haute")
STATUTS_VALIDES = ("à faire", "en cours", "terminée")
EMOJIS = {"à faire": "⏳", "en cours": "🔄", "terminée": "✅"}


@dataclass
class Tache:
    titre: str
    priorite: str = "moyenne"
    statut: str = "à faire"
    date_creation: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
    echeance: str | None = None

    def __post_init__(self):
        if self.priorite not in PRIORITES_VALIDES:
            raise PrioriteInvalideError(f"Priorité '{self.priorite}' invalide")
        if self.statut not in STATUTS_VALIDES:
            raise StatutInvalideError(f"Statut '{self.statut}' invalide")

    def __str__(self):
        emoji = EMOJIS[self.statut]
        ech = f" (échéance {self.echeance})" if self.echeance else ""
        return f"{emoji} [{self.priorite}] {self.titre}{ech}"


class ListeTaches:
    def __init__(self):
        self.taches = []

    def ajouter(self, tache):
        self.taches.append(tache)

    def __len__(self):
        return len(self.taches)

    def __str__(self):
        if not self.taches:
            return "(liste vide)"
        return "\n".join(f"{i}. {t}" for i, t in enumerate(self.taches))

    def sauver(self, chemin):
        """Sérialise les tâches en JSON."""
        donnees = [asdict(t) for t in self.taches]
        with open(chemin, "w", encoding="utf-8") as f:
            json.dump(donnees, f, ensure_ascii=False, indent=2)

    def charger(self, chemin):
        """Charge les tâches depuis un fichier JSON dans l'instance actuelle."""
        try:
            with open(chemin, "r", encoding="utf-8") as f:
                donnees = json.load(f)
        except FileNotFoundError:
            self.taches = []  # Assure que la liste reste vide si le fichier n'existe pas
            return

        self.taches = [Tache(**d) for d in donnees]


# Tests
liste = ListeTaches()
liste.ajouter(Tache("Alpha", priorite="haute"))
liste.ajouter(Tache("Beta", priorite="basse", statut="terminée"))
liste.ajouter(Tache("Gamma", echeance="30/04/2026"))

chemin = "todo.json"

# Sauvegarder
liste.sauver(chemin)

# Afficher le fichier
with open(chemin, "r", encoding="utf-8") as f:
    print("Fichier JSON sauvegardé :")
    print(f.read())

# Recharger
nouvelle = ListeTaches()
nouvelle.charger(chemin)
print("\nTâches rechargées :")
print(nouvelle)

# Charger un fichier inexistant
vide = ListeTaches()
vide.charger("n_existe_pas.json")
print(f"\nFichier inexistant : {len(vide)} tâches")

# Nettoyage
Path(chemin).unlink()
Fichier JSON sauvegardé :
[
  {
    "titre": "Alpha",
    "priorite": "haute",
    "statut": "à faire",
    "date_creation": "2026-09-22T21:34:40",
    "echeance": null
  },
  {
    "titre": "Beta",
    "priorite": "basse",
    "statut": "terminée",
    "date_creation": "2026-09-22T21:34:40",
    "echeance": null
  },
  {
    "titre": "Gamma",
    "priorite": "moyenne",
    "statut": "à faire",
    "date_creation": "2026-09-22T21:34:40",
    "echeance": "30/04/2026"
  }
]

Tâches rechargées :
0. ⏳ [haute] Alpha
1. ✅ [basse] Beta
2. ⏳ [moyenne] Gamma (échéance 30/04/2026)

Fichier inexistant : 0 tâches

Points à retenir

  • @classmethod : méthode qui reçoit la classe (cls) comme premier argument. Utile pour les constructeurs alternatifs comme charger().
  • asdict() convertit une dataclass en dict, puis en JSON.
  • Tache(**d) : déballage d’un dict pour recréer une tâche.
  • try/except FileNotFoundError : gestion gracieuse des fichiers absents (cas typique au premier lancement).
  • ensure_ascii=False pour préserver les accents dans le JSON.

Partie 5 — Menu interactif avec match/case

Consignes

Écrivez une fonction menu_principal(liste) qui propose un menu en boucle :

1. Afficher toutes les tâches
2. Ajouter une tâche
3. Marquer une tâche comme terminée
4. Supprimer une tâche
5. Statistiques
6. Sauvegarder
7. Quitter

Utilisez match/case pour le dispatch des choix. Gérez les erreurs de saisie (ID invalide, etc.).

# (À placer dans un fichier .py pour pouvoir utiliser input() de manière interactive)

def menu_principal(liste, chemin="todo.json"):
    while True:
        print("\n" + "=" * 40)
        print("  GESTIONNAIRE DE TÂCHES")
        print("=" * 40)
        print("  1. Afficher toutes les tâches")
        print("  2. Ajouter une tâche")
        print("  3. Marquer comme terminée")
        print("  4. Supprimer une tâche")
        print("  5. Statistiques")
        print("  6. Sauvegarder")
        print("  7. Quitter")
        print("=" * 40)

        choix = input("\nVotre choix : ").strip()

        match choix:
            case "1":
                print(liste)

            case "2":
                titre = input("Titre : ").strip()
                priorite = input("Priorité (basse/moyenne/haute) : ").strip() or "moyenne"
                try:
                    liste.ajouter(Tache(titre, priorite=priorite))
                    print("✅ Tâche ajoutée")
                except PrioriteInvalideError as e:
                    print(f"❌ {e}")

            case "3":
                try:
                    idx = int(input("Indice de la tâche : "))
                    liste.taches[idx].marquer_terminee()
                    print("✅ Terminée")
                except (ValueError, IndexError) as e:
                    print(f"❌ {e}")

            case "4":
                try:
                    idx = int(input("Indice de la tâche : "))
                    tache = liste.supprimer(idx)
                    print(f"✅ Supprimée : {tache}")
                except (ValueError, IndexError) as e:
                    print(f"❌ {e}")

            case "5":
                for k, v in liste.statistiques().items():
                    print(f"  {k} = {v}")

            case "6":
                liste.sauver(chemin)
                print(f"✅ Sauvegardé dans {chemin}")

            case "7":
                print("Au revoir !")
                break

            case _:
                print("❌ Choix invalide")


# Lancement
if __name__ == "__main__":
    liste = ListeTaches.charger("todo.json")
    menu_principal(liste)

Démonstration simulée (sans input())

Puisque Quarto ne gère pas input() interactif, voici une simulation des interactions via un dispatch par match/case :

from dataclasses import dataclass, field, asdict
from datetime import datetime


class PrioriteInvalideError(Exception): pass
class StatutInvalideError(Exception): pass

PRIORITES_VALIDES = ("basse", "moyenne", "haute")
STATUTS_VALIDES = ("à faire", "en cours", "terminée")
EMOJIS = {"à faire": "⏳", "en cours": "🔄", "terminée": "✅"}


@dataclass
class Tache:
    titre: str
    priorite: str = "moyenne"
    statut: str = "à faire"
    date_creation: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
    echeance: str | None = None

    def __post_init__(self):
        if self.priorite not in PRIORITES_VALIDES:
            raise PrioriteInvalideError(f"Priorité '{self.priorite}' invalide")
        if self.statut not in STATUTS_VALIDES:
            raise StatutInvalideError(f"Statut '{self.statut}' invalide")

    def marquer_terminee(self):
        self.statut = "terminée"

    def __str__(self):
        emoji = EMOJIS[self.statut]
        ech = f" (échéance {self.echeance})" if self.echeance else ""
        return f"{emoji} [{self.priorite}] {self.titre}{ech}"


class ListeTaches:
    def __init__(self):
        self.taches = []

    def ajouter(self, tache):
        self.taches.append(tache)

    def __str__(self):
        if not self.taches:
            return "(liste vide)"
        return "\n".join(f"{i}. {t}" for i, t in enumerate(self.taches))


def executer_commande(liste, commande):
    """Simule le dispatch menu avec match/case."""
    match commande:
        case ("afficher",):
            print(liste)
        case ("ajouter", titre):
            liste.ajouter(Tache(titre))
            print(f"✅ Ajoutée : {titre}")
        case ("ajouter", titre, priorite):
            try:
                liste.ajouter(Tache(titre, priorite=priorite))
                print(f"✅ Ajoutée : {titre} (priorité {priorite})")
            except PrioriteInvalideError as e:
                print(f"❌ {e}")
        case ("terminer", idx):
            try:
                liste.taches[idx].marquer_terminee()
                print(f"✅ Terminée : {liste.taches[idx].titre}")
            except IndexError:
                print(f"❌ Indice {idx} invalide")
        case _:
            print(f"❌ Commande inconnue : {commande}")


# Simulation
liste = ListeTaches()

for cmd in [
    ("ajouter", "Courses"),
    ("ajouter", "Rapport", "haute"),
    ("ajouter", "Appeler Bob", "extrême"),    # priorité invalide
    ("afficher",),
    ("terminer", 0),
    ("afficher",),
    ("terminer", 99),                          # indice invalide
]:
    print(f"\n>>> {cmd}")
    executer_commande(liste, cmd)

>>> ('ajouter', 'Courses')
✅ Ajoutée : Courses

>>> ('ajouter', 'Rapport', 'haute')
✅ Ajoutée : Rapport (priorité haute)

>>> ('ajouter', 'Appeler Bob', 'extrême')
❌ Priorité 'extrême' invalide

>>> ('afficher',)
0. ⏳ [moyenne] Courses
1. ⏳ [haute] Rapport

>>> ('terminer', 0)
✅ Terminée : Courses

>>> ('afficher',)
0. ✅ [moyenne] Courses
1. ⏳ [haute] Rapport

>>> ('terminer', 99)
❌ Indice 99 invalide

Points à retenir

  • match sur un tuple de commande : élégant pour un dispatch avec arguments variables.
  • Motifs avec arité différente : ("ajouter", titre) vs ("ajouter", titre, priorite).
  • Gestion d’erreurs par commande : chaque case peut avoir son try/except spécifique.
  • _ pour le fallback.
  • if __name__ == "__main__": pour que le menu ne se lance pas à l’import.

🏆 Bonus — Pour aller plus loin

Une fois les 5 parties maîtrisées, défis supplémentaires :

  1. Sous-classes de Tâche : créez TacheRecurrente(Tache) avec une fréquence (quotidienne, hebdomadaire, mensuelle), et TacheAvecProgression(Tache) avec un pourcentage d’avancement (0-100).

  2. Filtres combinés : une méthode filtrer(**criteres) qui accepte n’importe quelle combinaison (liste.filtrer(priorite="haute", statut="à faire")).

  3. Tri multi-critères : trier par priorité puis par date de création en cas d’ex æquo.

  4. Export CSV : exporter_csv(chemin) en plus du JSON.

  5. Échéances en retard : méthode taches_en_retard() qui renvoie les tâches non terminées dont l’échéance est dépassée (utilisez datetime.strptime).

from dataclasses import dataclass


@dataclass
class Tache:
    titre: str
    priorite: str = "moyenne"
    statut: str = "à faire"


def filtrer(liste_taches, **criteres):
    """Filtre les tâches selon n'importe quelle combinaison d'attributs."""
    resultat = liste_taches
    for attr, valeur in criteres.items():
        resultat = [t for t in resultat if getattr(t, attr, None) == valeur]
    return resultat


taches = [
    Tache("A", priorite="haute",   statut="à faire"),
    Tache("B", priorite="haute",   statut="terminée"),
    Tache("C", priorite="basse",   statut="à faire"),
    Tache("D", priorite="moyenne", statut="à faire"),
]

print("Priorité haute ET à faire :")
for t in filtrer(taches, priorite="haute", statut="à faire"):
    print(f"  {t.titre}")

print("\nJuste à faire :")
for t in filtrer(taches, statut="à faire"):
    print(f"  {t.titre}")
Priorité haute ET à faire :
  A

Juste à faire :
  A
  C
  D

Avantages : l’API est très flexible. On peut ajouter d’autres critères sans changer la signature.


✅ Validation du niveau Avancé

Bilan du TP

Si vous avez résolu les 5 parties (même partiellement), vous maîtrisez les compétences du niveau Avancé TOSA :

  • Compréhensions de liste, dict, set
  • *args / **kwargs et déballage
  • Lambdas pour tri et filtrage
  • Classes et @dataclass (POO moderne)
  • Exceptions personnalisées et gestion d’erreurs
  • Fichiers JSON (lecture/écriture)
  • pip et environnements virtuels (pour les dépendances)
  • Complexité algorithmique (choix des structures)
  • match/case (Python 3.10)

Score TOSA estimé : 726-875 (niveau Avancé).

Prêt(e) pour la Partie 4 — Niveau Expert ? Vous y découvrirez les regex, générateurs, décorateurs, closures, tests unitaires et plus encore — de quoi atteindre le niveau Expert (876+).

← Chapitre précédent : Pattern matchingPartie 4 — Niveau Expert →