Leçon 02-5 — Erreurs de logique : quand le programme tourne mais fait faux
1. Objectif
À la fin de cette leçon tu sauras : reconnaître les cinq erreurs de logique qui dominent le niveau 2 (condition toujours vraie ou toujours fausse, off-by-one, indentation qui change le sens, else mal accroché) ; et surtout appliquer une méthode de débogage au print() — reproductible, calme, efficace — pour localiser où la logique d'un programme dévie de ton intention.
2. Pourquoi c'est important
Depuis le niveau 1, tu sais lire les tracebacks. Mauvaise nouvelle : les bugs les plus fréquents — et les plus longs à trouver — n'en produisent aucun. Le programme tourne, affiche quelque chose de plausible, et ment. Ces bugs de logique sont le quotidien du programmeur : le compilateur ne peut pas savoir ce que tu VOULAIS. Cette leçon est une leçon de révision active (toutes les notions viennent des leçons 02-1 à 02-4) et une leçon de méthode : c'est elle qui transforme « je fixe mon écran en espérant voir le bug » en « je pose trois print() et je le coince en cinq minutes ».
3. Explication simple
Un bug de logique, c'est un écart entre ton intention et ce que le code dit vraiment. Python exécute ce que le code dit — toujours, exactement, sans deviner. La galerie des suspects du niveau 2 :
- La condition toujours vraie :
if x == 1 or 2:(leçon 02-2) — leelsene s'exécute jamais. - La condition toujours fausse :
if answer == 18:quandanswersort d'uninput()—"18" == 18estFalsepour l'éternité (leçon 02-1). - L'off-by-one :
range(1, 10)pour « de 1 à 10 » — un tour de moins ;while sold <= tickets:parti de 0 — un tour de plus (leçon 02-4). - L'indentation qui change le sens : une ligne dé-indentée sort du bloc, sans erreur — elle s'exécute juste à un autre moment (leçons 02-1/02-3).
- Le
elsemal accroché : il s'attache au mauvaisif, et une branche entière disparaît (leçon 02-1).
Aucun des cinq ne produit de traceback. L'arme contre tous les cinq : afficher ce que le programme voit vraiment au moment où il le voit — c'est la méthode de la section 4.
4. Explication approfondie
La méthode de débogage au print(), en cinq étapes.
- Formule l'attendu, par écrit. « À la fin,
totaldoit valoir 55. » Sans attendu chiffré, impossible de dire où ça dévie. - Reproduis le bug avec l'entrée la plus SIMPLE possible qui échoue. Déboguer sur
range(1, 4)est plus rapide que surrange(1, 101)— si le bug survit à la réduction, garde la petite version. - Pose des
print()espions aux points stratégiques : dans le corps de la boucle (valeurs du compteur et de l'accumulateur à chaque tour), juste avant unif(la valeur ET son type :print(x, type(x))), juste après la boucle. Préfixe-les deDEBUGpour les retrouver et les enlever après. - Compare tour par tour ce qui s'affiche avec ce que tu attendais. Le bug est entre le dernier affichage juste et le premier affichage faux. C'est un rétrécissement : à chaque
print, la zone suspecte se réduit. - Corrige, ré-exécute, puis enlève les espions. Et note le bug dans ton journal — les bugs de logique se répètent ; ton journal devient ton anti-sèche personnelle.
Étape 3 bis, l'espion le plus rentable du niveau 2 : print(repr(x))… pas encore — repr arrive plus tard (⏩ niveau 3). Sa version accessible dès maintenant : print(x, type(x)). La moitié des conditions toujours fausses sont des str comparées à des int, et type() les démasque en une ligne.
Deux compléments à la méthode.
- La lecture à voix haute. Lis la condition comme Python : « si x égale 1, OU BIEN 2 » — l'absurdité s'entend mieux qu'elle ne se voit. Pareil pour les boucles : « tant que le code est bon, redemande » (inversée !).
- Le test des bornes. Les off-by-one vivent aux extrémités : teste toujours la première valeur, la dernière, et les valeurs pile sur un seuil (
18pour>= 18,90pour>= 90). Écris première/dernière valeur d'unrangeà la main — le réflexe installé en 02-4.
Honnêteté sur la difficulté : cette compétence est LA plus longue à acquérir du niveau (le README du niveau t'avait prévenu : le débogage EST l'apprentissage). Personne ne « voit » les bugs de logique instantanément — les développeurs expérimentés sont juste plus rapides à dérouler la méthode. Chronomètre-toi sur les exercices : l'objectif n'est pas zéro bug, c'est un temps de localisation qui baisse.
5. Exemples commentés
Exemple 1 — condition toujours vraie, démasquée à voix haute
day = 3
if day == 6 or 7: # lu : (day == 6) or (7) — et 7 est truthy
print("Week-end !")
else:
print("Semaine")
Sortie : Week-end ! — un mercredi. La version juste (day == 6 or day == 7) afficherait Semaine.
Exemple 2 — condition toujours fausse : la str face à l'int
answer = input("Ton age ? ") # l'utilisateur tape 18
if answer == 18:
print("Pile 18 ans")
else:
print("Pas 18")
Sortie : Pas 18, quoi qu'on tape. L'espion qui tue : print(answer, type(answer)) → 18 <class 'str'>. Diagnostic en une ligne.
Exemple 3 — off-by-one des deux côtés
total = 0
for n in range(1, 10): # voulait 1 à 10 : le 10 manque
total += n
print(total) # 45 au lieu de 55
tickets = 3
sold = 0
while sold <= tickets: # <= avec un départ à 0 : 0,1,2,3 → 4 ventes !
print("vente numero", sold)
sold += 1
Sortie du second : quatre lignes (vente numero 0 à vente numero 3) pour trois tickets. Deux bugs jumeaux : borne exclue oubliée d'un côté, borne incluse de trop de l'autre. Antidote unique : compter les tours sur papier (première valeur, dernière valeur, combien).
Exemple 4 — l'indentation qui déplace une ligne dans le temps
total = 0
for i in range(1, 4):
total += i
print(f"apres l'etape {i} : total = {total}")
Sortie :
apres l'etape 3 : total = 6
Le print est HORS de la boucle : il ne s'exécute qu'une fois, après. La version indentée (le print DANS le bloc) donne :
apres l'etape 1 : total = 1
apres l'etape 2 : total = 3
apres l'etape 3 : total = 6
Mêmes lignes, seule l'indentation diffère — et le programme raconte une autre histoire. (Au passage : la variable de boucle i survit après le for, avec sa dernière valeur — c'est pour ça que la première version affiche 3.)
Exemple 5 — le else accroché au mauvais if
age = 30
has_ticket = False
if age >= 18:
if has_ticket:
print("Entree autorisee")
else:
print("Interdit aux mineurs")
print("(fin)")
Sortie :
(fin)
Un adulte sans billet ne déclenche… RIEN. Le else (même indentation que le if age >= 18:) appartient au if extérieur ; le if has_ticket: intérieur n'a pas de else, donc le cas « adulte sans billet » tombe dans un trou. Correction : indenter le else d'un niveau pour l'accrocher au if intérieur (et le message devient « Va acheter un billet »). Règle 02-1 : un else s'accroche au if de même indentation — vérifie-le à chaque imbrication.
Exemple 6 — la méthode complète sur un vrai bug
Intention : la moyenne des nombres de 1 à 5. Attendu (étape 1) : 3.0.
total = 0
count = 0
for n in range(1, 5):
total += n
count += 1
print(total / count)
Sortie : 2.5. Faux. Étape 3 — espions dans la boucle :
total = 0
count = 0
for n in range(1, 5):
total += n
count += 1
print(f"DEBUG n={n} total={total} count={count}")
print(total / count)
Sortie :
DEBUG n=1 total=1 count=1
DEBUG n=2 total=3 count=2
DEBUG n=3 total=6 count=3
DEBUG n=4 total=10 count=4
2.5
Étape 4 — lecture tour par tour : les cumuls sont justes… mais la boucle s'arrête à n=4. Le 5 ne passe jamais : off-by-one dans le range. Étape 5 — range(1, 6), ré-exécution → 3.0, on enlève les espions. Le bug n'était PAS dans le calcul (qu'on aurait fixé des heures) : les espions ont montré où la réalité déviait de l'intention.
6. Erreurs fréquentes
Les erreurs de cette leçon SONT ses exemples (section 5) — voici les cinq fiches réflexes, format court :
1) Condition toujours vraie — symptôme : le else ne sort jamais, tout passe. Suspect n°1 : un or avec un opérande nu (x == 1 or 2). Réflexe : lire à voix haute + print() de la condition.
2) Condition toujours fausse — symptôme : la même branche, toujours, quelles que soient les saisies. Suspect n°1 : str vs int ("18" == 18). Réflexe : print(x, type(x)) juste avant le if.
3) Off-by-one — symptôme : résultat faux « de peu », un tour de trop ou de moins. Suspects : stop de range (exclu !), < vs <=, compteur démarré à 0 vs 1. Réflexe : première valeur, dernière valeur, combien — sur papier.
4) Indentation qui change le sens — symptôme : une ligne s'exécute trop souvent (rentrée dans la boucle par erreur) ou une seule fois (sortie par erreur), ou une boucle devient infinie (la mise à jour est sortie du bloc, leçon 02-3). Réflexe : pour chaque ligne du bloc, demander « veux-tu vraiment qu'elle s'exécute À CHAQUE tour ? ».
5) else mal accroché — symptôme : un cas disparaît (rien ne s'affiche) ou un message sort dans la mauvaise situation. Réflexe : relier chaque else à son if de même indentation, au doigt, sur l'écran.
Et la sixième, la méta-erreur : déboguer au hasard — changer des lignes « pour voir », sans attendu ni espion. On aggrave presque toujours. La méthode des cinq étapes existe pour ça.
7. Lire les messages d'erreur
Le message d'erreur emblématique de cette leçon, c'est le silence. Quand un programme tourne sans traceback mais donne un résultat faux, le traceback, c'est TOI qui le fabriques avec des print() espions (section 4).
Un seul vrai traceback à connaître ici, cousin vicieux de l'indentation — le mélange tabulations/espaces :
File "c:\Users\jtron\Claude\learnpython\tabs.py", line 4
print(total)
^
TabError: inconsistent use of tabs and spaces in indentation
TabError(familleSyntaxError: rien n'a été exécuté) : la ligne 4 est indentée avec une tabulation alors que les lignes précédentes du bloc utilisent des espaces. À l'œil, les deux sont identiques — c'est ça le piège. Ça arrive typiquement en collant du code depuis un site web.- Correction : ré-indenter la ligne avec des espaces. Prévention : VS Code → réglage « Insert Spaces » (actif par défaut) et l'affichage des caractères invisibles (
View → Render Whitespace) quand un doute survient. - Note la nuance :
TabErroret lesIndentationErrorde la leçon 02-1 sont des erreurs bruyantes — Python te bloque. L'indentation LÉGALE mais fausse (exemple 4, section 5) ne déclenche rien : c'est elle, la dangereuse.
8. Exercices — faciles
Règles : nouveau fichier par exercice dans
lessons/level-02-conditions-boucles/exercices/mes-reponses/, nomméex-02-5-a.py, etc. Pour chaque débogage : écris l'attendu, prédis le comportement du code buggé, PUIS exécute et corrige.
a) Ce programme doit dire si une lettre saisie est une voyelle. Prédis ce qu'il répond pour z, explique le bug, corrige :
letter = input("Une lettre ? ")
if letter == "a" or "e" or "i" or "o" or "u":
print("Voyelle")
else:
print("Consonne")
b) Prédis la sortie EXACTE des deux versions (elles ne diffèrent que par l'indentation du dernier print), puis vérifie :
# version 1
count = 0
for i in range(3):
count += 2
print(count)
# version 2
count = 0
for i in range(3):
count += 2
print(count)
c) Ce compte à rebours doit afficher 5 4 3 2 1 mais affiche autre chose. Trouve le bug de borne, corrige, et note la règle qui l'évite :
for n in range(5, 1, -1):
print(n, end=" ")
9. Exercices — moyens
a) La caisse ci-dessous doit encaisser 3 articles et afficher Total : 60. Elle affiche autre chose. DEUX bugs de logique (aucun traceback) : trouve-les avec la méthode des espions (garde tes print DEBUG dans le fichier rendu, commentés), corrige :
total = 0
for item in range(1, 3):
price = 20
total += price
print(f"Total : {total}")
b) Ce contrôle d'accès doit afficher Bienvenue pour un membre OU un majeur, et Refuse sinon. Étrange : il marche PARFAITEMENT pour tous les membres… mais plante dès qu'un non-membre se présente. Explique pourquoi le crash est intermittent (pense au court-circuit de la leçon 02-2 : quand la comparaison d'âge est-elle réellement évaluée ?), corrige, puis teste les quatre combinaisons oui/non × 15/25 :
member = input("Membre ? (oui/non) ")
age = input("Age ? ")
if member == "oui" or age >= 18:
print("Bienvenue")
else:
print("Refuse")
(Leçon cachée : un bug bruyant peut se cacher derrière un court-circuit et ne se déclarer qu'avec CERTAINES saisies — ton programme peut « marcher » dix fois avant de planter. D'où l'importance de tester toutes les combinaisons.)
c) Le quiz suivant doit poser 3 questions et afficher le score sur 3. Il affiche un score toujours égal à 0 ou 1. Trouve le bug d'indentation SANS exécuter (déroule sur papier), corrige :
score = 0
for q in range(1, 4):
answer = int(input(f"Question {q} : 2 + {q} ? "))
if answer == 2 + q:
score = 1
print(f"Score : {score}/3")
(Deux problèmes distincts : un compteur qui n'accumule pas, et réfléchis — est-ce un bug d'indentation ou d'opérateur ? Justifie.)
10. Exercices — difficiles
a) Le FizzBuzz, capstone du niveau : affiche les nombres de 1 à 30, mais remplace les multiples de 3 par Fizz, les multiples de 5 par Buzz, et les multiples des deux par FizzBuzz. Début attendu : 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 ... Écris-le, teste-le, et vérifie SPÉCIFIQUEMENT la ligne 15.
- Indice 1 : si ton 15 affiche
Fizzau lieu deFizzBuzz, tu viens de rencontrer l'erreur n°6 de la leçon 02-1 — l'ordre des branches d'une chaîneelif. Laquelle des trois conditions est la plus exigeante ? - Indice 2 : « multiple de 3 ET de 5 » se teste avec
and(leçon 02-2), ou avec un seul%si tu vois lequel. - Indice 3 : ordre correct : d'abord
n % 15 == 0(oun % 3 == 0 and n % 5 == 0), PUISn % 3 == 0, PUISn % 5 == 0, sinon le nombre.
11. Mini-projet lié — « menu de caisse »
Le troisième mini-projet du niveau (annoncé dans le README) : cash_menu.py, une caisse à menu qui tourne jusqu'à « quitter ». Session type :
1. Ajouter un article
2. Afficher le total
3. Quitter
Ton choix : 1
Prix de l'article ? 12.50
Ajoute ! (1 article, total 12.50 EUR)
Ton choix : 2
Total : 12.50 EUR (1 articles)
Ton choix : 3
Au revoir !
Contraintes — c'est la synthèse des cinq leçons : un while de menu avec une condition d'arrêt propre (leçon 02-3) ; un if/elif/else sur le choix (02-1), le choix étant comparé comme chaîne ("1", piège 02-1) ; un choix invalide affiche un message et re-boucle (pas de plantage) ; un accumulateur total et un compteur d'articles initialisés AVANT la boucle (02-3, et piège 9-a de cette leçon) ; total affiché en :.2f (01-2).
Avant de coder, écris le squelette en français (« tant que le choix n'est pas 3 : afficher le menu, lire le choix, si… »). Puis code, puis DÉBOGUE avec la méthode de la section 4 — le premier jet aura un bug, c'est statistique, et cette fois tu as la méthode. Durée cible : 45 min – 1 h. Ce projet coche le dernier critère de sortie du niveau.
12. Correction / méthode de correction
Les solutions détaillées sont dans solutions/serie-02-5-solutions.md.
Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) compare le raisonnement, pas seulement le code ; (3) pour chaque écart, une ligne de journal : « je pensais X, en fait Y » — pour CETTE leçon, note aussi le TEMPS mis à localiser chaque bug ; (4) refais de tête l'exercice raté, le lendemain.
13. À retenir
- Les bugs les plus coûteux ne produisent aucun traceback : le programme tourne et ment.
- Les cinq suspects du niveau : condition toujours vraie (
ornu), condition toujours fausse (strvsint), off-by-one, indentation légale mais fausse,elsemal accroché. - La méthode : attendu écrit → reproduction minimale →
print()espions → comparaison tour par tour → correction puis nettoyage. - L'espion le plus rentable :
print(x, type(x))avant unif. - Les off-by-one vivent aux bornes : teste première valeur, dernière valeur, valeurs pile sur les seuils.
- Un
elses'accroche auifde même indentation ; une ligne dé-indentée change de moment d'exécution, pas de légalité. TabError= mélange tabs/espaces, souvent du code collé du web.- Déboguer au hasard aggrave ; déboguer avec des espions rétrécit.
14. Questions de révision
- Pourquoi les bugs de logique sont-ils plus dangereux que les erreurs à traceback ?
- Récite les cinq étapes de la méthode de débogage au
print(). - Quel espion démasque une condition toujours fausse due à un
input()non converti ? - Donne un exemple d'off-by-one avec
rangeet un avecwhile. Quel test systématique les attrape ? - Dans l'exemple 5 (billet), pourquoi l'adulte sans billet ne déclenche-t-il RIEN ? À quoi le
elseest-il accroché ? - Quelle différence entre une
IndentationErroret une indentation « légale mais fausse » ? Laquelle est la plus dangereuse et pourquoi ? - Que signifie
TabErroret d'où vient-il en pratique ? - Dans FizzBuzz, pourquoi le test « multiple de 3 et de 5 » doit-il venir en PREMIER dans la chaîne
elif?
15. Checklist de compréhension
- Je peux citer les cinq bugs de logique du niveau avec un exemple de code pour chacun, de tête.
- Devant un résultat faux sans traceback, mon premier geste est d'écrire l'attendu et de poser des espions — pas de modifier du code au hasard.
- J'utilise
print(x, type(x))par réflexe quand unifprend toujours la même branche. - J'ai localisé les bugs des exercices 9-a, 9-b et 9-c AVANT d'exécuter (papier), puis confirmé à l'exécution.
- Mon FizzBuzz affiche
FizzBuzzen 15 et 30, et je sais expliquer l'ordre des branches. - Le menu de caisse tourne sans plantage, y compris sur un choix invalide.
- Je remplis les critères de sortie du README du niveau — tous cochés.
Si une case reste vide : refais les exercices de la section correspondante demain. Si tout est coché : direction le niveau 3 (structures de données) — et le commit de fin de niveau ci-dessous.
16. Commit conseillé
git add lessons/level-02-conditions-boucles/exercices/mes-reponses/
git commit -m "exercises: complete lesson 02-5 logic errors and debugging"
Puis le commit de fin de niveau (voir README du niveau) et, si c'est ta fin de session : entrée de journal + git push.