41. Déboguer et tester votre code
Écrire du code n’est que la moitié du travail. L’autre moitié consiste à s’assurer que votre code fonctionne correctement et à trouver les problèmes lorsqu’il ne fonctionne pas. Chaque programmeur, du débutant à l’expert, écrit du code avec des bugs. La différence, c’est que les programmeurs expérimentés ont développé des approches systématiques pour trouver et corriger ces bugs.
Dans ce chapitre, vous apprendrez des techniques pratiques de débogage qui vous aident à comprendre ce que fait réellement votre code, à localiser rapidement les problèmes et à vérifier que votre code fonctionne comme prévu. Ces compétences feront de vous un programmeur plus confiant et plus efficace.
41.1) Lire les tracebacks pour localiser les erreurs (rappel rapide)
Comme nous l’avons appris au chapitre 24, Python fournit des messages d’erreur détaillés appelés tracebacks lorsqu’un problème survient. Révisons comment les lire efficacement, puisque c’est votre première ligne de défense lors du débogage.
41.1.1) L’anatomie d’un traceback
Lorsque Python rencontre une erreur, il vous montre exactement où le problème s’est produit et quel type d’erreur c’était. Voici un traceback typique :
def calculate_average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
def process_student_grades(grades):
average = calculate_average(grades)
return f"Average: {average:.1f}"
# Ceci provoquera une erreur
student_grades = []
result = process_student_grades(student_grades)
print(result)Output:
Traceback (most recent call last):
File "grades.py", line 12, in <module>
result = process_student_grades(student_grades)
File "grades.py", line 7, in process_student_grades
average = calculate_average(grades)
File "grades.py", line 4, in calculate_average
return total / count
~~~~~~^~~~~~~
ZeroDivisionError: division by zeroDécomposons ce que ce traceback nous dit :
Lecture de bas en haut :
- Le type d’erreur et le message (en bas) :
ZeroDivisionError: division by zeronous indique exactement ce qui s’est mal passé - La ligne exacte où l’erreur s’est produite :
return total / countà la ligne 4 - La chaîne d’appels montrant comment on en est arrivé là : a commencé à la ligne 12, est passé par la ligne 7, s’est terminé à la ligne 4
41.1.2) Utiliser les tracebacks pour trouver la cause racine
Le traceback vous montre le symptôme (où l’erreur s’est produite), mais vous devez trouver la cause (pourquoi elle s’est produite). Examinons le déroulement du problème :
# L'erreur se produit ici
return total / count # count vaut 0
# Mais le vrai problème est ici
student_grades = [] # Liste vide passée à la fonctionLa division par zéro se produit parce que nous avons passé une liste vide. Le traceback pointe vers la ligne 4, mais la correction doit se faire plus tôt — soit en validant l’entrée, soit en gérant le cas de la liste vide :
def calculate_average(numbers):
"""Return the average of numbers, or None if the list is empty."""
if not numbers:
return None
return sum(numbers) / len(numbers)
def process_student_grades(grades):
"""Process student grades and return a formatted string."""
average = calculate_average(grades)
if average is None:
return "No grades to process"
return f"Average: {average:.1f}"
# Maintenant, cela fonctionne en toute sécurité
student_grades = []
result = process_student_grades(student_grades)
print(result) # Output: No grades to process
# Et ceci fonctionne aussi
student_grades = [85, 92, 78, 90]
result = process_student_grades(student_grades)
print(result) # Output: Average: 86.2Points clés à retenir :
- Lisez les tracebacks de bas en haut
- L’emplacement de l’erreur (symptôme) n’est pas toujours la cause racine
- Validez les entrées tôt pour éviter les erreurs plus tard
- Utilisez la programmation défensive (
.get(), vérifications de longueur) pour un code plus sûr
Différents types d’erreurs produisent différents tracebacks, mais le processus de lecture est toujours le même : commencez en bas pour voir ce qui s’est mal passé, puis remontez pour comprendre comment vous en êtes arrivé là. Si vous avez besoin d’un rappel sur des types d’exceptions spécifiques, reportez-vous au chapitre 24.
Maintenant que vous savez lire efficacement les tracebacks, apprenons à suivre l’exécution de votre code mentalement pour comprendre ce qu’il fait étape par étape.
41.2) Tracer l’exécution du code mentalement
Parfois, vous rencontrez un bug mais vous ne pouvez pas exécuter immédiatement le code — peut-être êtes-vous en train de relire du code sur papier, de lire la pull request de quelqu’un d’autre, ou d’essayer de comprendre pourquoi une fonction se comporte de façon inattendue. Dans ces situations, l’exécution mentale — parcourir le code ligne par ligne dans votre tête, en suivant ce qui arrive à chaque variable — devient inestimable.
Même les programmeurs expérimentés utilisent régulièrement cette technique. Avant d’ajouter des instructions print ou d’exécuter un débogueur, ils tracent souvent mentalement quelques itérations pour formuler une hypothèse sur l’endroit où se trouve le problème. C’est plus rapide que l’essai-erreur et cela vous aide à comprendre votre code plus profondément.
L’exécution mentale est particulièrement utile lorsque :
- Vous lisez du code inconnu pour comprendre ce qu’il fait
- Vous relisez de petites fonctions (5 à 15 lignes) avant de les exécuter
- Vous déboguez des erreurs de logique où le code s’exécute mais produit de mauvais résultats
- Vous comprenez le comportement d’une boucle quand le schéma n’est pas immédiatement évident
- Vous faites une revue de code où vous ne pouvez pas facilement exécuter le code vous-même
Pour du code plus volumineux ou plus complexe, vous combinerez le traçage mental avec d’autres techniques que nous verrons plus loin dans ce chapitre. Mais maîtriser cette compétence fera de vous un débogueur bien plus efficace.
41.2.1) Le processus d’exécution mentale
Quand vous exécutez du code mentalement, vous agissez comme l’interpréteur Python, en suivant les mêmes règles que Python. Entraînons-nous avec un exemple simple :
def find_maximum(numbers):
max_value = numbers[0]
for num in numbers:
if num > max_value:
max_value = num
return max_value
result = find_maximum([3, 7, 2, 9, 5])
print(result) # Output: 9Voici comment tracer ce code :
Trace pas à pas :
Initial state:
numbers = [3, 7, 2, 9, 5]
max_value = 3 (numbers[0])
Iteration 1: num = 3
Check: 3 > 3? → False
max_value remains 3
Iteration 2: num = 7
Check: 7 > 3? → True
max_value = 7 ✓
Iteration 3: num = 2
Check: 2 > 7? → False
max_value remains 7
Iteration 4: num = 9
Check: 9 > 7? → True
max_value = 9 ✓
Iteration 5: num = 5
Check: 5 > 9? → False
max_value remains 9
Return: 941.2.2) Créer une table de traçage
Pour du code plus complexe, créez une table de traçage qui montre comment les variables changent au fil du temps. C’est particulièrement utile pour les boucles et les structures imbriquées :
def calculate_running_totals(numbers):
totals = []
running_sum = 0
for num in numbers:
running_sum += num
totals.append(running_sum)
return totals
result = calculate_running_totals([10, 20, 30, 40])
print(result) # Output: [10, 30, 60, 100]Table de traçage :
Le tableau montre l’état des variables à chaque étape. Remarquez comment running_sum change de « avant » à « après » chaque addition :
| Itération | num | running_sum (avant) | running_sum (après) | totals |
|---|---|---|---|---|
| Début | - | 0 | 0 | [] |
| 1 | 10 | 0 | 10 | [10] |
| 2 | 20 | 10 | 30 | [10, 30] |
| 3 | 30 | 30 | 60 | [10, 30, 60] |
| 4 | 40 | 60 | 100 | [10, 30, 60, 100] |
Créer ce tableau vous aide à voir exactement comment les données circulent dans votre code. Si la sortie ne correspond pas à ce que vous attendez, vous pouvez identifier précisément où les choses dérapent.
41.2.3) Tracer la logique conditionnelle
Les instructions conditionnelles exigent une attention particulière quant aux branches qui s’exécutent. Traçons un exemple plus complexe :
def categorize_grade(score):
if score >= 90:
category = "Excellent"
bonus = 10
elif score >= 80:
category = "Good"
bonus = 5
elif score >= 70:
category = "Satisfactory"
bonus = 0
else:
category = "Needs Improvement"
bonus = 0
final_score = score + bonus
return category, final_score
result = categorize_grade(85)
print(result) # Output: ('Good', 90)Trace mentale pour score = 85 :
- Vérifier
85 >= 90→ False, ignorer le premier bloc - Vérifier
85 >= 80→ True, entrer dans le second bloc - Définir
category = "Good"etbonus = 5 - Ignorer les blocs
elifrestants etelse(une correspondance a déjà été trouvée) - Calculer
final_score = 85 + 5 = 90 - Retourner
("Good", 90)
41.2.4) Tracer les appels et retours de fonctions
Quand des fonctions s’appellent entre elles, vous devez suivre la pile d’appels (call stack) — la séquence des appels de fonctions et leurs variables locales :
def calculate_tax(amount, rate):
tax = amount * rate
return tax
def calculate_total(price, quantity, tax_rate):
subtotal = price * quantity
tax = calculate_tax(subtotal, tax_rate)
total = subtotal + tax
return total
result = calculate_total(50, 3, 0.08)
print(f"Total: ${result:.2f}") # Output: Total: $162.00Trace avec pile d’appels :
┌─ calculate_total(50, 3, 0.08)
│ price = 50, quantity = 3, tax_rate = 0.08
│ subtotal = 150
│
│ ┌─ calculate_tax(150, 0.08)
│ │ amount = 150, rate = 0.08
│ │ tax = 12.0
│ │ return 12.0
│ └─
│
│ tax = 12.0 (from calculate_tax)
│ total = 162.0
│ return 162.0
└─
result = 162.0Cette trace pas à pas montre exactement comment les données circulent entre les fonctions. Lors du débogage, si le résultat final est incorrect, vous pouvez remonter pour voir quelle fonction a produit une valeur intermédiaire incorrecte.
Le traçage mental est puissant, mais pour du code complexe cela peut être fastidieux. Dans la section suivante, nous apprendrons à utiliser des instructions print de manière stratégique pour voir ce qui se passe réellement pendant l’exécution, ce qui est souvent plus rapide et plus fiable que l’exécution mentale seule.
41.3) Déboguer avec print : f"{var=}" et repr()
Bien que l’exécution mentale fonctionne bien pour de petites fonctions, elle devient peu pratique pour du code plus volumineux ou plus complexe. Quand vous ne savez pas ce qui se passe dans une boucle, ou quand un calcul produit des résultats inattendus, le moyen le plus rapide d’enquêter consiste souvent à ajouter des instructions print() stratégiques.
Le débogage par print a certains avantages par rapport à d’autres techniques :
- Aucun outil spécial nécessaire : fonctionne dans n’importe quel environnement Python
- Rapide à mettre en place : ajoutez un print en quelques secondes
- Sortie claire : vous voyez exactement ce que vous avez demandé
- Facile à retirer : supprimez les prints une fois terminé
Les développeurs professionnels utilisent tout le temps le débogage par print — ce n’est pas une technique « de débutant ». Apprenons à l’utiliser efficacement.
41.3.1) Débogage de base avec print
L’approche de débogage la plus simple consiste à afficher les valeurs des variables à des points clés de votre code :
def process_order(items, discount_rate):
print(f"Starting process_order")
print(f"Items: {items}")
print(f"Discount rate: {discount_rate}")
subtotal = sum(item['price'] * item['quantity'] for item in items)
print(f"Subtotal: {subtotal}")
discount = subtotal * discount_rate
print(f"Discount amount: {discount}")
total = subtotal - discount
print(f"Final total: {total}")
return total
order_items = [
{'name': 'Book', 'price': 25.99, 'quantity': 2},
{'name': 'Pen', 'price': 3.50, 'quantity': 5}
]
result = process_order(order_items, 0.10)Output:
Starting process_order
Items: [{'name': 'Book', 'price': 25.99, 'quantity': 2}, {'name': 'Pen', 'price': 3.5, 'quantity': 5}]
Discount rate: 0.1
Subtotal: 69.47999999999999
Discount amount: 6.9479999999999995
Final total: 62.53199999999999Ces instructions print vous montrent le flux d’exécution et les valeurs à chaque étape. Si le résultat final est incorrect, vous pouvez voir exactement où le calcul a déraillé.
41.3.2) Utiliser f"{var=}" pour une inspection rapide
Python 3.8 a introduit une syntaxe de débogage pratique : f"{var=}". Elle affiche à la fois le nom de la variable et sa valeur :
def calculate_compound_interest(principal, rate, years):
# Approche traditionnelle
print(f"principal: {principal}")
print(f"rate: {rate}")
print(f"years: {years}")
# Approche plus propre avec f"{var=}"
print(f"{principal=}")
print(f"{rate=}")
print(f"{years=}")
# Vous pouvez utiliser des expressions, pas seulement des variables
print(f"{principal * rate=}")
print(f"{(1 + rate) ** years=}")
amount = principal * (1 + rate) ** years
print(f"{amount=}")
return amount
result = calculate_compound_interest(1000, 0.05, 10)Output:
principal: 1000
rate: 0.05
years: 10
principal=1000
rate=0.05
years=10
principal * rate=50.0
(1 + rate) ** years=1.628894626777442
amount=1628.89462677744241.3.3) Utiliser repr() pour voir la forme réelle des données
Parfois, ce que vous voyez affiché n’est pas exactement ce que vous croyez qu’il est. La fonction repr() vous montre la représentation exacte d’un objet, y compris les caractères cachés :
# Ces chaînes semblent identiques lorsqu'on les affiche
text1 = "Hello"
text2 = "Hello\n" # Contient un retour à la ligne à la fin
print("Using print():")
print(f"text1: {text1}")
print(f"text2: {text2}")
print("\nUsing repr():")
print(f"text1: {repr(text1)}")
print(f"text2: {repr(text2)}")Output:
Using print():
text1: Hello
text2: Hello
Using repr():
text1: 'Hello'
text2: 'Hello\n'La sortie de repr() montre que text2 a un caractère de retour à la ligne caché. C’est crucial lors du débogage du traitement des chaînes :
def clean_user_input():
# Les entrées utilisateur contiennent souvent des espaces invisibles
username = input("Enter username: ") # L'utilisateur saisit "Alice "
print(f"Username with print(): {username}")
print(f"Username with repr(): {repr(username)}")
# Nettoyer l'entrée
cleaned = username.strip()
print(f"Cleaned with repr(): {repr(cleaned)}")
return cleanedSi un utilisateur tape « Alice » suivi d’espaces puis appuie sur Entrée, vous pourriez voir :
Output:
Enter username: Alice
Username with print(): Alice
Username with repr(): 'Alice '
Cleaned with repr(): 'Alice'La sortie de repr() révèle les espaces de fin que print() ne montre pas clairement.
Quand utiliser repr() vs str() :
repr() est conçue pour les développeurs — elle montre la représentation « officielle » d’une chaîne qui pourrait recréer l’objet. str() (que print() utilise par défaut) est conçue pour les utilisateurs finaux — elle affiche une version lisible et conviviale.
Pour le débogage, repr() est généralement plus utile car elle révèle la structure réelle de vos données.
41.3.4) Placement stratégique des prints
Ne dispersez pas des instructions print partout. Placez-les de manière stratégique :
def calculate_shipping_cost(weight, distance, express=False):
print(f"=== calculate_shipping_cost called ===")
print(f"Input: {weight=}, {distance=}, {express=}")
# Calculer le coût de base
base_rate = 0.50
base_cost = weight * distance * base_rate
print(f"Calculated: {base_cost=}")
# Appliquer un supplément express
if express:
surcharge = base_cost * 0.50
print(f"Express surcharge: {surcharge=}")
total = base_cost + surcharge
else:
print("No express surcharge")
total = base_cost
print(f"Final: {total=}")
print(f"=== calculate_shipping_cost returning ===\n")
return total
# Tester différents scénarios
cost1 = calculate_shipping_cost(10, 500, express=True)
cost2 = calculate_shipping_cost(5, 200, express=False)Output:
=== calculate_shipping_cost called ===
Input: weight=10, distance=500, express=True
Calculated: base_cost=2500.0
Express surcharge: surcharge=1250.0
Final: total=3750.0
=== calculate_shipping_cost returning ===
=== calculate_shipping_cost called ===
Input: weight=5, distance=200, express=False
Calculated: base_cost=500.0
No express surcharge
Final: total=500.0
=== calculate_shipping_cost returning ===Les marqueurs clairs (===) et la sortie organisée facilitent le suivi du flux d’exécution.
41.3.5) Retirer les prints de débogage
Une fois que vous avez trouvé et corrigé le bug, pensez à retirer vos prints de débogage. Voici quelques stratégies :
Stratégie 1 : Utiliser un préfixe distinct
# Facile à trouver et à supprimer avec rechercher/remplacer
print(f"DEBUG: {total=}")
print(f"DEBUG: {items=}")Stratégie 2 : Utiliser un indicateur de débogage
DEBUG = True
def calculate_total(items):
if DEBUG:
print(f"Processing {len(items)} items")
total = sum(item['price'] for item in items)
if DEBUG:
print(f"{total=}")
return total
# Désactiver toute la sortie de débogage d'un coup
DEBUG = FalseStratégie 3 : Les commenter mais les garder
def process_data(data):
# print(f"DEBUG: {data=}") # Utile pour un futur débogage
result = transform(data)
# print(f"DEBUG: {result=}")
return resultPour une journalisation plus sophistiquée que vous pouvez laisser en code de production, Python dispose d’un module logging, mais de simples instructions print sont parfaites pour un débogage rapide pendant le développement.
Le débogage par print vous montre les valeurs des variables, mais parfois vous devez comprendre la structure d’un objet — quelles méthodes il a, quel est son type, et ce qu’il peut faire. Dans la section suivante, nous apprendrons à inspecter des objets avec type() et dir().
41.4) Inspecter des objets : type() et dir()
Le débogage par print vous montre les valeurs de vos variables, mais parfois le problème n’est pas la valeur — c’est le type d’objet avec lequel vous travaillez. Vous pourriez vous attendre à une liste mais recevoir une chaîne, ou vous travaillez avec un objet inconnu et vous ne savez pas quelles méthodes il prend en charge.
Python fournit des outils intégrés pour inspecter les objets : type() vous dit quel type d’objet vous avez, et dir() vous montre quelles opérations il prend en charge. Ces fonctions sont essentielles lorsque :
- Vous déboguez des erreurs liées aux types (TypeError, AttributeError)
- Vous travaillez avec des bibliothèques ou des API inconnues
- Vous cherchez à comprendre des objets renvoyés par du code tiers
- Vous vérifiez que votre code reçoit les types attendus
Apprenons à utiliser ces outils d’inspection efficacement.
41.4.1) Utiliser type() pour identifier les types d’objets
La fonction type() vous dit exactement quel type d’objet vous avez. C’est crucial lors du débogage d’erreurs liées aux types :
def process_data(data):
print(f"Received data: {data}")
print(f"Data type: {type(data)}")
if isinstance(data, list):
print("Processing as list")
return sum(data)
elif isinstance(data, dict):
print("Processing as dictionary")
return sum(data.values())
else:
print("Unexpected type!")
return None
# Tester avec différents types
result1 = process_data([10, 20, 30])
print(f"Result: {result1}\n")
result2 = process_data({'a': 10, 'b': 20, 'c': 30})
print(f"Result: {result2}\n")
result3 = process_data("123")
print(f"Result: {result3}")Output:
Received data: [10, 20, 30]
Data type: <class 'list'>
Processing as list
Result: 60
Received data: {'a': 10, 'b': 20, 'c': 30}
Data type: <class 'dict'>
Processing as dictionary
Result: 60
Received data: 123
Data type: <class 'str'>
Unexpected type!
Result: None41.4.2) Déboguer une confusion de types
La confusion de types est une source fréquente de bugs, en particulier lorsque vous travaillez avec des fonctions qui peuvent recevoir des données de plusieurs sources — saisie utilisateur, lecture de fichier, réponses d’API, ou autres fonctions. Vous pourriez vous attendre à une liste de nombres mais recevoir accidentellement une chaîne, ou vous attendre à un dictionnaire mais obtenir une liste.
Utiliser type() aide à identifier quand vous avez le mauvais type. En affichant le type tôt dans votre fonction, vous pouvez immédiatement repérer des incompatibilités de type avant qu’elles ne provoquent des messages d’erreur déroutants plus loin dans votre code :
def calculate_average(numbers):
print(f"{type(numbers)=}")
print(f"{numbers=}") # Montrer ce que nous avons réellement reçu
# Ceci échouera si numbers n'est pas une liste de nombres
total = sum(numbers)
count = len(numbers)
return total / count
# Erreur fréquente : oublié de convertir une chaîne en liste
scores = "85" # Devrait être [85] ou simplement 85
try:
avg = calculate_average(scores)
print(f"Average: {avg}")
except TypeError as e:
print(f"TypeError: {e}")
print(f"Expected list of numbers, got {type(scores)}")
print(f"The string contains: {repr(scores)}")Output:
type(numbers)=<class 'str'>
numbers='85'
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Expected list of numbers, got <class 'str'>
The string contains: '85'Le contrôle type() révèle immédiatement le problème : nous avons passé une chaîne alors qu’il nous fallait une liste. Sans cette sortie de débogage, vous auriez peut-être passé du temps à essayer de comprendre pourquoi sum() a échoué, alors que le vrai problème est que le mauvais type de données est entré dans la fonction dès le départ.
41.4.3) Utiliser dir() pour découvrir les méthodes disponibles
Quand vous travaillez avec des objets inconnus — qu’ils proviennent d’une bibliothèque que vous apprenez, d’une réponse d’API, ou même des types intégrés de Python — vous avez souvent besoin de savoir : « Que puis-je faire avec cet objet ? » La fonction dir() répond à cette question en listant tous les attributs et méthodes disponibles sur un objet.
C’est particulièrement utile lorsque :
- Vous explorez une nouvelle bibliothèque et voulez voir quelles méthodes un objet fournit
- Vous recevez un objet depuis du code tiers et devez comprendre ses capacités
- Vous avez oublié le nom exact d’une méthode que vous voulez utiliser
- Vous déboguez et voulez vérifier qu’un objet possède les méthodes que vous attendez
Explorons quelles méthodes possède une chaîne :
# Explorer quelles méthodes possède une chaîne
text = "Python Programming"
print(f"Type: {type(text)}")
print(f"\nAvailable string methods (showing first 10):")
methods = [m for m in dir(text) if not m.startswith('_')]
for method in methods[:10]: # Show first 10
print(f" {method}")
print(f" ... and {len(methods) - 10} more")Output:
Type: <class 'str'>
Available string methods (showing first 10):
capitalize
casefold
center
count
encode
endswith
expandtabs
find
format
format_map
... and 37 moreVous pouvez maintenant voir toutes les opérations disponibles sur les chaînes. Si vous ne saviez pas si les chaînes avaient une méthode count ou une méthode endswith, dir() vous montre qu’elles existent. Vous pouvez ensuite utiliser la fonction help() de Python pour en savoir plus sur une méthode spécifique :
# En savoir plus sur une méthode spécifique
help(text.count)Cela vous montrera la documentation de la méthode count :
Help on built-in function count:
count(sub[, start[, end]], /) method of builtins.str instance
Return the number of non-overlapping occurrences of substring sub in string S[start:end].
Optional arguments start and end are interpreted as in slice notation.La fonction dir() est comme avoir une documentation directement intégrée à Python — elle vous montre ce qui est possible avec n’importe quel objet sur lequel vous travaillez.
41.4.4) Inspecter des objets personnalisés
Quand vous travaillez avec des classes personnalisées, type() et dir() vous aident à comprendre à quoi vous avez affaire. De plus, Python fournit hasattr() pour vérifier si un objet a un attribut spécifique avant d’essayer d’y accéder — cela évite des exceptions AttributeError.
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def get_status(self):
return "Passing" if self.grade >= 60 else "Failing"
student = Student("Alice", 85)
print(f"Object type: {type(student)}")
print(f"\nAvailable attributes and methods:")
for attr in dir(student):
if not attr.startswith('_'):
print(f" {attr}")
# Vérifier si des attributs spécifiques existent
print(f"\nHas 'name' attribute: {hasattr(student, 'name')}")
print(f"Has 'age' attribute: {hasattr(student, 'age')}")
print(f"Has 'get_status' method: {hasattr(student, 'get_status')}")
# Maintenant, nous pouvons accéder en toute sécurité aux attributs dont nous savons qu'ils existent
if hasattr(student, 'name'):
print(f"\nStudent name: {student.name}")
else:
print("\nNo name attribute found")
if hasattr(student, 'get_status'):
print(f"Status: {student.get_status()}")
else:
print("No get_status method found")
# Cela évite des erreurs comme celle-ci :
# print(student.age) # Would raise AttributeError!Output:
Object type: <class '__main__.Student'>
Available attributes and methods:
get_status
grade
name
Has 'name' attribute: True
Has 'age' attribute: False
Has 'get_status' method: True
Student name: Alice
Status: PassingLa fonction hasattr() est essentielle pour écrire du code défensif — du code qui vérifie si les opérations sont sûres avant de les effectuer. La fonction renvoie True si l’attribut existe, False sinon — ce qui vous permet de prendre des décisions avant d’essayer d’accéder aux attributs. C’est particulièrement important lorsque vous travaillez avec des objets provenant de bibliothèques externes ou de saisies utilisateur, où vous ne pouvez pas garantir quels attributs seront présents.
41.4.5) Utiliser getattr() pour un accès sûr aux attributs
Quand vous n’êtes pas sûr qu’un attribut existe, utilisez getattr() avec une valeur par défaut :
def display_student_info(student):
"""Safely display student info even if some attributes are missing."""
print(f"Type: {type(student)}")
# Accès sûr aux attributs avec des valeurs par défaut
name = getattr(student, 'name', 'Unknown')
grade = getattr(student, 'grade', 0)
age = getattr(student, 'age', 'Not specified')
print(f"Name: {name}")
print(f"Grade: {grade}")
print(f"Age: {age}")
# Vérifier si la méthode existe avant de l'appeler
if hasattr(student, 'get_status'):
status = student.get_status()
print(f"Status: {status}")
# Utiliser la même classe Student que ci-dessus
student = Student("Bob", 72)
display_student_info(student)Output:
Type: <class '__main__.Student'>
Name: Bob
Grade: 72
Age: Not specified
Status: PassingCette approche évite des exceptions AttributeError lorsque vous travaillez avec des objets qui pourraient ne pas avoir tous les attributs attendus. La fonction getattr() est particulièrement utile lorsque :
- Vous travaillez avec des objets provenant d’API externes qui peuvent avoir différentes versions
- Vous gérez des attributs optionnels dans vos propres classes
- Vous construisez du code défensif qui gère élégamment les données manquantes
Comprendre le type d’objet que vous avez et les méthodes qu’il prend en charge est crucial pour le débogage. Mais parfois vous devez vérifier non seulement que votre code s’exécute, mais qu’il produit les bons résultats. Dans la section suivante, nous apprendrons à utiliser des instructions assert pour tester vos hypothèses et détecter les bugs tôt.
41.5) Tester avec des instructions assert
Nous avons appris à déboguer du code quand les choses tournent mal — lire les tracebacks, tracer l’exécution mentalement, utiliser des instructions print, et inspecter des objets. Mais il existe une meilleure approche que de corriger des bugs après leur apparition : les prévenir dès le départ grâce aux tests.
L’instruction assert est l’outil de test le plus simple de Python. Elle vous permet de vérifier que votre code se comporte correctement en contrôlant des hypothèses à des points critiques. Quand une assertion échoue, Python vous dit immédiatement ce qui s’est mal passé et où, ce qui facilite grandement la détection précoce des bugs.
Les assertions sont particulièrement utiles pour :
- Vérifier que des fonctions produisent les résultats attendus
- Contrôler que les entrées respectent vos exigences
- Tester des cas limites susceptibles de casser votre code
- Documenter les hypothèses sur lesquelles votre code s’appuie
Considérez les assertions comme des contrôles automatisés qui vérifient en continu que votre code fonctionne comme prévu. Voyons comment les utiliser efficacement.
41.5.1) Ce que fait assert
Une instruction assert vérifie qu’une condition est vraie. Si la condition est vraie, il ne se passe rien — le code continue normalement. Si elle est fausse, Python lève une AssertionError et arrête l’exécution.
Syntaxe :
assert condition, "Optional error message"condition: toute expression qui s’évalue à True ou False"Optional error message": texte utile affiché quand l’assertion échoue
Voici comment cela fonctionne en pratique :
# Assertions simples
x = 10
assert x > 0 # Réussit silencieusement (x est bien > 0)
assert x < 5 # Échec ! Lève AssertionError
# Avec des messages d'erreur (beaucoup plus utile !)
assert x > 0, f"x must be positive, got {x}"
assert x < 5, f"x must be less than 5, got {x}" # Échec avec un message clairVoyons maintenant les assertions dans une fonction réelle :
def calculate_discount(price, discount_percent):
# Vérifier que les entrées sont valides
assert price >= 0, "Price cannot be negative"
assert 0 <= discount_percent <= 100, "Discount must be between 0 and 100"
discount_amount = price * (discount_percent / 100)
final_price = price - discount_amount
# Vérifier que la sortie est cohérente
assert final_price >= 0, "Final price cannot be negative"
return final_price
# Des entrées valides fonctionnent correctement
result = calculate_discount(100, 20)
print(f"Price after 20% discount: ${result}") # Output: Price after 20% discount: $80.0
# Des entrées invalides déclenchent des assertions
try:
result = calculate_discount(-50, 20)
except AssertionError as e:
print(f"Assertion failed: {e}") # Output: Assertion failed: Price cannot be negative
try:
result = calculate_discount(100, 150)
except AssertionError as e:
print(f"Assertion failed: {e}") # Output: Assertion failed: Discount must be between 0 and 10041.5.2) Utiliser des assertions pour vérifier le comportement d’une fonction
Les assertions sont excellentes pour tester que des fonctions produisent les résultats attendus :
def calculate_average(numbers):
if not numbers:
return 0.0
return sum(numbers) / len(numbers)
# Tester avec diverses entrées
result = calculate_average([10, 20, 30])
assert result == 20.0, f"Expected 20.0, got {result}"
print(f"Test 1 passed: average of [10, 20, 30] = {result}")
result = calculate_average([5, 5, 5, 5])
assert result == 5.0, f"Expected 5.0, got {result}"
print(f"Test 2 passed: average of [5, 5, 5, 5] = {result}")
result = calculate_average([])
assert result == 0.0, f"Expected 0.0 for empty list, got {result}"
print(f"Test 3 passed: average of [] = {result}")
result = calculate_average([100])
assert result == 100.0, f"Expected 100.0, got {result}"
print(f"Test 4 passed: average of [100] = {result}")Output:
Test 1 passed: average of [10, 20, 30] = 20.0
Test 2 passed: average of [5, 5, 5, 5] = 5.0
Test 3 passed: average of [] = 0.0
Test 4 passed: average of [100] = 100.0Si une assertion échoue, vous savez immédiatement quel cas de test a révélé le problème.
41.5.3) Tester des cas limites
Les cas limites sont des entrées aux frontières de ce que votre fonction doit gérer. Les tester révèle des bugs que des entrées « normales » pourraient manquer :
def get_first_and_last(items):
"""Return the first and last items from a sequence."""
assert len(items) > 0, "Cannot get first and last from empty sequence"
return items[0], items[-1]
# Tester le cas normal
result = get_first_and_last([1, 2, 3, 4, 5])
assert result == (1, 5), f"Expected (1, 5), got {result}"
print(f"Normal case: {result}")
# Tester le cas limite : un seul élément
result = get_first_and_last([42])
assert result == (42, 42), f"Expected (42, 42), got {result}"
print(f"Single item: {result}")
# Tester le cas limite : deux éléments
result = get_first_and_last([10, 20])
assert result == (10, 20), f"Expected (10, 20), got {result}"
print(f"Two items: {result}")
# Tester le cas limite : séquence vide (devrait échouer)
try:
result = get_first_and_last([])
print("ERROR: Should have raised AssertionError for empty list")
except AssertionError as e:
print(f"Empty list correctly rejected: {e}")Output:
Normal case: (1, 5)
Single item: (42, 42)
Two items: (10, 20)
Empty list correctly rejected: Cannot get first and last from empty sequence41.5.4) Tester des transformations de données
Quand votre fonction transforme des données, vérifiez par assert que la transformation est correcte :
def remove_duplicates(items):
"""Remove duplicates while preserving order."""
seen = set()
result = []
for item in items:
if item not in seen:
seen.add(item)
result.append(item)
return result
# Tester une suppression de doublons de base
input_data = [1, 2, 2, 3, 1, 4, 3, 5]
result = remove_duplicates(input_data)
expected = [1, 2, 3, 4, 5]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 1 passed: {input_data} -> {result}")
# Tester que l'ordre est conservé
input_data = [3, 1, 2, 1, 3, 2]
result = remove_duplicates(input_data)
expected = [3, 1, 2]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 2 passed: {input_data} -> {result}")
# Tester sans doublons
input_data = [1, 2, 3, 4, 5]
result = remove_duplicates(input_data)
assert result == input_data, f"Expected {input_data}, got {result}"
print(f"Test 3 passed: {input_data} -> {result}")
# Tester avec uniquement des doublons
input_data = [5, 5, 5, 5]
result = remove_duplicates(input_data)
expected = [5]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 4 passed: {input_data} -> {result}")Output:
Test 1 passed: [1, 2, 2, 3, 1, 4, 3, 5] -> [1, 2, 3, 4, 5]
Test 2 passed: [3, 1, 2, 1, 3, 2] -> [3, 1, 2]
Test 3 passed: [1, 2, 3, 4, 5] -> [1, 2, 3, 4, 5]
Test 4 passed: [5, 5, 5, 5] -> [5]41.5.5) Créer une fonction de test simple
Au fur et à mesure que votre code grandit, disperser des instructions assert dans votre code principal devient désordonné et difficile à gérer. Une meilleure approche consiste à organiser vos tests dans des fonctions de test dédiées. Cela sépare le code de test du code de production et facilite l’exécution de tous vos tests d’un seul coup.
Pourquoi utiliser des fonctions de test dédiées ?
- Organisation : tous les tests pour une fonction sont au même endroit
- Réutilisabilité : exécutez les tests à chaque modification du code
- Documentation : les tests montrent comment la fonction doit se comporter
- Débogage : quand un test échoue, vous savez immédiatement quel scénario a cassé
- Workflow de développement : tester d’abord, puis implémenter ou corriger le code
Voyons cela en pratique :
def calculate_grade(score):
"""Convert numeric score to letter grade."""
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
def test_calculate_grade():
"""Test the calculate_grade function.
This function tests all expected behaviors:
- Each grade range (A, B, C, D, F)
- Boundary values (90, 80, 70, 60)
- Edge cases (just below each boundary)
"""
print("Testing calculate_grade...")
# Tester les notes A
assert calculate_grade(95) == 'A', "95 should be A"
assert calculate_grade(90) == 'A', "90 should be A (boundary)"
print(" ✓ A grades: passed")
# Tester les notes B
assert calculate_grade(85) == 'B', "85 should be B"
assert calculate_grade(80) == 'B', "80 should be B (boundary)"
print(" ✓ B grades: passed")
# Tester les notes C
assert calculate_grade(75) == 'C', "75 should be C"
assert calculate_grade(70) == 'C', "70 should be C (boundary)"
print(" ✓ C grades: passed")
# Tester les notes D
assert calculate_grade(65) == 'D', "65 should be D"
assert calculate_grade(60) == 'D', "60 should be D (boundary)"
print(" ✓ D grades: passed")
# Tester les notes F
assert calculate_grade(55) == 'F', "55 should be F"
assert calculate_grade(0) == 'F', "0 should be F"
print(" ✓ F grades: passed")
# Tester les cas limites (un en dessous de chaque seuil)
assert calculate_grade(89) == 'B', "89 should be B (just below A)"
assert calculate_grade(79) == 'C', "79 should be C (just below B)"
assert calculate_grade(69) == 'D', "69 should be D (just below C)"
assert calculate_grade(59) == 'F', "59 should be F (just below D)"
print(" ✓ Boundary cases: passed")
print("All tests passed! ✓\n")
# Exécuter les tests
test_calculate_grade()
# Maintenant, vous pouvez utiliser la fonction en toute confiance
student_score = 87
grade = calculate_grade(student_score)
print(f"Student score {student_score} = Grade {grade}")Output:
Testing calculate_grade...
✓ A grades: passed
✓ B grades: passed
✓ C grades: passed
✓ D grades: passed
✓ F grades: passed
✓ Boundary cases: passed
All tests passed! ✓
Student score 87 = Grade BAvantages de cette approche :
- Organisation claire des tests : vous pouvez voir tous les cas de test d’un coup d’œil
- Facile à exécuter : il suffit d’appeler
test_calculate_grade()chaque fois que vous modifiez la fonction - Retour progressif : vous voyez quels groupes de tests réussissent au fur et à mesure que la fonction s’exécute
- Auto-documenté : la fonction de test montre exactement comment
calculate_grade()doit fonctionner
Quand exécuter vos tests :
- Avant de faire des changements : assurez-vous que vos tests passent avec le code actuel
- Après avoir fait des changements : vérifiez que vous n’avez rien cassé
- Quand vous ajoutez des fonctionnalités : écrivez d’abord des tests pour la nouvelle fonctionnalité (développement piloté par les tests)
- Quand vous corrigez des bugs : ajoutez un test qui reproduit le bug, puis corrigez-le
Ce schéma simple — écrire des fonctions de test avec des assertions — est la base des tests logiciels professionnels. En progressant, vous découvrirez des frameworks de test comme pytest et unittest, mais l’idée centrale reste la même : écrire des fonctions qui vérifient que votre code fonctionne correctement.
41.5.6) Quand utiliser des assertions vs des exceptions
Comprendre quand utiliser des assertions plutôt que des exceptions est crucial. Elles servent des objectifs fondamentalement différents :
Les assertions servent à trouver des bugs pendant le développement :
- Elles vérifient des choses qui ne devraient jamais être fausses si votre code est correctement écrit
- Elles vérifient les hypothèses internes et la logique de votre propre code
- Elles vous aident à détecter des erreurs de programmation pendant que vous écrivez et testez du code
- Exemple : « À ce stade de ma fonction, cette liste ne devrait jamais être vide »
- Exemple : « Tous les éléments de cette liste devraient être des entiers parce que je viens de les filtrer »
Les exceptions servent à gérer des erreurs qui peuvent se produire en fonctionnement normal :
- Elles traitent des conditions externes que vous ne pouvez pas contrôler
- Elles gèrent des situations qui peuvent survenir même lorsque votre code est parfait
- Elles permettent à votre programme de se rétablir proprement ou d’échouer de manière informative
- Exemple : l’utilisateur saisit du texte alors que vous attendiez un nombre
- Exemple : un fichier que votre code tente d’ouvrir n’existe pas
- Exemple : une requête réseau dépasse le délai
La différence clé : les assertions disent « cela devrait être impossible », tandis que les exceptions disent « cela peut arriver, et voici comment nous allons le gérer ».
Voyons cela en pratique :
# Exemple 1 : fonction utilisée avec une SAISIE UTILISATEUR
# Les utilisateurs peuvent saisir n'importe quoi, y compris 0
def calculate_user_ratio(numerator, denominator):
"""Calculate ratio from user-provided numbers."""
# L'utilisateur peut saisir 0, donc utilisez la gestion d'exception
if denominator == 0:
raise ValueError("Denominator cannot be zero")
return numerator / denominator
# Exemple 2 : calcul interne où 0 devrait être impossible
def calculate_percentage(part, total):
"""Calculate what percentage 'part' is of 'total'."""
# Ceci est appelé en interne après avoir vérifié total > 0
# Si total vaut 0, c'est un bug de programmation dans notre code
assert total > 0, "total must be positive - check calling code"
return (part / total) * 100Plus d’exemples de ce que chacun doit gérer :
| Situation | Utiliser une assertion | Utiliser une exception |
|---|---|---|
| L’utilisateur saisit une entrée invalide | ❌ Non | ✅ Oui |
| Le fichier n’existe pas | ❌ Non | ✅ Oui |
| Une requête réseau échoue | ❌ Non | ✅ Oui |
| La fonction reçoit un mauvais type de paramètre depuis votre code | ✅ Oui | ❌ No |
| Une liste devrait contenir des éléments mais est vide à cause d’une erreur de logique | ✅ Oui | ❌ No |
| Une structure de données est dans un état inattendu à cause d’un bug | ✅ Oui | ❌ No |
| La connexion à la base de données échoue | ❌ Non | ✅ Oui |
| L’API renvoie un format inattendu | ❌ Non | ✅ Oui |
| Votre algorithme produit un résultat mathématiquement impossible | ✅ Oui | ❌ No |
Limitation critique des assertions :
Les assertions peuvent être totalement désactivées lorsque Python s’exécute avec l’optimisation :
python -O script.py # All assert statements are ignored!Quand les assertions sont désactivées, elles disparaissent tout simplement — Python ne les vérifie pas du tout. Cela signifie :
- ❌ N’utilisez jamais les assertions pour valider une saisie utilisateur
- ❌ N’utilisez jamais les assertions pour des contrôles de sécurité
- ❌ N’utilisez jamais les assertions pour quoi que ce soit qui doit toujours fonctionner en production
# DANGEREUX - NE FAITES PAS ÇA :
def process_payment(amount):
assert amount > 0, "Amount must be positive" # WRONG! Gets disabled with -O
# Process payment...
# CORRECT - FAITES ÇA :
def process_payment(amount):
if amount <= 0:
raise ValueError("Amount must be positive") # Always checked!
# Process payment...En résumé :
-
Assertions = « Je vérifie mon propre code pour détecter des bugs pendant le développement »
- Pensez : « Cela devrait être impossible si j’ai codé correctement »
- Elles vous aident à trouver des erreurs dans votre logique
-
Exceptions = « Je gère des conditions du monde réel qui peuvent réellement se produire »
- Pensez : « Cela peut arriver en utilisation normale, et je dois le gérer »
- Elles aident votre programme à gérer des situations imprévisibles
Les assertions sont des outils de développement et de débogage qui vous aident à écrire du code correct. Les exceptions sont des outils de production qui aident votre programme à gérer la réalité compliquée des saisies utilisateur, des systèmes de fichiers, des réseaux, et d’autres facteurs externes que vous ne pouvez pas contrôler.
Vous avez maintenant appris les techniques essentielles de débogage et de test qui vous serviront tout au long de votre parcours de programmation :
- Lire les tracebacks pour localiser rapidement où les erreurs se produisent
- Tracer le code mentalement pour comprendre ce que fait votre code étape par étape
- Utiliser des instructions print de manière stratégique pour voir les valeurs à l’exécution et le flux
- Inspecter des objets avec
type()etdir()pour comprendre ce avec quoi vous travaillez - Tester avec des assertions pour vérifier que votre code fonctionne et détecter les bugs tôt
Ces compétences fonctionnent ensemble comme une boîte à outils complète de débogage. Quand vous rencontrez un problème :
- Lisez le traceback pour trouver où ça a échoué
- Utilisez le débogage par print ou le traçage mental pour comprendre pourquoi
- Utilisez l’inspection type/dir quand vous n’êtes pas sûr de ce qu’un objet peut faire
- Écrivez des assertions pour éviter que le bug ne revienne
Avec de la pratique, vous développerez une intuition sur la technique à utiliser dans chaque situation. Rappelez-vous : chaque programmeur débogue du code — la différence, c’est que les programmeurs expérimentés le font de manière systématique et efficace.