Débogage
Lire un traceback, choisir la bonne stratégie, maîtriser pdb et le débogueur VS Code, reconnaître les erreurs fréquentes en Python, JS et C — et développer l'instinct du débogueur.
État d'esprit du débogueur
Un bug n'est pas une catastrophe — c'est un écart entre ce que le programme fait et ce que tu penses qu'il fait. La plupart des bugs existent parce que le développeur avait une hypothèse incorrecte sur le comportement du code. Le débogage, c'est retrouver cette hypothèse fausse.
Les pièges classiques du débogage :
| Mauvaise habitude | Pourquoi ça ne marche pas |
|---|---|
| Modifier le code au hasard jusqu'à ce que ça marche | Crée de nouveaux bugs cachés, ne comprend pas la cause |
| Lire le code en cherchant l'erreur "à l'œil" | Le cerveau voit ce qu'il veut voir, pas ce qui est écrit |
| Assumer que le bug est dans une bibliothèque externe | Dans 99% des cas, le bug est dans ton code |
| Déboguer mentalement sans exécuter de tests | Les intuitions sont souvent fausses sur le comportement réel |
| Corriger le symptôme sans trouver la cause | Le bug réapparaîtra sous une autre forme |
Rubber Duck Debugging — expliquer son code à voix haute, ligne par ligne, à un canard en plastique (ou un collègue, ou soi-même). Le simple fait de verbaliser force à articuler les hypothèses implicites, et c'est souvent là que l'erreur saute aux yeux avant même de finir la phrase.
Lire un traceback
Un traceback Python se lit de bas en haut : la dernière ligne est l'erreur, les lignes au-dessus sont la pile d'appels qui a mené à l'erreur.
ZeroDivisionError : on divise par zéro. La liste passée à la fonction est vide.division by zero : confirme la cause directe.line 12, in calculer_moyenne → c'est len(liste) qui vaut 0.line 18 : c'est main.py qui a appelé la fonction avec une liste vide. La vraie cause est peut-être là.Tracebacks JS (Node.js / navigateur) :
app.js:24 dans getUser. user est undefined à ce moment-là.Erreur de compilation C :
^ pointe sur le token fautif. Ici une faute de frappe dans le nom de variable.Quand il y a plusieurs erreurs (C, Java…) : corriger toujours la première en premier. Les erreurs suivantes sont souvent des effets de cascade de la première.
Méthode scientifique de débogage
print(f"liste reçue : {liste}, len : {len(liste)}")if len == 0: return 0# Le bug est quelque part dans 100 lignes.
# Stratégie : commenter la moitié du code
# et tester si le bug est encore là.
def traiter_donnees(data):
data = nettoyer(data) # ← est-ce que le bug est ici ?
# --- POINT DE TEST DICHOTOMIE ---
print("data après nettoyage:", data) # vérifier ici
# ---------------------------------
data = transformer(data) # ou ici ?
data = calculer_stats(data) # ou ici ?
return exporter(data) # ou ici ?
# Si le print montre des données correctes :
# → le bug est APRÈS (dans transformer, calculer_stats, exporter)
# Si les données sont déjà incorrectes :
# → le bug est DANS nettoyer ou dans les données d'entrée
# Recommencer avec la moitié qui contient le bug.
# Transformer un bug complexe en exemple minimal
# ✗ Trop complexe pour déboguer
app = Application()
app.charger_config("config.json")
app.connecter_base()
app.lancer_serveur()
# ... 200 lignes plus tard : KeyError quelque part
# ✓ Cas minimal : isoler le dictionnaire incriminé
d = {"utilisateurs": [{"nom": "Alice"}]}
# Reproduit l'erreur sans l'application entière :
print(d["utilisateurs"][0]["email"]) # → KeyError: 'email'
# Maintenant la cause est évidente :
# la clé "email" n'existe pas dans le dict utilisateur.
print & logging
# ✗ Print inutile — ne donne aucune info
print(x)
# ✓ Print informatif — type, valeur, contexte
print(f"[DEBUG] x = {x!r} (type: {type(x).__name__})")
# !r utilise repr() → montre None vs "None", '' vs " ", etc.
# ✓ Inspecter un dictionnaire ou une liste
import json
print(json.dumps(data, indent=2, default=str)) # lisible
# ✓ Tracer le flux d'exécution
def ma_fonction(x, y):
print(f"→ ma_fonction appelée avec x={x!r}, y={y!r}")
result = x + y
print(f"← ma_fonction retourne {result!r}")
return result
# ✓ Afficher la pile d'appels depuis n'importe où
import traceback
traceback.print_stack() # qui a appelé cette fonction ?
# ✓ Breakpoint express (Python 3.7+)
breakpoint() # équivalent à import pdb; pdb.set_trace()
# mais peut être désactivé avec PYTHONBREAKPOINT=0
import logging
# Configuration de base (à mettre dans le main)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%H:%M:%S",
# filename="app.log" ← pour écrire dans un fichier
)
logger = logging.getLogger(__name__)
# 5 niveaux — du plus détaillé au plus critique
logger.debug(f"Valeur intermédiaire : {x}") # dev uniquement
logger.info("Traitement démarré") # flux normal
logger.warning(f"Liste vide reçue pour {id}") # suspect
logger.error("Connexion DB échouée") # erreur récupérable
logger.critical("Crash imminent") # fatal
# En production : passer à WARNING pour ne voir que les problèmes
logging.basicConfig(level=logging.WARNING)
# Avantage sur print : on peut filtrer, horodater,
# rediriger vers fichier/Sentry/Datadog sans changer le code
Les print() de débogage doivent être retirés avant de committer. Utiliser logging.debug() est meilleur : il peut être désactivé sans toucher au code, et ne pollue pas la sortie en production.
assert & préconditions
# assert condition, "message si faux"
# Lance AssertionError si la condition est False
def calculer_moyenne(notes: list) -> float:
# Préconditions : rendre les hypothèses explicites
assert isinstance(notes, list), f"notes doit être une liste, reçu {type(notes)}"
assert len(notes) > 0, "La liste ne doit pas être vide"
assert all(0 <= n <= 20 for n in notes), "Toutes les notes doivent être entre 0 et 20"
# Traitement
moyenne = sum(notes) / len(notes)
# Postcondition : vérifier le résultat
assert 0 <= moyenne <= 20, f"Résultat incohérent : {moyenne}"
return moyenne
# Utilisation
calculer_moyenne(["a", "b"]) # → AssertionError: notes doit être une liste...
calculer_moyenne([]) # → AssertionError: La liste ne doit pas être vide
calculer_moyenne([10, 14]) # → 12.0
# ⚠️ assert est désactivé avec python -O (optimized)
# → pour les vraies validations d'entrée, utiliser ValueError/TypeError
if not notes:
raise ValueError("La liste de notes ne peut pas être vide")
# Vérifier qu'un invariant est maintenu à chaque itération
def tri_bulles(lst):
n = len(lst)
for i in range(n):
for j in range(0, n - i - 1):
if lst[j] > lst[j + 1]:
lst[j], lst[j + 1] = lst[j + 1], lst[j]
# Invariant : les i derniers éléments sont à leur place
assert lst[n-i-1:] == sorted(lst)[n-i-1:], \
f"Invariant violé à l'itération {i}: {lst}"
return lst
| Outil | Quand l'utiliser |
|---|---|
assert | Vérifier des hypothèses développeur en dev/test |
raise ValueError | Valider des entrées utilisateur/API en production |
raise TypeError | Mauvais type passé à une fonction |
logging.warning | Situation anormale mais récupérable |
| Test unitaire | Vérification reproductible et automatisée |
Débogueur Python — pdb
# Méthode 1 — breakpoint() dans le code (Python 3.7+)
def ma_fonction(x):
breakpoint() # le programme s'arrête ici
return x * 2
# Méthode 2 — depuis la ligne de commande
python -m pdb mon_script.py
# Méthode 3 — après une exception (post-mortem)
import pdb
try:
code_qui_plante()
except:
pdb.post_mortem() # inspecter l'état au moment du crash
# L'invite pdb ressemble à ceci :
# > main.py(8)ma_fonction()
# -> return x * 2
# (Pdb) _
# Navigation
n # next — ligne suivante (sans entrer)
s # step — entrer dans la fonction
r # return — sortir de la fonction
c # continue — jusqu'au prochain breakpoint
q # quit — quitter pdb
# Inspection
p x # print(x) — afficher une variable
pp x # pretty-print (dicts, listes...)
l # list — afficher le code autour
ll # longlist — toute la fonction
w # where — pile d'appels
a # args — arguments de la fonction
# Breakpoints
b 12 # breakpoint à la ligne 12
b fn # breakpoint au début de fn
b # lister les breakpoints
cl 1 # clear breakpoint #1
b 12, x>5 # breakpoint conditionnel !
# Expressions
!x = 42 # modifier x en cours d'exécution
# Toute expression Python est valide dans pdb
# Code
def traiter(items):
total = 0
breakpoint()
for item in items:
total += item["valeur"]
return total
# Session pdb
# > traiter(items)
# (Pdb) p items
# [{'valeur': 10}, {'val': 20}] ← ici ! "val" pas "valeur"
# (Pdb) p items[1]
# {'val': 20} ← clé différente !
# (Pdb) n ← avancer
# (Pdb) n
# KeyError: 'valeur' ← bug trouvé !
# (Pdb) q
ipdb — version améliorée de pdb avec coloration syntaxique et autocomplétion. Installer avec pip install ipdb. S'utilise de la même façon : import ipdb; ipdb.set_trace(). Fortement recommandé pour une meilleure expérience en terminal.
Dans pdb, appuyer sur Entrée sans rien taper répète la dernière commande. Pratique pour avancer rapidement ligne par ligne avec n répété.
VS Code Debugger
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python : fichier courant",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
},
{
"name": "Python : main.py avec args",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"args": ["--input", "data.csv"],
"env": { "DEBUG": "1" }
},
{
"name": "Python : tests pytest",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": ["-v", "tests/"]
}
]
}
| Action | Raccourci | Équivalent pdb |
|---|---|---|
| Lancer le débogueur | F5 | python -m pdb |
| Ligne suivante (sans entrer) | F10 | n |
| Entrer dans la fonction | F11 | s |
| Sortir de la fonction | Shift+F11 | r |
| Continuer jusqu'au BP | F5 | c |
| Poser/retirer un BP | F9 | b / cl |
| Arrêter | Shift+F5 | q |
Breakpoints conditionnels dans VS Code : clic droit sur un breakpoint existant → "Edit Breakpoint…" → entrer une condition Python ex: i == 5 ou len(data) == 0. Le programme ne s'arrêtera que quand la condition est vraie. Idéal pour les boucles avec des milliers d'itérations.
Watch expressions — dans le panneau "Watch" du débogueur VS Code, ajouter des expressions à surveiller (len(data), user.is_admin…). Elles se mettent à jour automatiquement à chaque pas.
Erreurs Python
if qui n'a pas été exécuté).# ✗ Faute de frappe
compteur = 0
compteur += 1
print(comteur) # NameError: name 'comteur' is not defined
# ✗ Variable définie conditionnellement
if condition:
resultat = calculer()
print(resultat) # NameError si condition était False
# ✓ Initialiser avant le if
resultat = None
if condition:
resultat = calculer()
print(resultat) # None si condition False, valeur sinon
None là où un objet est attendu.# ✗ str + int sans conversion
age = input("Âge : ") # input() renvoie TOUJOURS une str
print("Dans 10 ans : " + age + 10) # TypeError: can only concatenate str
# ✓ Convertir explicitement
age = int(input("Âge : "))
print(f"Dans 10 ans : {age + 10}")
# ✗ Appeler None comme une fonction
def get_user():
user = chercher_user()
# oubli du return !
user = get_user()
user.afficher() # TypeError: 'NoneType' object is not callable
# Diagnostic rapide : toujours vérifier le type
print(f"type(x) = {type(x)}, valeur = {x!r}")
# ✗ IndexError — liste vide ou index trop grand
lst = [1, 2, 3]
print(lst[5]) # IndexError: list index out of range
# ✓ Vérifier avant d'accéder
if len(lst) > 5:
print(lst[5])
# ✗ KeyError — clé absente
d = {"nom": "Alice"}
print(d["email"]) # KeyError: 'email'
# ✓ Option 1 : .get() avec valeur par défaut
print(d.get("email", "non renseigné"))
# ✓ Option 2 : vérifier avec `in`
if "email" in d:
print(d["email"])
None inattendu ou un mauvais type.# ✗ Méthode inexistante sur le type réel
x = 42
x.upper() # AttributeError: 'int' object has no attribute 'upper'
# → x est un int, pas une str. D'où vient ce int ?
# ✗ Objet None — très fréquent !
user = trouver_user("alice") # peut retourner None
print(user.nom) # AttributeError: 'NoneType' object has no attribute 'nom'
# ✓ Vérifier None avant d'accéder
user = trouver_user("alice")
if user is not None:
print(user.nom)
# ou avec le walrus operator (Python 3.8+)
if user := trouver_user("alice"):
print(user.nom)
# ✗ Mélange tabs et espaces (invisible à l'œil !)
def f():
x = 1 # 4 espaces
y = 2 # 1 tabulation → IndentationError
# ✓ VS Code : afficher les caractères invisibles
# View → Render Whitespace (ou Ctrl+Shift+P → "Toggle Render Whitespace")
# Configurer : "Editor: Detect Indentation" → off, "Editor: Insert Spaces" → on
# ✓ Convertir les tabs en espaces dans VS Code
# Ctrl+Shift+P → "Convert Indentation to Spaces"
# ✓ En ligne de commande
python -tt mon_fichier.py # détecte les TabError
Erreurs JavaScript
undefined ou null à ce moment-là. Très fréquent avec les données asynchrones (fetch, setTimeout) qui ne sont pas encore arrivées.// ✗ Données async pas encore disponibles
let user;
fetch("/api/user").then(r => r.json()).then(d => { user = d; });
console.log(user.name); // TypeError ! fetch n'est pas encore terminé
// ✓ Attendre la promesse
const user = await fetch("/api/user").then(r => r.json());
console.log(user.name); // OK
// ✓ Optional chaining ?. (ES2020)
console.log(user?.address?.city); // undefined si user est null, pas d'erreur
// ✓ Nullish coalescing ?? pour valeur par défaut
const name = user?.name ?? "Inconnu";
let/const avant sa déclaration (temporal dead zone), ou variable hors portée de bloc.// ✗ let/const non hoistées (temporal dead zone)
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
// ✗ Variable de bloc inaccessible hors du bloc
if (true) {
let result = 42;
}
console.log(result); // ReferenceError: result is not defined
// ✓ Déclarer dans la bonne portée
let result;
if (true) {
result = 42;
}
console.log(result); // 42
NaN, undefined, ou des comparaisons inattendues.// NaN — contagieux et silencieux
"abc" * 2 // → NaN (pas d'erreur !)
NaN === NaN // → false ← piège !
Number.isNaN(x) // ✓ bonne façon de tester
// == vs === (toujours utiliser ===)
0 == "" // → true (coercition !)
0 === "" // → false (correct)
null == undefined // → true
// typeof — prudence avec null
typeof null // → "object" (bug historique JS !)
x === null // ✓ seule façon fiable de tester null
// Array.sort() par défaut = alphabétique !
[10, 9, 2].sort() // → [10, 2, 9] ← FAUX
[10, 9, 2].sort((a,b)=>a-b) // → [2, 9, 10] ← correct
Erreurs C
// ✗ Pointeur NULL non vérifié
char *p = malloc(100);
// si malloc échoue, p == NULL
p[0] = 'a'; // segfault si p est NULL !
// ✓ Toujours vérifier malloc
char *p = malloc(100);
if (p == NULL) { perror("malloc"); exit(1); }
// ✗ Accès hors tableau
int tab[5] = {0};
for (int i = 0; i <= 5; i++) // ← <= au lieu de <
tab[i] = i; // segfault à i=5
// ✓ Détecter avec Valgrind ou AddressSanitizer
gcc -fsanitize=address -g main.c -o prog
./prog # affiche où exactement la mémoire est corrompue
// ✗ Variable non initialisée
int x;
printf("%d\n", x); // UB : valeur aléatoire en mémoire
// ✗ Dépassement entier signé
int max = INT_MAX;
max + 1; // UB en C (défini pour unsigned)
// ✗ Modification d'un littéral de chaîne
char *s = "hello";
s[0] = 'H'; // segfault ou UB (mémoire en lecture seule)
// ✓ Utiliser un tableau de chars
char s[] = "hello"; // copie modifiable
// Compiler avec tous les warnings !
gcc -Wall -Wextra -Wpedantic -g main.c
malloc() doit être suivi d'un free(). Oublier de libérer la mémoire provoque une fuite qui grossit au fil du temps — critique pour les programmes longs.char *tampon = malloc(256);
// utiliser tampon...
// oubli de free(tampon) → memory leak
// ✓ Toujours libérer, même en cas d'erreur
char *tampon = malloc(256);
if (!tampon) { exit(1); }
// ... traitement ...
free(tampon); // obligatoire
tampon = NULL; # bonus : évite use-after-free
// Détecter avec Valgrind :
valgrind --leak-check=full ./mon_programme
// == HEAP SUMMARY: 256 bytes in 1 blocks are definitely lost
Bugs logiques — les plus difficiles
Les bugs logiques ne provoquent aucune erreur — le programme tourne, mais produit un résultat incorrect. Ce sont les plus difficiles à trouver car il n'y a pas de traceback pour guider.
< vs <=, un index commençant à 0 ou 1.# ✗ Traite n-1 éléments au lieu de n
for i in range(len(lst) - 1): # oubli du dernier
traiter(lst[i])
# ✗ Calcul de longueur de sous-chaîne
s = "bonjour"
# longueur de s[2:5] → 5-2 = 3, pas 5-2+1
# ✓ Astuce : tester avec n=0, n=1, n=2
# Pour les boucles : vérifier la première et dernière itération
for provoque des sauts ou des doublons.# ✗ Suppression pendant itération — saute des éléments
for item in liste:
if item < 0:
liste.remove(item) # comportement imprévisible
# ✓ Itérer sur une copie
for item in liste[:]: # copie avec [:]
if item < 0:
liste.remove(item)
# ✓ Encore mieux : list comprehension
liste = [x for x in liste if x >= 0]
# ✗ Alias — modifie les deux
a = [1, 2, 3]
b = a # b est un alias, pas une copie !
b.append(4)
print(a) # → [1, 2, 3, 4] ← a a été modifié !
# ✓ Copie superficielle
b = a.copy() # ou a[:]
b = list(a)
# ✓ Copie profonde (listes imbriquées)
import copy
b = copy.deepcopy(a)
# ✗ Argument mutable par défaut — piège classique !
def ajouter(item, liste=[]): # [] créé UNE SEULE FOIS
liste.append(item)
return liste
ajouter(1) # → [1]
ajouter(2) # → [1, 2] ← pas [2] !
# ✓ Corriger avec None
def ajouter(item, liste=None):
if liste is None: liste = []
liste.append(item)
return liste
== est presque toujours une erreur.0.1 + 0.2 == 0.3 # → False !
print(0.1 + 0.2) # → 0.30000000000000004
# ✓ Comparer avec une tolérance
import math
math.isclose(0.1 + 0.2, 0.3) # → True
abs(a - b) < 1e-9 # tolérance manuelle
# ✓ Pour les calculs financiers : decimal
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # → Decimal('0.3')
Déboguer la POO
class Animal:
def __init__(self, nom, age):
self.nom = nom
self.age = age
# ✓ Toujours implémenter __repr__ pour le débogage
def __repr__(self):
return f"Animal(nom={self.nom!r}, age={self.age!r})"
a = Animal("Rex", 3)
print(a) # Animal(nom='Rex', age=3) ← utile dans print() et pdb
# Inspecter tous les attributs d'un objet
vars(a) # → {'nom': 'Rex', 'age': 3}
a.__dict__ # idem
dir(a) # liste TOUS les attributs + méthodes héritées
# Inspecter l'héritage
type(a) # → <class '__main__.Animal'>
type(a).__mro__ # → MRO : ordre de résolution des méthodes
isinstance(a, Animal) # → True
issubclass(Chien, Animal) # → True si Chien hérite de Animal
# Savoir d'où vient une méthode
a.afficher.__qualname__ # → 'Animal.afficher' ou 'Chien.afficher' ?
# ✗ Attribut de classe vs attribut d'instance
class Compteur:
total = 0 # ← attribut de CLASSE, partagé par toutes les instances !
def incrementer(self):
Compteur.total += 1 # toutes les instances voient le même total
# ✓ Attribut d'instance dans __init__
class Compteur:
def __init__(self):
self.total = 0 # attribut de l'instance uniquement
# ✗ Oublier self — appel de méthode échoue
class Foo:
def calculer(self):
return helper() # NameError : manque self.helper()
# ✗ super() oublié dans __init__ → attributs du parent manquants
class Chien(Animal):
def __init__(self, nom, age, race):
# oubli de super().__init__(nom, age) !
self.race = race
# → AttributeError: 'Chien' object has no attribute 'nom'
# ✓ Correction
class Chien(Animal):
def __init__(self, nom, age, race):
super().__init__(nom, age) # ← obligatoire
self.race = race
Cheat Sheet Débogage
🔍 Lire un traceback
| Python | Lire de bas en haut |
| JavaScript | Lire de haut en bas |
| C | Format fichier:ligne:colonne |
| Plusieurs erreurs | Corriger la première d'abord |
| Erreur confuse | Chercher la vraie cause en amont |
🛠️ Stratégies
print(f"{x=}") | Debug rapide (Python 3.8+) |
logging.debug() | Debug production |
assert cond, msg | Valider hypothèses |
breakpoint() | Lancer pdb |
| Binary search | Isoler par dichotomie |
| MRE | Cas minimal de reproduction |
⌨️ pdb essentiel
n | Ligne suivante |
s | Entrer dans la fonction |
c | Continuer jusqu'au BP |
p x | Afficher x |
b 12, x>5 | BP conditionnel |
q | Quitter pdb |
🐍 Erreurs Python
NameError | Variable non définie / faute de frappe |
TypeError | Mauvais type / None inattendu |
IndexError | Hors bornes de liste |
KeyError | Clé absente → utiliser .get() |
AttributeError | Attribut manquant / None |
IndentationError | Mix tabs/espaces |
F5 # Lancer / Continuer
F9 # Poser / retirer un breakpoint
F10 # Step over (ligne suivante)
F11 # Step into (entrer fonction)
Shift+F11 # Step out (sortir fonction)
Shift+F5 # Arrêter
Ctrl+Shift+F5 # Redémarrer
□ J'ai lu le traceback complet (pas juste la dernière ligne)
□ J'ai affiché les valeurs des variables au point du crash
□ J'ai un cas minimal qui reproduit le bug
□ J'ai vérifié le type réel de chaque variable impliquée
□ J'ai testé avec des valeurs limites (None, [], 0, "")
□ J'ai cherché le message d'erreur exact sur le web
□ J'ai essayé d'expliquer le bug à voix haute (rubber duck)