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
Un fichier .py pour travailler (todo.py).
Avancez partie par partie, testez après chaque.
Ne copiez pas les solutions avant d’avoir essayé.
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__).
🔍 Solution Partie 1
from dataclasses import dataclass, fieldfrom datetime import datetimeclass PrioriteInvalideError(Exception):"""Levée quand la priorité n'est pas dans ('basse', 'moyenne', 'haute')."""passclass StatutInvalideError(Exception):"""Levée quand le statut n'est pas valide."""passPRIORITES_VALIDES = ("basse", "moyenne", "haute")STATUTS_VALIDES = ("à faire", "en cours", "terminée")EMOJIS = {"à faire": "⏳", "en cours": "🔄", "terminée": "✅"}@dataclassclass Tache: titre: str priorite: str="moyenne" statut: str="à faire" date_creation: str= field(default_factory=lambda: datetime.now().isoformat(timespec="seconds")) echeance: str|None=Nonedef __post_init__(self):ifself.priorite notin PRIORITES_VALIDES:raise PrioriteInvalideError(f"Priorité '{self.priorite}' invalide. Valeurs : {PRIORITES_VALIDES}" )ifself.statut notin STATUTS_VALIDES:raise StatutInvalideError(f"Statut '{self.statut}' invalide. Valeurs : {STATUTS_VALIDES}" )def est_terminee(self):returnself.statut =="terminée"def__str__(self): emoji = EMOJIS[self.statut] echeance_str =f" (échéance {self.echeance})"ifself.echeance else""returnf"{emoji} [{self.priorite}] {self.titre}{echeance_str}"# Testst1 = 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}")
from dataclasses import dataclass, fieldfrom datetime import datetimeclass PrioriteInvalideError(Exception): passclass StatutInvalideError(Exception): passPRIORITES_VALIDES = ("basse", "moyenne", "haute")STATUTS_VALIDES = ("à faire", "en cours", "terminée")EMOJIS = {"à faire": "⏳", "en cours": "🔄", "terminée": "✅"}@dataclassclass Tache: titre: str priorite: str="moyenne" statut: str="à faire" date_creation: str= field(default_factory=lambda: datetime.now().isoformat(timespec="seconds")) echeance: str|None=Nonedef __post_init__(self):ifself.priorite notin PRIORITES_VALIDES:raise PrioriteInvalideError(f"Priorité '{self.priorite}' invalide")ifself.statut notin STATUTS_VALIDES:raise StatutInvalideError(f"Statut '{self.statut}' invalide")def est_terminee(self):returnself.statut =="terminée"def__str__(self): emoji = EMOJIS[self.statut] ech =f" (échéance {self.echeance})"ifself.echeance else""returnf"{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):ifnot0<= index <len(self.taches):raiseIndexError(f"Indice {index} hors limites (0 à {len(self.taches)-1})")returnself.taches.pop(index)def__len__(self):returnlen(self.taches)def__str__(self):ifnotself.taches:return"(liste vide)" lignes = [f"{i}. {t}"for i, t inenumerate(self.taches)]return"\n".join(lignes)def filtrer_par_statut(self, statut):return [t for t inself.taches if t.statut == statut]def filtrer_par_priorite(self, priorite):return [t for t inself.taches if t.priorite == priorite]def taches_terminees(self):returnself.filtrer_par_statut("terminée")def taches_en_cours(self):returnself.filtrer_par_statut("en cours")def taches_a_faire(self):returnself.filtrer_par_statut("à faire")# Testsliste = 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’instanceself.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.
🔍 Solution Partie 3
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, fieldfrom datetime import datetimeclass PrioriteInvalideError(Exception): passclass StatutInvalideError(Exception): passPRIORITES_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}@dataclassclass Tache: titre: str priorite: str="moyenne" statut: str="à faire" date_creation: str= field(default_factory=lambda: datetime.now().isoformat(timespec="seconds")) echeance: str|None=Nonedef __post_init__(self):ifself.priorite notin PRIORITES_VALIDES:raise PrioriteInvalideError(f"Priorité '{self.priorite}' invalide")ifself.statut notin STATUTS_VALIDES:raise StatutInvalideError(f"Statut '{self.statut}' invalide")def est_terminee(self):returnself.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})"ifself.echeance else""returnf"{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):ifnot0<= index <len(self.taches):raiseIndexError(f"Indice {index} hors limites")returnself.taches.pop(index)def__len__(self):returnlen(self.taches)def__str__(self):ifnotself.taches:return"(liste vide)"return"\n".join(f"{i}. {t}"for i, t inenumerate(self.taches))def filtrer_par_statut(self, statut):return [t for t inself.taches if t.statut == statut]def filtrer_par_priorite(self, priorite):return [t for t inself.taches if t.priorite == priorite]def taches_terminees(self):returnself.filtrer_par_statut("terminée")def taches_en_cours(self):returnself.filtrer_par_statut("en cours")def taches_a_faire(self):returnself.filtrer_par_statut("à faire")def trier_par_priorite(self):returnsorted(self.taches, key=lambda t: ORDRE_PRIORITES[t.priorite])def trier_par_date_creation(self):returnsorted(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), }# Testsliste = 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 statutliste.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.
🔍 Solution Partie 4
import jsonfrom dataclasses import dataclass, field, asdictfrom datetime import datetimefrom pathlib import Pathclass PrioriteInvalideError(Exception): passclass StatutInvalideError(Exception): passPRIORITES_VALIDES = ("basse", "moyenne", "haute")STATUTS_VALIDES = ("à faire", "en cours", "terminée")EMOJIS = {"à faire": "⏳", "en cours": "🔄", "terminée": "✅"}@dataclassclass Tache: titre: str priorite: str="moyenne" statut: str="à faire" date_creation: str= field(default_factory=lambda: datetime.now().isoformat(timespec="seconds")) echeance: str|None=Nonedef __post_init__(self):ifself.priorite notin PRIORITES_VALIDES:raise PrioriteInvalideError(f"Priorité '{self.priorite}' invalide")ifself.statut notin STATUTS_VALIDES:raise StatutInvalideError(f"Statut '{self.statut}' invalide")def__str__(self): emoji = EMOJIS[self.statut] ech =f" (échéance {self.echeance})"ifself.echeance else""returnf"{emoji} [{self.priorite}] {self.titre}{ech}"class ListeTaches:def__init__(self):self.taches = []def ajouter(self, tache):self.taches.append(tache)def__len__(self):returnlen(self.taches)def__str__(self):ifnotself.taches:return"(liste vide)"return"\n".join(f"{i}. {t}"for i, t inenumerate(self.taches))def sauver(self, chemin):"""Sérialise les tâches en JSON.""" donnees = [asdict(t) for t inself.taches]withopen(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:withopen(chemin, "r", encoding="utf-8") as f: donnees = json.load(f)exceptFileNotFoundError:self.taches = [] # Assure que la liste reste vide si le fichier n'existe pasreturnself.taches = [Tache(**d) for d in donnees]# Testsliste = 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"# Sauvegarderliste.sauver(chemin)# Afficher le fichierwithopen(chemin, "r", encoding="utf-8") as f:print("Fichier JSON sauvegardé :")print(f.read())# Rechargernouvelle = ListeTaches()nouvelle.charger(chemin)print("\nTâches rechargées :")print(nouvelle)# Charger un fichier inexistantvide = ListeTaches()vide.charger("n_existe_pas.json")print(f"\nFichier inexistant : {len(vide)} tâches")# NettoyagePath(chemin).unlink()
@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.).
🔍 Solution Partie 5
# (À placer dans un fichier .py pour pouvoir utiliser input() de manière interactive)def menu_principal(liste, chemin="todo.json"):whileTrue: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 !")breakcase _:print("❌ Choix invalide")# Lancementif__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 :
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 :
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).
Filtres combinés : une méthode filtrer(**criteres) qui accepte n’importe quelle combinaison (liste.filtrer(priorite="haute", statut="à faire")).
Tri multi-critères : trier par priorité puis par date de création en cas d’ex æquo.
Export CSV : exporter_csv(chemin) en plus du JSON.
É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).
🔍 Idée pour le bonus 2 — Filtres combinés avec **criteres
from dataclasses import dataclass@dataclassclass 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_tachesfor attr, valeur in criteres.items(): resultat = [t for t in resultat ifgetattr(t, attr, None) == valeur]return resultattaches = [ 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+).