Python & AI Tutorials Logo
Programmation Python

26. Techniques de programmation défensive avec les exceptions et la validation

La programmation défensive (defensive programming) consiste à écrire du code qui anticipe les problèmes avant qu’ils ne se produisent. Au lieu de supposer que tout fonctionnera parfaitement, le code défensif valide les entrées, gère les erreurs de manière élégante et vérifie les hypothèses. Cette approche permet de créer des programmes plus fiables, plus faciles à déboguer et moins susceptibles de planter de manière inattendue.

Dans les chapitres précédents, nous avons appris à gérer les exceptions lorsqu’elles se produisent. Maintenant, nous allons apprendre à éviter que de nombreuses erreurs ne se produisent en premier lieu, et à détecter les problèmes tôt lorsqu’ils se produisent.

26.1) Valider les arguments des fonctions

Les fonctions reçoivent souvent des données provenant d’autres parties de votre programme ou des utilisateurs. Si une fonction reçoit des données invalides, elle peut produire des résultats incorrects, planter avec une erreur déroutante, ou provoquer des problèmes ailleurs dans votre programme. La validation des arguments (argument validation) consiste à vérifier que les arguments d’une fonction respectent vos exigences avant de les utiliser.

26.1.1) Pourquoi valider les arguments ?

Considérez cette fonction qui calcule le pourcentage de note d’un étudiant :

python
def calculate_percentage(points_earned, total_points):
    return (points_earned / total_points) * 100
 
# Utilisation de la fonction
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%")  # Output: Grade: 85.0%

Cela fonctionne très bien avec des entrées valides. Mais que se passe-t-il avec des données problématiques ?

python
# Problème 1 : division par zéro
percentage = calculate_percentage(85, 0)  # ZeroDivisionError!
 
# Problème 2 : valeurs négatives (ça n’a pas de sens)
percentage = calculate_percentage(-10, 100)  # -10.0%
 
# Problème 3 : points obtenus supérieurs au total (impossible)
percentage = calculate_percentage(120, 100)  # 120.0%

Sans validation, la fonction plante ou produit des résultats absurdes. Les messages d’erreur n’expliquent pas ce qui s’est mal passé du point de vue de la logique métier—ils ne montrent que des échecs techniques.

26.1.2) Validation de base des arguments avec des conditions

L’approche de validation la plus simple utilise des instructions if pour vérifier les arguments et lever des exceptions lorsqu’ils sont invalides :

python
def calculate_percentage(points_earned, total_points):
    # Valider total_points
    if total_points <= 0:
        raise ValueError("total_points must be positive")
    
    # Valider points_earned
    if points_earned < 0:
        raise ValueError("points_earned cannot be negative")
    
    if points_earned > total_points:
        raise ValueError("points_earned cannot exceed total_points")
    
    # Toutes les validations ont réussi - on peut calculer en toute sécurité
    return (points_earned / total_points) * 100
 
# Utilisation valide
percentage = calculate_percentage(85, 100)
print(f"Grade: {percentage}%")  # Output: Grade: 85.0%
 
# Utilisation invalide - messages d'erreur clairs
try:
    percentage = calculate_percentage(85, 0)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: total_points must be positive
 
try:
    percentage = calculate_percentage(-10, 100)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: points_earned cannot be negative
 
try:
    percentage = calculate_percentage(120, 100)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: points_earned cannot exceed total_points

Désormais, quand quelque chose se passe mal, le message d’erreur explique clairement quel est le problème et comment le corriger.

26.1.3) Valider les types des arguments

Parfois, vous devez vous assurer que les arguments sont du bon type :

python
def calculate_discount(price, discount_percent):
    # Valider les types
    if not isinstance(price, (int, float)):
        raise TypeError("price must be a number")
    
    if not isinstance(discount_percent, (int, float)):
        raise TypeError("discount_percent must be a number")
    
    # Valider les valeurs
    if price < 0:
        raise ValueError("price cannot be negative")
    
    if not (0 <= discount_percent <= 100):
        raise ValueError("discount_percent must be between 0 and 100")
    
    # Calculer la remise
    discount_amount = price * (discount_percent / 100)
    return price - discount_amount
 
# Utilisation valide
final_price = calculate_discount(50.00, 20)
print(f"Final price: ${final_price:.2f}")  # Output: Final price: $40.00
 
# Erreur de type
try:
    final_price = calculate_discount("50", 20)
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: price must be a number
 
# Erreur de valeur
try:
    final_price = calculate_discount(50.00, 150)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: discount_percent must be between 0 and 100

La fonction isinstance() vérifie si un objet est une instance d’un type (ou de types) spécifié(s). Nous passons un tuple (int, float) pour accepter soit des entiers soit des flottants, puisque ce sont tous deux des types numériques valides pour des prix.

Quand valider les types : la philosophie de Python est le « duck typing »—si un objet se comporte comme ce dont vous avez besoin, utilisez-le. La validation de type est la plus utile lorsque :

  • Vous écrivez une fonction qui sera utilisée par d’autres
  • Des erreurs de type provoqueraient des échecs déroutants plus tard
  • La fonction fait partie d’une API publique ou d’une bibliothèque

26.1.4) Valider les arguments de type collection

Lorsque des fonctions acceptent des listes, des dictionnaires, ou d’autres collections, validez à la fois la collection et son contenu :

python
def calculate_average_grade(grades):
    # Valider la collection elle-même
    if not isinstance(grades, list):
        raise TypeError("grades must be a list")
    
    if len(grades) == 0:
        raise ValueError("grades list cannot be empty")
    
    # Valider chaque note dans la collection
    for i, grade in enumerate(grades):
        if not isinstance(grade, (int, float)):
            raise TypeError(f"grade at index {i} must be a number, got {type(grade).__name__}")
        
        if not (0 <= grade <= 100):
            raise ValueError(f"grade at index {i} must be between 0 and 100, got {grade}")
    
    # Toutes les validations ont réussi
    return sum(grades) / len(grades)
 
# Utilisation valide
grades = [85, 92, 78, 95]
average = calculate_average_grade(grades)
print(f"Average: {average:.1f}")  # Output: Average: 87.5
 
# Erreur : liste vide
try:
    average = calculate_average_grade([])
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: grades list cannot be empty
 
# Type de note invalide
try:
    average = calculate_average_grade([85, "92", 78])
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: grade at index 1 must be a number, got str
 
# Valeur de note invalide
try:
    average = calculate_average_grade([85, 92, 150])
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: grade at index 2 must be between 0 and 100, got 150

Remarquez comment nous incluons l’index dans les messages d’erreur lors de la validation des éléments d’une collection. Cela aide à identifier précisément quel élément pose problème, en particulier dans de grandes collections.

Type invalide

Valeur invalide

Valide

Fonction appelée

Valider
les arguments

Lever TypeError

Lever ValueError

Exécuter la logique de la fonction

Retourner le résultat

L'appelant gère l'exception

26.2) Vérifier la validité des entrées utilisateur

Les entrées utilisateur sont intrinsèquement peu fiables—les utilisateurs font des fautes de frappe, comprennent mal les instructions, ou saisissent des données dans des formats inattendus. Valider les entrées utilisateur empêche ces erreurs de provoquer des plantages du programme ou des résultats incorrects.

26.2.1) Modèle de base pour la validation des entrées

Le modèle fondamental de validation d’entrée combine input() avec des contrôles de validation :

python
# Obtenir l’entrée utilisateur
age_str = input("Enter your age: ")
 
# Valider l’entrée
try:
    age = int(age_str)
    if age < 0:
        print("Error: Age cannot be negative")
    elif age > 150:
        print("Error: Age seems unrealistic")
    else:
        print(f"You are {age} years old")
except ValueError:
    print("Error: Please enter a valid number")

Ce modèle comporte trois parties :

  1. Obtenir l’entrée sous forme de chaîne
  2. Essayer de la convertir dans le type nécessaire
  3. Vérifier si la valeur convertie est valide

Voyons cela en action avec différentes entrées :

python
# Entrée valide
# User enters: 25
# Output: You are 25 years old
 
# Type invalide
# User enters: twenty-five
# Output: Error: Please enter a valid number
 
# Valeur invalide (négative)
# User enters: -5
# Output: Error: Age cannot be negative
 
# Valeur invalide (irréaliste)
# User enters: 200
# Output: Error: Age seems unrealistic

26.2.2) Valider des plages et des formats d’entrée

Certaines entrées doivent se situer dans des plages spécifiques ou correspondre à des formats particuliers :

python
# Validation d’un mois (1-12)
month_str = input("Enter month (1-12): ")
try:
    month = int(month_str)
    if not (1 <= month <= 12):
        print("Error: Month must be between 1 and 12")
    else:
        print(f"Month: {month}")
except ValueError:
    print("Error: Please enter a whole number")
 
# Validation du format d’un email (contrôle simple)
email = input("Enter email: ")
if '@' not in email or '.' not in email:
    print("Error: Email must contain @ and .")
else:
    print(f"Email: {email}")
 
# Validation d’une entrée oui/non
response = input("Continue? (yes/no): ").lower().strip()
if response not in ['yes', 'no', 'y', 'n']:
    print("Error: Please answer yes or no")
else:
    if response in ['yes', 'y']:
        print("Continuing...")
    else:
        print("Stopping...")

La validation des e-mails ici est volontairement simple—elle vérifie seulement une structure de base. La validation réelle des e-mails est bien plus complexe et utilise généralement des expressions régulières (que nous apprendrons au Chapitre 39).

26.2.3) Fournir des messages d’erreur utiles

De bons messages d’erreur indiquent aux utilisateurs exactement ce qui s’est mal passé et comment le corriger :

python
# Mauvais message d’erreur
password = input("Enter password: ")
if len(password) < 8:
    print("Error: Invalid password")  # Pas utile !
 
# Meilleur message d’erreur
password = input("Enter password: ")
if len(password) < 8:
    print("Error: Password must be at least 8 characters long")
    print(f"Your password is only {len(password)} characters")
 
# Encore mieux - expliquer toutes les exigences dès le départ
print("Password requirements:")
print("- At least 8 characters")
print("- Must contain at least one number")
password = input("Enter password: ")
 
# Vérifier la longueur
if len(password) < 8:
    print(f"Error: Password too short ({len(password)} characters)")
    print("Password must be at least 8 characters")
# Vérifier la présence d’un chiffre
elif not any(char.isdigit() for char in password):
    print("Error: Password must contain at least one number")
else:
    print("Password accepted")

La fonction any() renvoie True si au moins un élément d’un itérable est vrai. Ici, char.isdigit() vérifie si chaque caractère est un chiffre, et any() nous indique si au moins un caractère a réussi le test.

Échec de conversion

Conversion réussie

Hors plage

Format invalide

Valide

Obtenir l'entrée utilisateur

Essayer la conversion de type

ValueError:
Format invalide

Vérifier les
contraintes de valeur

Erreur de valeur :
Message clair

Erreur de format :
Message clair

Utiliser l'entrée

Afficher l'erreur,
expliquer le format attendu

26.3) Combiner input(), les boucles et try/except pour une gestion robuste des entrées

Les vérifications de validation isolées sont utiles, mais elles ne suffisent pas à gérer des erreurs utilisateur répétées. Si un utilisateur saisit des données invalides, votre programme doit lui donner une autre chance. Combiner des boucles avec la validation crée une gestion robuste des entrées qui continue de demander jusqu’à obtenir des données valides.

26.3.1) Le modèle de boucle d’entrée de base

Le modèle fondamental utilise une boucle while qui continue jusqu’à ce qu’une entrée valide soit reçue :

python
# Continuer à demander jusqu’à obtenir un âge valide
while True:
    age_str = input("Enter your age: ")
    try:
        age = int(age_str)
        if age < 0:
            print("Error: Age cannot be negative. Please try again.")
        elif age > 150:
            print("Error: Age seems unrealistic. Please try again.")
        else:
            # Entrée valide - sortir de la boucle
            break
    except ValueError:
        print("Error: Please enter a valid number.")
 
print(f"You are {age} years old")

Ce modèle comporte plusieurs éléments clés :

  • while True: crée une boucle infinie
  • La validation se fait à l’intérieur de la boucle
  • break sort de la boucle lorsque l’entrée est valide
  • Les messages d’erreur encouragent l’utilisateur à réessayer

Voyons comment cela gère diverses entrées :

python
# Exemple d’interaction :
# Enter your age: twenty
# Error: Please enter a valid number.
# Enter your age: -5
# Error: Age cannot be negative. Please try again.
# Enter your age: 25
# You are 25 years old

26.3.2) Créer des fonctions d’entrée réutilisables

Quand vous avez besoin du même type d’entrée validée à plusieurs endroits, créez une fonction :

python
def get_positive_integer(prompt):
    """Continuer à demander jusqu’à ce que l’utilisateur saisisse un entier positif."""
    while True:
        try:
            value = int(input(prompt))
            if value <= 0:
                print("Error: Please enter a positive number.")
            else:
                return value
        except ValueError:
            print("Error: Please enter a valid whole number.")
 
def get_number_in_range(prompt, min_value, max_value):
    """Continuer à demander jusqu’à ce que l’utilisateur saisisse un nombre dans l’intervalle spécifié."""
    while True:
        try:
            value = float(input(prompt))
            if value < min_value or value > max_value:
                print(f"Error: Please enter a number between {min_value} and {max_value}.")
            else:
                return value
        except ValueError:
            print("Error: Please enter a valid number.")
 
# Utilisation des fonctions
quantity = get_positive_integer("Enter quantity: ")
print(f"Quantity: {quantity}")
 
grade = get_number_in_range("Enter grade (0-100): ", 0, 100)
print(f"Grade: {grade}")
 
temperature = get_number_in_range("Enter temperature (-50 to 50): ", -50, 50)
print(f"Temperature: {temperature}°C")

Ces fonctions encapsulent la logique de validation, ce qui rend votre code principal plus propre et plus lisible. Elles garantissent également un comportement de validation cohérent dans l’ensemble de votre programme.

26.4) Utiliser les assertions pour des vérifications d’invariants en phase de développement

Les assertions sont un type particulier de vérification utilisé pendant le développement pour s’assurer que les hypothèses de votre code sont correctes. Contrairement à la validation (qui gère les erreurs attendues provenant des utilisateurs ou de données externes), les assertions détectent les erreurs de programmation—des situations qui ne devraient jamais se produire si votre code est correct.

26.4.1) Ce que sont les assertions et quand les utiliser

Une assertion est une instruction qui doit toujours être vraie à un moment donné de votre code. Si elle est fausse, quelque chose ne va fondamentalement pas dans la logique de votre programme :

python
def calculate_average(numbers):
    # Cela ne devrait jamais arriver si la fonction est appelée correctement
    assert len(numbers) > 0, "numbers list cannot be empty"
    
    return sum(numbers) / len(numbers)
 
# Utilisation correcte
grades = [85, 90, 78]
average = calculate_average(grades)
print(f"Average: {average:.1f}")  # Output: Average: 84.3
 
# Utilisation incorrecte - déclenche l’assertion
empty_list = []
average = calculate_average(empty_list)  # AssertionError: numbers list cannot be empty

Quand une assertion échoue, Python lève une AssertionError avec votre message. Cela arrête immédiatement le programme et vous montre exactement où votre hypothèse a été violée.

Distinction clé :

  • Validation (avec if et raise) : pour gérer des problèmes attendus provenant des utilisateurs ou de données externes
  • Assertions : pour détecter des bugs de programmation pendant le développement
python
# Validation - gère les erreurs utilisateur attendues
def get_positive_number(prompt):
    while True:
        try:
            value = float(input(prompt))
            if value <= 0:
                print("Error: Please enter a positive number.")
            else:
                return value
        except ValueError:
            print("Error: Please enter a valid number.")
 
# Assertion - détecte les erreurs de programmation
def calculate_discount(price, discount_rate):
    # Elles ne devraient jamais être violées si le programme est écrit correctement
    assert price >= 0, "price should be non-negative"
    assert 0 <= discount_rate <= 1, "discount_rate should be between 0 and 1"
    
    return price * (1 - discount_rate)

26.4.2) Vérifier les préconditions d’une fonction

Les assertions sont excellentes pour vérifier que les préconditions (preconditions) (exigences qui doivent être vraies avant l’exécution de la fonction) sont respectées :

python
def get_list_element(items, index):
    """Obtenir un élément d’une liste à l’index spécifié."""
    # Préconditions
    assert isinstance(items, list), "items must be a list"
    assert isinstance(index, int), "index must be an integer"
    assert 0 <= index < len(items), f"index {index} out of range for list of length {len(items)}"
    
    return items[index]
 
# Utilisation correcte
numbers = [10, 20, 30, 40]
value = get_list_element(numbers, 2)
print(f"Value: {value}")  # Output: Value: 30
 
# Erreur de programmation - mauvais type
value = get_list_element("not a list", 0)  # AssertionError: items must be a list
 
# Erreur de programmation - index invalide
value = get_list_element(numbers, 10)  # AssertionError: index 10 out of range for list of length 4

Ces assertions aident à détecter des bugs pendant le développement. Si vous passez accidentellement le mauvais type ou un index invalide, l’assertion vous indique immédiatement ce qui s’est mal passé.

26.4.3) Vérifier les postconditions d’une fonction

Les postconditions (postconditions) sont des conditions qui doivent être vraies après l’exécution d’une fonction. Les assertions peuvent vérifier que votre fonction a produit des résultats valides :

python
def calculate_percentage(part, whole):
    """Calcule le pourcentage que représente 'part' par rapport à 'whole'."""
    # Préconditions
    assert whole > 0, "whole must be positive"
    assert part >= 0, "part must be non-negative"
    
    # Calculer le pourcentage
    percentage = (part / whole) * 100
    
    # Postcondition - le résultat doit être un pourcentage valide
    assert 0 <= percentage <= 100, f"percentage {percentage} is outside valid range"
    
    return percentage
 
# Cela fonctionne correctement
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")  # Output: Percentage: 25.0%
 
# Cela révèle une erreur de logique dans notre fonction
# (nous n’avons pas vérifié que part <= whole)
percentage = calculate_percentage(150, 100)  # AssertionError: percentage 150.0 is outside valid range

L’assertion de postcondition a détecté un bug dans notre fonction—nous avons oublié de valider que part ne dépasse pas whole. C’est exactement à cela que servent les assertions : détecter des erreurs de programmation.

26.4.4) Les assertions peuvent être désactivées

Une caractéristique importante des assertions est qu’elles peuvent être désactivées lorsqu’on exécute Python avec l’option -O (optimize) :

python
# Ce fichier s’appelle test_assertions.py
def divide(a, b):
    assert b != 0, "divisor cannot be zero"
    return a / b
 
result = divide(10, 2)
print(f"Result: {result}")
 
result = divide(10, 0)  # AssertionError when assertions are enabled

Exécution normale :

bash
python test_assertions.py
# Output: Result: 5.0
# Then: AssertionError: divisor cannot be zero

Exécution avec optimisation :

bash
python -O test_assertions.py
# Output: Result: 5.0
# Then: ZeroDivisionError: division by zero

C’est pourquoi les assertions ne doivent jamais être utilisées pour valider des données externes—si quelqu’un exécute votre programme avec -O, toutes les assertions sont ignorées. Utilisez les assertions uniquement pour détecter des bugs de programmation pendant le développement et les tests.

Condition vraie

Condition fausse

Exécution du code

Vérification d’assertion

Continuer l’exécution

Lever AssertionError
avec message

Le programme s’arrête
affiche la trace

Le développeur corrige le bug

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