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 :

  1. La condition toujours vraie : if x == 1 or 2: (leçon 02-2) — le else ne s'exécute jamais.
  2. La condition toujours fausse : if answer == 18: quand answer sort d'un input()"18" == 18 est False pour l'éternité (leçon 02-1).
  3. 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).
  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).
  5. Le else mal accroché : il s'attache au mauvais if, 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.

  1. Formule l'attendu, par écrit. « À la fin, total doit valoir 55. » Sans attendu chiffré, impossible de dire où ça dévie.
  2. Reproduis le bug avec l'entrée la plus SIMPLE possible qui échoue. Déboguer sur range(1, 4) est plus rapide que sur range(1, 101) — si le bug survit à la réduction, garde la petite version.
  3. 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 un if (la valeur ET son type : print(x, type(x))), juste après la boucle. Préfixe-les de DEBUG pour les retrouver et les enlever après.
  4. 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.
  5. 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.

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
  1. TabError (famille SyntaxError : 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.
  2. 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.
  3. Note la nuance : TabError et les IndentationError de 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.

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

14. Questions de révision

  1. Pourquoi les bugs de logique sont-ils plus dangereux que les erreurs à traceback ?
  2. Récite les cinq étapes de la méthode de débogage au print().
  3. Quel espion démasque une condition toujours fausse due à un input() non converti ?
  4. Donne un exemple d'off-by-one avec range et un avec while. Quel test systématique les attrape ?
  5. Dans l'exemple 5 (billet), pourquoi l'adulte sans billet ne déclenche-t-il RIEN ? À quoi le else est-il accroché ?
  6. Quelle différence entre une IndentationError et une indentation « légale mais fausse » ? Laquelle est la plus dangereuse et pourquoi ?
  7. Que signifie TabError et d'où vient-il en pratique ?
  8. 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

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.