Python & AI Tutorials Logo
Programmation Python

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 :

python
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 zero

Décomposons ce que ce traceback nous dit :

Ligne 12 : process_student_grades appelé

Ligne 7 : calculate_average appelé

Ligne 4 : opération de division

ZeroDivisionError : division par zéro

Lecture de bas en haut :

  1. Le type d’erreur et le message (en bas) : ZeroDivisionError: division by zero nous indique exactement ce qui s’est mal passé
  2. La ligne exacte où l’erreur s’est produite : return total / count à la ligne 4
  3. 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 :

python
# 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 fonction

La 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 :

python
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.2

Points 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 :

python
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: 9

Voici 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: 9

41.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 :

python
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érationnumrunning_sum (avant)running_sum (après)totals
Début-00[]
110010[10]
2201030[10, 30]
3303060[10, 30, 60]
44060100[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 :

python
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 :

  1. Vérifier 85 >= 90 → False, ignorer le premier bloc
  2. Vérifier 85 >= 80 → True, entrer dans le second bloc
  3. Définir category = "Good" et bonus = 5
  4. Ignorer les blocs elif restants et else (une correspondance a déjà été trouvée)
  5. Calculer final_score = 85 + 5 = 90
  6. 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 :

python
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.00

Trace 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.0

Cette 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 :

python
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.53199999999999

Ces 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 :

python
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.894626777442

41.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 :

python
# 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 :

python
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 cleaned

Si 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 :

python
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

python
# Facile à trouver et à supprimer avec rechercher/remplacer
print(f"DEBUG: {total=}")
print(f"DEBUG: {items=}")

Stratégie 2 : Utiliser un indicateur de débogage

python
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 = False

Stratégie 3 : Les commenter mais les garder

python
def process_data(data):
    # print(f"DEBUG: {data=}")  # Utile pour un futur débogage
    result = transform(data)
    # print(f"DEBUG: {result=}")
    return result

Pour 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 :

python
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: None

41.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 :

python
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 :

python
# 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 more

Vous 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 :

python
# 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.

python
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: Passing

La 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 :

python
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: Passing

Cette 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 :

python
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 :

python
# 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 clair

Voyons maintenant les assertions dans une fonction réelle :

python
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 100

41.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 :

python
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.0

Si 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 :

python
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 sequence

41.5.4) Tester des transformations de données

Quand votre fonction transforme des données, vérifiez par assert que la transformation est correcte :

python
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 :

python
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 B

Avantages de cette approche :

  1. Organisation claire des tests : vous pouvez voir tous les cas de test d’un coup d’œil
  2. Facile à exécuter : il suffit d’appeler test_calculate_grade() chaque fois que vous modifiez la fonction
  3. Retour progressif : vous voyez quels groupes de tests réussissent au fur et à mesure que la fonction s’exécute
  4. 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 :

python
# 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) * 100

Plus d’exemples de ce que chacun doit gérer :

SituationUtiliser une assertionUtiliser 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 :

bash
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
python
# 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() et dir() 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 :

  1. Lisez le traceback pour trouver où ça a échoué
  2. Utilisez le débogage par print ou le traçage mental pour comprendre pourquoi
  3. Utilisez l’inspection type/dir quand vous n’êtes pas sûr de ce qu’un objet peut faire
  4. É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.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai