Python & AI Tutorials Logo
Programmation Python

40. Écrire du code propre et lisible

Tout au long de ce livre, vous avez appris la syntaxe de Python, les structures de données, le flux de contrôle, les fonctions, les classes, et de nombreux autres concepts de programmation. Vous pouvez désormais écrire des programmes qui fonctionnent. Mais il y a une différence cruciale entre du code qui fonctionne et du code qui est maintenable — du code que vous et d’autres pouvez comprendre, modifier et déboguer des mois ou des années plus tard.

Ce chapitre se concentre sur l’écriture de code propre et lisible. Vous apprendrez les conventions et les pratiques qui rendent le code Python professionnel et maintenable. Ce ne sont pas de simples règles arbitraires — ce sont des recommandations éprouvées sur le terrain qui facilitent la collaboration, réduisent les bugs et vous aident à comprendre votre propre code lorsque vous y revenez plus tard.

40.1) Pourquoi le style compte : lire vs. écrire du code

40.1.1) Le code est lu plus souvent qu’il n’est écrit

Lorsque vous écrivez du code, vous passez des minutes ou des heures à le créer. Mais ce code sera lu de nombreuses fois : quand vous le déboguez, quand vous ajoutez des fonctionnalités, quand d’autres développeurs travaillent avec, et quand vous y revenez des mois plus tard en essayant de vous souvenir de ce qu’il fait.

Considérez ce code fonctionnel mais mal stylé :

python
# ATTENTION : Mauvais style - uniquement pour démonstration
def c(l):
    t=0
    for i in l:
        t=t+i
    return t/len(l)
 
data=[85,92,78,90,88]
result=c(data)
print(result)  # Output: 86.6

Ce code fonctionne parfaitement. Il calcule la moyenne d’une liste de nombres. Mais comprendre ce qu’il fait demande une analyse attentive. Comparez maintenant avec cette version :

python
def calculate_average(numbers):
    """Calculate the arithmetic mean of a list of numbers."""
    total = 0
    for number in numbers:
        total = total + number
    return total / len(numbers)
 
test_scores = [85, 92, 78, 90, 88]
average_score = calculate_average(test_scores)
print(average_score)  # Output: 86.6

Qu’est-ce qui rend la deuxième version meilleure ?

  • Le nom de fonction (calculate_average) indique clairement l’objectif
  • Les noms de variables (numbers, total, test_scores) sont descriptifs
  • La docstring explique ce que fait la fonction
  • Un espacement approprié rend la structure claire
  • N’importe qui peut comprendre ce code sans l’étudier

Les deux versions produisent des résultats identiques, mais la seconde version est immédiatement compréhensible.

L’idée clé : vous écrivez du code une fois, mais vous le lisez des dizaines ou des centaines de fois. Investir quelques secondes supplémentaires dans un nommage clair et une mise en forme soignée économise des heures de confusion plus tard.

40.1.2) La lisibilité réduit les bugs

Un code clair est plus facile à déboguer parce que vous pouvez rapidement comprendre ce que fait chaque partie. Quand les noms de variables sont descriptifs et que la structure est propre, vous pouvez repérer plus facilement les erreurs de logique.

python
# Difficile à déboguer - que représentent ces variables ?
# ATTENTION : Mauvais style - uniquement pour démonstration
def process(x, y):
    if x > y:
        return x * (1 - y)
    return x
 
result = process(100, 0.1)
python
# Facile à déboguer - on voit clairement ce qui se passe
def apply_discount(price, discount_rate):
    """Calculate price after applying discount rate (0.0 to 1.0)."""
    discount_amount = price * discount_rate
    final_price = price - discount_amount
    return final_price
 
original_price = 100
discount = 0.1  # remise de 10 %
final_price = apply_discount(original_price, discount)
print(f"Final price: ${final_price}")
# Output: Final price: $90.0

Dans la deuxième version, vous voyez immédiatement la logique : « Nous calculons un montant de remise, puis nous le soustrayons du prix. » Dans la première version, vous devez suivre mentalement ce que représentent x et y et comprendre ce que signifie x * (1 - y).

40.1.3) La cohérence facilite la collaboration

Quand tout le monde dans une équipe suit les mêmes conventions de style, le code devient prévisible. Vous ne gaspillez pas d’énergie mentale à déchiffrer différents styles de mise en forme — vous pouvez vous concentrer sur la compréhension de la logique.

Python a un guide de style officiel appelé PEP 8 (Python Enhancement Proposal 8). PEP 8 définit des conventions pour :

  • Comment nommer les variables, les fonctions et les classes
  • Comment formater le code (espacement, longueur de ligne, indentation)
  • Quand utiliser des commentaires et des docstrings
  • Comment organiser les imports

Suivre PEP 8 signifie que votre code aura un aspect familier pour d’autres programmeurs Python, rendant la collaboration plus fluide. Nous couvrirons les recommandations essentielles de PEP 8 dans les sections suivantes.

Oui

Non

Écrire du code

Suivre un guide de style ?

Code cohérent, lisible

Code incohérent

Facile à comprendre

Facile à maintenir

Facile à collaborer

Charge mentale

Confusion

Bugs

40.2) Conventions de nommage : variables, fonctions et classes (PEP 8)

40.2.1) Principes généraux de nommage

Les bons noms sont descriptifs et sans ambiguïté. Ils doivent vous dire ce que quelque chose représente ou fait sans exiger que vous lisiez l’implémentation.

Principes clés :

  • Utilisez des mots complets, pas des abréviations (sauf pour celles très courantes comme id, url, html)
  • Soyez spécifique : user_count est mieux que count, calculate_total_price est mieux que calculate
  • Évitez les noms à une seule lettre sauf pour des boucles très courtes ou des formules mathématiques
  • N’incluez pas d’information de type dans les noms (Python est typé dynamiquement)
python
# Mauvais noms - on ne sait pas clairement ce qu’ils représentent
# ATTENTION : Mauvais style - uniquement pour démonstration
# Qu’est-ce que 'n' ? Un nombre ? Un nom ? Un nœud ?
# Qu’est-ce que 'd' ? Une date ? Une distance ? Une durée ?
# Qu’est-ce que 'l' ? On dirait le chiffre 1 !
n = "Alice"
d = 25
l = [1, 2, 3]
calc = lambda x: x * 2
 
# Bons noms - clairs et descriptifs
student_name = "Alice"
age_in_years = 25
test_scores = [1, 2, 3]
double_value = lambda x: x * 2

Exception : variables de boucle courtes

python
# Acceptable : très court, contexte clair
for i in range(10):
    print(i)
 
for x, y in coordinates:
    distance = (x**2 + y**2) ** 0.5
 
# Mais préférez des noms descriptifs pour plus de clarté
for student_index in range(len(students)):
    print(students[student_index])
 
for point_x, point_y in coordinates:
    distance = (point_x**2 + point_y**2) ** 0.5

40.2.2) Noms de variables et de fonctions : snake_case

En Python, les variables et les fonctions utilisent snake_case : tout en minuscules, avec des mots séparés par des underscores.

python
# Variables
user_name = "Bob"
total_price = 99.99
is_valid = True
max_retry_count = 3
 
# Fonctions
def calculate_tax(amount, rate):
    """Calculate tax on a given amount."""
    return amount * rate
 
def send_email_notification(recipient, message):
    """Send an email to the specified recipient."""
    print(f"Sending to {recipient}: {message}")
 
# Utilisation des fonctions
tax_amount = calculate_tax(100, 0.08)
send_email_notification("user@example.com", "Welcome!")

Pourquoi snake_case ? C’est très lisible. Les underscores créent des limites de mots claires, rendant les noms faciles à parcourir. Comparez calculatetotalprice (difficile à lire) avec calculate_total_price (immédiatement clair).

40.2.3) Noms de constantes : UPPER_SNAKE_CASE

Les constantes — des valeurs qui ne devraient pas changer pendant l’exécution du programme — utilisent UPPER_SNAKE_CASE : tout en majuscules avec des underscores.

python
# Constantes au niveau du module
MAX_LOGIN_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
PI = 3.14159
DATABASE_URL = "postgresql://localhost/mydb"
 
def validate_password_length(password):
    """Check if password meets minimum length requirement."""
    MIN_PASSWORD_LENGTH = 8  # Constante dans la fonction
    return len(password) >= MIN_PASSWORD_LENGTH
 
# Utilisation des constantes
if login_attempts > MAX_LOGIN_ATTEMPTS:
    print("Account locked")

Important : Python n’a pas de syntaxe intégrée pour les constantes. Contrairement à certains langages (comme const en JavaScript ou final en Java), Python n’a pas de moyen de déclarer qu’une variable ne peut pas être modifiée.

À la place, les programmeurs Python utilisent une convention de nommage pour signaler l’intention :

  • UPPER_SNAKE_CASE signifie : « J’ai l’intention que ce soit une constante — ne la modifiez pas »
  • C’est un outil de communication entre programmeurs, pas une fonctionnalité du langage
python
# Python n’a pas de syntaxe de constante - ce n’est qu’une variable ordinaire
MAX_LOGIN_ATTEMPTS = 3
 
# Python ne vous empêchera pas de la modifier
MAX_LOGIN_ATTEMPTS = 5  # ❌ Fonctionne techniquement, mais viole la convention
 
# La convention de nommage est un signal d’INTENTION :
# "Je l’ai nommée en MAJUSCULES pour montrer que je ne veux pas qu’elle change"

Bonne pratique : si une valeur doit réellement changer pendant l’exécution du programme, ne la nommez pas comme une constante :

python
# Cette valeur va changer - utilisez des minuscules
max_login_attempts = 3
max_login_attempts = 5  # ✅ OK - le nom indique qu’elle peut changer
 
# Cette valeur ne devrait jamais changer - utilisez des MAJUSCULES
MAX_LOGIN_ATTEMPTS = 3
# Ne la réassignez pas plus tard dans le code

La convention aide les programmeurs à comprendre votre intention et à éviter des bugs. Quand vous voyez MAX_LOGIN_ATTEMPTS, vous savez qu’il ne faut pas le modifier.

40.2.4) Noms de classes : PascalCase

Les noms de classes utilisent PascalCase (aussi appelé CapWords) : chaque mot commence par une majuscule, sans underscores.

python
# Définitions de classes
class Student:
    """Represent a student with name and grades."""
    def __init__(self, name):
        self.name = name
        self.grades = []
 
class ShoppingCart:
    """Manage items in a shopping cart."""
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        """Add an item to the cart."""
        self.items.append(item)
 
class DatabaseConnection:
    """Handle database connection and queries."""
    def __init__(self, url):
        self.url = url
 
# Création d’instances (note : les instances utilisent des noms de variables en snake_case)
student = Student("Alice")
shopping_cart = ShoppingCart()
db_connection = DatabaseConnection("localhost")

Pourquoi PascalCase pour les classes ? Cela distingue visuellement les classes des fonctions et des variables. Quand vous voyez Student(), vous savez immédiatement que cela crée une instance d’une classe. Quand vous voyez calculate_average(), vous savez que cela appelle une fonction.

40.2.5) Noms privés et internes : underscore en préfixe

Les noms commençant par un underscore unique (_name) indiquent un usage interne — ils sont destinés à être utilisés dans le module ou la classe, pas par du code externe.

Python n’a pas de syntaxe pour marquer des méthodes ou attributs comme « privés » (contrairement à private en Java ou C++). À la place, Python utilise une convention de nommage avec un underscore en préfixe (_name) pour communiquer l’intention.

Ce que signifie _name :

  • « Ceci est pour un usage interne uniquement »
  • « Je l’ai fait pour être utilisé dans cette classe/module, pas pour du code externe »
  • « Cela peut changer à tout moment dans des versions futures — ne vous appuyez pas dessus »
python
class BankAccount:
    """Represent a bank account with balance tracking."""
    
    def __init__(self, account_number, initial_balance):
        self.account_number = account_number
        self._balance = initial_balance  # Attribut interne
    
    def deposit(self, amount):
        """Add money to the account."""
        if self._validate_amount(amount):  # Méthode interne
            self._balance += amount
    
    def _validate_amount(self, amount):
        """Internal helper to validate transaction amounts."""
        return amount > 0
    
    def get_balance(self):
        """Return the current balance."""
        return self._balance
 
# Utilisation de la classe
account = BankAccount("12345", 1000)
account.deposit(500)
print(account.get_balance())  # Output: 1500
 
# Fonctionne techniquement, mais viole la convention
print(account._balance)
# Output: 1500 (works, but you shouldn't do this!)
 
# Fonctionne techniquement, mais viole la convention
result = account._validate_amount(100)
# Output: True (works, but you shouldn't do this!)

Point clé : Python ne peut pas vous empêcher d’accéder à _balance ou d’appeler _validate_amount(). L’underscore est un signal entre programmeurs, pas une fonctionnalité de sécurité.

Pourquoi cette convention existe

Comme Python ne peut pas imposer la confidentialité, l’underscore est la façon dont les auteurs de classes communiquent leur intention :

Ce que l’underscore signale :

  • « Ceci est une implémentation interne — cela peut changer dans des versions futures »
  • « Utilisez plutôt les méthodes publiques — elles sont garanties stables »
  • « Si vous dépendez de détails internes, votre code peut casser quand je mets à jour la bibliothèque »

La convention crée un contrat : les auteurs de classes peuvent modifier librement l’implémentation interne (tout ce qui commence par _), mais doivent garder l’interface publique stable. Cela permet aux bibliothèques d’évoluer sans casser le code utilisateur.

40.2.6) Noms spéciaux : doubles underscores

Les noms avec des doubles underscores en préfixe et en suffixe (__name__) sont des méthodes spéciales ou méthodes magiques définies par Python. Ne créez pas vos propres noms avec ce motif — il est réservé à l’usage de Python.

python
class Point:
    """Represent a 2D point."""
    
    def __init__(self, x, y):  # Méthode spéciale : initialisation
        self.x = x
        self.y = y
    
    def __str__(self):  # Méthode spéciale : représentation en chaîne
        return f"Point({self.x}, {self.y})"
    
    def __add__(self, other):  # Méthode spéciale : opérateur d’addition
        return Point(self.x + other.x, self.y + other.y)
 
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1)  # Output: Point(1, 2)
print(p1 + p2)  # Output: Point(4, 6)

Comme nous l’avons appris au chapitre 31, ces méthodes spéciales permettent la surcharge d’opérateurs et l’intégration avec les fonctions intégrées de Python.

40.2.7) Tableau récapitulatif du nommage

TypeConventionExemple
Variablessnake_caseuser_name, total_count
Fonctionssnake_casecalculate_tax(), send_email()
ConstantesUPPER_SNAKE_CASEMAX_SIZE, DEFAULT_TIMEOUT
ClassesPascalCaseStudent, ShoppingCart
Interne/Privé_leading_underscore_balance, _validate()
Spécial/Magiquedouble_underscore__init__, __str__

40.3) Mise en forme du code : indentation, espacement et lignes vides

40.3.1) Indentation : quatre espaces

Python utilise l’indentation pour définir les blocs de code. Utilisez toujours 4 espaces par niveau d’indentation — jamais des tabulations, et ne mélangez jamais tabulations et espaces.

python
def calculate_grade(score):
    """Determine letter grade from numeric score."""
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    else:
        return "F"
 
# Indentation imbriquée : 4 espaces par niveau
def process_students(students):
    """Process a list of student records."""
    for student in students:
        if student["active"]:
            grade = calculate_grade(student["score"])
            print(f"{student['name']}: {grade}")
 
students = [
    {"name": "Alice", "score": 92, "active": True},
    {"name": "Bob", "score": 78, "active": True}
]
process_students(students)
# Output:
# Alice: A
# Bob: C

Pourquoi 4 espaces ? C’est la norme de la communauté Python. La plupart du code Python que vous rencontrerez utilise 4 espaces, donc suivre cette convention rend votre code cohérent avec l’écosystème.

Configurer votre éditeur : les éditeurs de code modernes peuvent être réglés pour insérer 4 espaces lorsque vous appuyez sur Tab. Cela vous donne la commodité de la touche Tab tout en respectant la norme des 4 espaces.

40.3.2) Longueur maximale de ligne : 79 caractères

PEP 8 recommande de limiter les lignes à 79 caractères (avec jusqu’à 99 caractères pour les docstrings et les commentaires). Cela peut sembler restrictif, mais cela a des avantages pratiques :

  • Le code reste lisible sur de plus petits écrans
  • Vous pouvez afficher deux fichiers côte à côte
  • Cela encourage à découper les expressions complexes en parties plus simples

Note : de nombreux projets modernes utilisent des limites légèrement plus élevées (88, 100 ou 120 caractères). L’essentiel est la cohérence au sein de votre projet. Choisissez une limite et respectez-la.

python
# Trop long - difficile à lire
# ATTENTION : Mauvais style - uniquement pour démonstration
def calculate_monthly_payment(principal, annual_rate, years):
    return principal * (annual_rate / 12) * (1 + annual_rate / 12) ** (years * 12) / ((1 + annual_rate / 12) ** (years * 12) - 1)
 
# Mieux - découpé en lignes lisibles
def calculate_monthly_payment(principal, annual_rate, years):
    """Calculate monthly loan payment using amortization formula."""
    monthly_rate = annual_rate / 12
    num_payments = years * 12
    
    numerator = principal * monthly_rate * (1 + monthly_rate) ** num_payments
    denominator = (1 + monthly_rate) ** num_payments - 1
    
    return numerator / denominator
 
payment = calculate_monthly_payment(200000, 0.045, 30)
print(f"Monthly payment: ${payment:.2f}")  # Output: Monthly payment: $1013.37

Casser les lignes longues : lorsque vous devez couper une ligne, utilisez la continuation implicite à l’intérieur de parenthèses, crochets ou accolades :

python
# Appel de fonction long
result = some_function(
    first_argument,
    second_argument,
    third_argument,
    fourth_argument
)
 
# Liste longue
colors = [
    "red", "green", "blue",
    "yellow", "orange", "purple",
    "pink", "brown", "gray"
]
 
# Chaîne longue
message = (
    "This is a very long message that needs to be broken "
    "across multiple lines for readability. Python automatically "
    "concatenates adjacent string literals."
)
print(message)
# Output: This is a very long message that needs to be broken across multiple lines for readability. Python automatically concatenates adjacent string literals.

40.3.3) Espacement autour des opérateurs et après les virgules

Utilisez des espaces autour des opérateurs et après les virgules pour améliorer la lisibilité :

python
# Mauvais espacement - serré et difficile à lire
# ATTENTION : Mauvais style - uniquement pour démonstration
x=5
y=x*2+3
result=calculate_tax(100,0.08)
data=[1,2,3,4,5]
 
# Bon espacement - clair et lisible
x = 5
y = x * 2 + 3
result = calculate_tax(100, 0.08)
data = [1, 2, 3, 4, 5]
 
# Espacement dans les expressions
total = (price * quantity) + shipping_cost
is_valid = (age >= 18) and (has_license == True)
 
# Espacement dans les définitions de fonctions
def calculate_discount(price, discount_rate, minimum_purchase=0):
    """Calculate discounted price if minimum purchase is met."""
    if price >= minimum_purchase:
        return price * (1 - discount_rate)
    return price

Exception : n’utilisez pas d’espaces autour de = dans les arguments nommés ou les valeurs par défaut des paramètres :

python
# Espacement correct pour les arguments nommés
result = calculate_discount(price=100, discount_rate=0.1, minimum_purchase=50)
 
# Espacement correct pour les paramètres par défaut
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

40.3.4) Lignes vides pour une séparation logique

Utilisez des lignes vides pour séparer des sections logiques du code :

Deux lignes vides entre les fonctions et classes de niveau supérieur :

python
def first_function():
    """First function."""
    pass
 
 
def second_function():
    """Second function."""
    pass
 
 
class MyClass:
    """A class definition."""
    pass

Une ligne vide entre les méthodes dans une classe :

python
class Student:
    """Represent a student with grades."""
    
    def __init__(self, name):
        self.name = name
        self.grades = []
    
    def add_grade(self, grade):
        """Add a grade to the student's record."""
        self.grades.append(grade)
    
    def get_average(self):
        """Calculate the student's grade average."""
        if not self.grades:
            return 0
        return sum(self.grades) / len(self.grades)

Lignes vides dans les fonctions pour séparer des étapes logiques :

python
def process_order(order_items, customer):
    """Process a customer order and calculate total."""
    
    # Calculer le sous-total
    subtotal = 0
    for item in order_items:
        subtotal += item["price"] * item["quantity"]
    
    # Appliquer la remise client
    discount = 0
    if customer["is_premium"]:
        discount = subtotal * 0.1
    
    # Calculer la taxe
    tax = (subtotal - discount) * 0.08
    
    # Calculer le total final
    total = subtotal - discount + tax
    
    return {
        "subtotal": subtotal,
        "discount": discount,
        "tax": tax,
        "total": total
    }

Ces lignes vides agissent comme des « paragraphes » visuels, rendant la structure du code immédiatement apparente.

40.3.5) Éviter les espaces en fin de ligne

Ne laissez pas d’espaces à la fin des lignes — ils sont invisibles mais peuvent causer des problèmes avec les systèmes de contrôle de version et certains éditeurs.

python
# Mauvais - espaces invisibles en fin de ligne (montrés comme · à titre d’illustration)
# ATTENTION : Mauvais style - uniquement pour démonstration
def calculate(x):···
    return x * 2···
 
# Bon - pas d’espaces en fin de ligne
def calculate(x):
    return x * 2

La plupart des éditeurs modernes peuvent être configurés pour supprimer automatiquement les espaces en fin de ligne lorsque vous enregistrez un fichier.

40.4) Documentation : écrire des commentaires et docstrings utiles

40.4.1) Quand écrire des commentaires

Les commentaires expliquent pourquoi le code fait quelque chose, pas ce qu’il fait. Des variables et des fonctions bien nommées devraient rendre le « quoi » évident.

python
# Mauvais commentaire - énonce l’évidence
# ATTENTION : Mauvais style - uniquement pour démonstration
x = x + 1  # Ajouter 1 à x
 
# Bon commentaire - explique pourquoi
x = x + 1  # Ajuster pour l’indexation à partir de zéro
 
# Mauvais commentaire - redondant avec le code
# ATTENTION : Mauvais style - uniquement pour démonstration
# Vérifier si l’âge est supérieur ou égal à 18
if age >= 18:
    print("Adult")
 
# Bon commentaire - explique la logique métier
# Âge légal pour boire aux États-Unis
if age >= 21:
    print("Can purchase alcohol")

Quand les commentaires sont utiles :

  1. Expliquer des algorithmes complexes :
python
def binary_search(sorted_list, target):
    """Search for target in sorted list using binary search."""
    left = 0
    right = len(sorted_list) - 1
    
    while left <= right:
        # Calculer le point milieu, en évitant un dépassement d’entier
        # (right + left) // 2 pourrait déborder avec des indices très grands
        mid = left + (right - left) // 2
        
        if sorted_list[mid] == target:
            return mid
        elif sorted_list[mid] < target:
            left = mid + 1  # La cible est dans la moitié droite
        else:
            right = mid - 1  # La cible est dans la moitié gauche
    
    return -1  # Cible non trouvée
  1. Clarifier des règles métier non évidentes :
python
def calculate_shipping_cost(weight, distance):
    """Calculate shipping cost based on weight and distance."""
    base_cost = 5.00
    
    # Promotion de livraison gratuite pour les articles lourds (politique d’entreprise en 2024)
    # Cela encourage les commandes en gros et réduit les coûts de livraison par unité
    if weight > 50:
        return 0
    
    # Tarif standard : 0,50 $ par livre plus 0,10 $ par mile
    # Basé sur le contrat transporteur négocié au T1 2024
    return base_cost + (weight * 0.50) + (distance * 0.10)
  1. Documenter des contournements ou des solutions temporaires :
python
def process_data(data):
    """Process incoming data records."""
    # TODO: Ceci est un correctif temporaire pour des enregistrements malformés
    # À supprimer une fois que la validation des données est implémentée en amont
    if not isinstance(data, list):
        data = [data]
    
    for record in data:
        # Traiter chaque enregistrement
        pass

40.4.2) Écrire des docstrings efficaces

Les docstrings sont des commentaires spéciaux qui documentent les modules, classes et fonctions. Elles sont entre triples guillemets et apparaissent comme la première instruction dans la définition.

python
def calculate_bmi(weight_kg, height_m):
    """
    Calculate Body Mass Index (BMI).
    
    BMI is calculated as weight in kilograms divided by the square of height in meters.
    
    Args:
        weight_kg: Weight in kilograms (float or int)
        height_m: Height in meters (float or int)
    
    Returns:
        float: The calculated BMI value
    
    Example:
        >>> calculate_bmi(70, 1.75)
        22.857142857142858
    """
    return weight_kg / (height_m ** 2)
 
# Accéder aux docstrings
print(calculate_bmi.__doc__)
# Output:
#     Calculate Body Mass Index (BMI).
#     
#     BMI is calculated as weight in kilograms divided by the square of height in meters.
#     ...

Docstrings sur une ligne pour les fonctions simples :

python
def square(x):
    """Return the square of x."""
    return x * x
 
def is_even(n):
    """Return True if n is even, False otherwise."""
    return n % 2 == 0

Docstrings multi-lignes pour les fonctions complexes :

python
def find_prime_factors(n):
    """
    Find all prime factors of a positive integer.
    
    This function returns a list of prime numbers that, when multiplied
    together, equal the input number. The factors are returned in ascending order.
    
    Args:
        n: A positive integer greater than 1
    
    Returns:
        list: Prime factors in ascending order
    
    Raises:
        ValueError: If n is less than 2
    
    Example:
        >>> find_prime_factors(12)
        [2, 2, 3]
        >>> find_prime_factors(17)
        [17]
    """
    if n < 2:
        raise ValueError("n must be at least 2")
    
    factors = []
    divisor = 2
    
    while n > 1:
        while n % divisor == 0:
            factors.append(divisor)
            n = n // divisor
        divisor += 1
    
    return factors

Docstrings de classe :

python
class BankAccount:
    """
    Represent a bank account with deposit and withdrawal operations.
    
    This class maintains an account balance and provides methods for
    depositing and withdrawing money. All transactions are validated to prevent negative balances.
    
    Attributes:
        account_number: Unique identifier for the account
        balance: Current account balance in dollars
    """
    
    def __init__(self, account_number, initial_balance=0):
        """
        Initialize a new bank account.
        
        Args:
            account_number: Unique account identifier (string)
            initial_balance: Starting balance (default: 0)
        """
        self.account_number = account_number
        self.balance = initial_balance
    
    def deposit(self, amount):
        """
        Add money to the account.
        
        Args:
            amount: Amount to deposit (must be positive)
        
        Raises:
            ValueError: If amount is not positive
        """
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self.balance += amount

40.4.3) Conventions de docstring

Première ligne : bref résumé de ce que fait la fonction/classe. Doit tenir sur une ligne.

Ligne vide : séparer le résumé de la description détaillée.

Description détaillée : expliquez ce que fait la fonction, les détails importants, et comment l’utiliser.

Args/Parameters : listez chaque paramètre avec son type et son rôle.

Returns : décrivez ce que la fonction renvoie et son type.

Raises : documentez les exceptions que la fonction peut lever.

Example : montrez un usage typique (optionnel mais utile).

python
def calculate_compound_interest(principal, rate, time, compounds_per_year=1):
    """
    Calculate compound interest on an investment.
    
    Uses the compound interest formula: A = P(1 + r/n)^(nt)
    where A is the final amount, P is principal, r is annual rate,
    n is compounds per year, and t is time in years.
    
    Args:
        principal: Initial investment amount (float)
        rate: Annual interest rate as decimal (e.g., 0.05 for 5%)
        time: Investment period in years (float)
        compounds_per_year: Number of times interest compounds annually
                           (default: 1 for annual compounding)
    
    Returns:
        float: Final amount after compound interest
    
    Example:
        >>> calculate_compound_interest(1000, 0.05, 10, 12)
        1647.0095
    """
    return principal * (1 + rate / compounds_per_year) ** (compounds_per_year * time)

40.4.4) Commentaires TODO pour des travaux futurs

Utilisez des commentaires TODO pour marquer les zones nécessitant une attention ultérieure :

python
def process_payment(amount, payment_method):
    """Process a payment transaction."""
    # TODO: Ajouter le support des paiements en cryptomonnaie
    # TODO: Implémenter des contrôles de détection de fraude
    
    if payment_method == "credit_card":
        return process_credit_card(amount)
    elif payment_method == "paypal":
        return process_paypal(amount)
    else:
        raise ValueError(f"Unsupported payment method: {payment_method}")

De nombreux éditeurs peuvent rechercher les commentaires TODO, ce qui facilite la localisation des zones à traiter.

40.5) Organiser votre code : imports, constantes, fonctions et main

40.5.1) Structure standard d’un module

Un module Python bien organisé suit cette structure :

  1. Docstring de module : décrit ce que fait le module
  2. Imports : bibliothèque standard, bibliothèques tierces, puis imports locaux
  3. Constantes : constantes au niveau du module
  4. Fonctions et classes : code principal
  5. Bloc d’exécution main : code exécuté lorsque le script est lancé
python
"""
student_manager.py
 
Manage student records including grades and GPA calculations.
 
This module provides functions for adding students, recording grades,
and calculating grade point averages.
"""
 
# Imports de la bibliothèque standard
import sys
from datetime import datetime
 
# Imports tiers (le cas échéant)
# import requests
 
# Imports locaux (le cas échéant)
# from .database import save_student
 
# Constantes
MAX_GRADE = 100
MIN_GRADE = 0
PASSING_GRADE = 60
 
# Fonctions
def calculate_gpa(grades):
    """
    Calculate GPA from a list of numeric grades.
    
    Args:
        grades: List of numeric grades (0-100)
    
    Returns:
        float: GPA on 4.0 scale
    """
    if not grades:
        return 0.0
    
    average = sum(grades) / len(grades)
    
    # Convertir vers l’échelle 4.0
    if average >= 90:
        return 4.0
    elif average >= 80:
        return 3.0
    elif average >= 70:
        return 2.0
    elif average >= 60:
        return 1.0
    else:
        return 0.0
 
def validate_grade(grade):
    """
    Check if a grade is within valid range.
    
    Args:
        grade: Numeric grade to validate
    
    Returns:
        bool: True if grade is valid, False otherwise
    """
    return MIN_GRADE <= grade <= MAX_GRADE
 
# Exécution main
if __name__ == "__main__":
    # Code exécuté lorsque le script est lancé directement
    test_grades = [85, 92, 78, 88]
    gpa = calculate_gpa(test_grades)
    print(f"GPA: {gpa}")  # Output: GPA: 3.0

40.5.2) Organisation des imports

Regroupez les imports en trois sections, séparées par des lignes vides :

  1. Imports de la bibliothèque standard : modules intégrés de Python
  2. Imports tiers : paquets installés (comme requests, numpy)
  3. Imports locaux : vos propres modules
python
# Imports de la bibliothèque standard
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
 
# Imports tiers
import requests
from flask import Flask, render_template
 
# Imports locaux de l’application
from myapp.database import connect_db
from myapp.models import User, Product
from myapp.utils import format_currency

Styles d’import :

python
# Importer tout le module
import math
result = math.sqrt(16)  # Output: 4.0
 
# Importer des éléments spécifiques
from math import sqrt, pi
result = sqrt(16)  # Output: 4.0
 
# Importer avec un alias
import numpy as np
array = np.array([1, 2, 3])
 
# Importer plusieurs éléments
from os import path, getcwd, listdir

Évitez les imports wildcard (from module import *) — ils rendent flou l’origine des noms :

python
# Mauvais - on ne sait pas d’où vient sqrt
# ATTENTION : Mauvais style - uniquement pour démonstration
from math import *
result = sqrt(16)
 
# Bon - import explicite
from math import sqrt
result = sqrt(16)

40.5.3) Organiser les constantes

Placez les constantes au niveau du module près du haut, après les imports :

python
"""Configuration settings for the application."""
 
import os
 
# Constantes de l’application
APP_NAME = "Student Manager"
VERSION = "1.0.0"
DEBUG_MODE = True
 
# Configuration de la base de données
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///students.db")
MAX_CONNECTIONS = 10
 
# Règles métier
MAX_STUDENTS_PER_CLASS = 30
PASSING_GRADE = 60
GRADE_WEIGHTS = {
    "homework": 0.3,
    "midterm": 0.3,
    "final": 0.4
}
 
def calculate_final_grade(homework, midterm, final):
    """Calculate weighted final grade."""
    return (
        homework * GRADE_WEIGHTS["homework"] +
        midterm * GRADE_WEIGHTS["midterm"] +
        final * GRADE_WEIGHTS["final"]
    )

40.5.4) Ordonnancement logique des fonctions

Organisez les fonctions dans un ordre logique :

  1. Fonctions publiques d’abord : fonctions destinées à être utilisées par d’autres modules
  2. Fonctions helper ensuite : fonctions internes qui soutiennent les fonctions publiques
  3. Fonctions liées ensemble : regroupez les fonctions qui travaillent ensemble
python
"""Order processing module."""
 
# Fonctions d’API publiques
def process_order(order_items, customer):
    """
    Process a customer order.
    
    This is the main entry point for order processing.
    """
    subtotal = _calculate_subtotal(order_items)
    discount = _calculate_discount(subtotal, customer)
    tax = _calculate_tax(subtotal - discount)
    total = subtotal - discount + tax
    
    return {
        "subtotal": subtotal,
        "discount": discount,
        "tax": tax,
        "total": total
    }
 
def validate_order(order_items):
    """Validate that an order contains valid items."""
    if not order_items:
        return False
    
    for item in order_items:
        if not _validate_item(item):
            return False
    
    return True
 
# Fonctions helper internes
def _calculate_subtotal(items):
    """Calculate order subtotal (internal use)."""
    total = 0
    for item in items:
        total += item["price"] * item["quantity"]
    return total
 
def _calculate_discount(subtotal, customer):
    """Calculate customer discount (internal use)."""
    if customer.get("is_premium"):
        return subtotal * 0.1
    return 0
 
def _calculate_tax(amount):
    """Calculate sales tax (internal use)."""
    TAX_RATE = 0.08
    return amount * TAX_RATE
 
def _validate_item(item):
    """Validate a single order item (internal use)."""
    required_fields = ["name", "price", "quantity"]
    return all(field in item for field in required_fields)

Remarquez comment les fonctions publiques (process_order, validate_order) viennent en premier, et les fonctions helper (préfixées par _) viennent ensuite. Cela rend clair quelles fonctions constituent l’API principale.

40.5.5) Organisation des classes au sein des modules

Lorsqu’un module contient des classes, organisez-les de façon logique :

python
"""User management system."""
 
# Constantes
DEFAULT_ROLE = "user"
ADMIN_ROLE = "admin"
 
# Classes de base d’abord
class User:
    """Base user class."""
    
    def __init__(self, username, email):
        self.username = username
        self.email = email
        self.role = DEFAULT_ROLE
    
    def can_edit(self, resource):
        """Check if user can edit a resource."""
        return resource.owner == self.username
 
# Classes dérivées après les classes de base
class AdminUser(User):
    """Administrator with elevated privileges."""
    
    def __init__(self, username, email):
        super().__init__(username, email)
        self.role = ADMIN_ROLE
    
    def can_edit(self, resource):
        """Admins can edit any resource."""
        return True
 
# Classes liées regroupées
class Resource:
    """Represent a resource that can be owned and edited."""
    
    def __init__(self, name, owner):
        self.name = name
        self.owner = owner
 
# Fonctions utilitaires liées aux classes
def create_user(username, email, is_admin=False):
    """Factory function to create appropriate user type."""
    if is_admin:
        return AdminUser(username, email)
    return User(username, email)

Principes d’organisation des classes :

  • Classes de base avant classes dérivées (les lecteurs doivent comprendre la base d’abord)
  • Classes liées regroupées (User et Resource sont liées)
  • Les fonctions utilitaires qui travaillent avec les classes viennent après les définitions de classes
  • Chaque classe doit avoir une docstring claire expliquant son objectif

40.6) Le motif if __name__ == "__main__"

40.6.1) Comprendre le motif

Chaque fichier Python possède une variable intégrée appelée __name__. Python définit automatiquement la valeur de cette variable en fonction de la façon dont le fichier est utilisé :

  • Quand vous exécutez un fichier directement (par exemple, python my_script.py), Python définit __name__ à "__main__"
  • Quand vous importez un fichier en tant que module, Python définit __name__ au nom du module (le nom du fichier sans .py)

Cela vous permet d’écrire du code qui ne s’exécute que lorsque le fichier est lancé directement, et pas lorsqu’il est importé :

python
"""math_utils.py - Mathematical utility functions."""
 
def add(a, b):
    """Add two numbers."""
    return a + b
 
def multiply(a, b):
    """Multiply two numbers."""
    return a * b
 
# Ce code ne s’exécute que lorsque le fichier est lancé directement
if __name__ == "__main__":
    # Tester les fonctions
    print(f"5 + 3 = {add(5, 3)}")  # Output: 5 + 3 = 8
    print(f"5 * 3 = {multiply(5, 3)}")  # Output: 5 * 3 = 15

Quand vous lancez python math_utils.py, vous verrez la sortie. Mais quand vous l’importez dans un autre fichier :

python
# another_file.py
from math_utils import add, multiply
 
result = add(10, 20)
print(result)  # Output: 30
# Le code de test de math_utils.py ne s’exécute PAS

Remarquez que le code de test (à l’intérieur de if __name__ == "__main__":) ne s’exécute PAS lorsqu’il est importé !

python math_utils.py

import math_utils

Fichier Python exécuté

Comment est-il lancé ?

name = 'main'

name = 'math_utils'

Le code dans if name == 'main' s’exécute

Le code dans if name == 'main' est ignoré

40.6.2) Pourquoi ce motif est important

Ce motif sert plusieurs objectifs importants :

1. Tests et démonstration : vous pouvez inclure des exemples d’utilisation dans le même fichier que vos fonctions :

python
"""temperature.py - Temperature conversion utilities."""
 
def celsius_to_fahrenheit(celsius):
    """Convert Celsius to Fahrenheit."""
    return (celsius * 9/5) + 32
 
def fahrenheit_to_celsius(fahrenheit):
    """Convert Fahrenheit to Celsius."""
    return (fahrenheit - 32) * 5/9
 
if __name__ == "__main__":
    # Démontrer les fonctions
    print("Temperature Conversion Examples:")
    print(f"0°C = {celsius_to_fahrenheit(0)}°F")  # Output: 0°C = 32.0°F
    print(f"100°C = {celsius_to_fahrenheit(100)}°F")  # Output: 100°C = 212.0°F
    print(f"32°F = {fahrenheit_to_celsius(32)}°C")  # Output: 32°F = 0.0°C

2. Modules réutilisables : le même fichier peut être à la fois un script autonome et un module importable :

python
"""data_processor.py - Process and analyze data files."""
 
import sys
 
def load_data(filename):
    """Load data from a file."""
    with open(filename) as f:
        return [line.strip() for line in f]
 
def analyze_data(data):
    """Perform analysis on data."""
    return {
        "count": len(data),
        "average_length": sum(len(item) for item in data) / len(data)
    }
 
if __name__ == "__main__":
    # Lorsqu’il est lancé comme script, traiter les arguments en ligne de commande
    if len(sys.argv) < 2:
        print("Usage: python data_processor.py <filename>")
        sys.exit(1)
    
    filename = sys.argv[1]
    data = load_data(filename)
    results = analyze_data(data)
    
    print(f"Processed {results['count']} items")
    print(f"Average length: {results['average_length']:.2f}")

Vous pouvez l’exécuter comme script :

bash
$ python data_processor.py data.txt
Processed 42 items
Average length: 15.23

Ou l’importer dans un autre fichier :

python
# my_analysis.py
from data_processor import load_data, analyze_data
 
my_data = load_data("myfile.txt")
results = analyze_data(my_data)
print(f"Found {results['count']} items")

40.6.3) Motifs courants pour les blocs main

Motif 1 : cas de test simples

python
"""calculator.py - Basic calculator operations."""
 
def add(a, b):
    """Add two numbers."""
    return a + b
 
def subtract(a, b):
    """Subtract b from a."""
    return a - b
 
if __name__ == "__main__":
    # Tests rapides
    assert add(2, 3) == 5
    assert subtract(10, 4) == 6
    print("All tests passed!")  # Output: All tests passed!

Motif 2 : fonction main

Pour des scripts plus complexes, définissez une fonction main() :

python
"""report_generator.py - Generate reports from data."""
 
import sys
 
def load_data(filename):
    """Load data from file."""
    # Implementation here
    pass
 
def generate_report(data):
    """Generate report from data."""
    # Implementation here
    pass
 
def save_report(report, output_file):
    """Save report to file."""
    # Implementation here
    pass
 
def main():
    """Main entry point for the script."""
    if len(sys.argv) < 3:
        print("Usage: python report_generator.py <input> <output>")
        return 1
    
    input_file = sys.argv[1]
    output_file = sys.argv[2]
    
    try:
        data = load_data(input_file)
        report = generate_report(data)
        save_report(report, output_file)
        print(f"Report saved to {output_file}")
        return 0
    except Exception as e:
        print(f"Error: {e}")
        return 1
 
if __name__ == "__main__":
    # Quitter avec le code de statut de main (0 = succès, 1 = erreur)
    sys.exit(main())

Ce motif présente plusieurs avantages :

  • La fonction main() peut être testée indépendamment
  • Point d’entrée clair pour le script
  • Codes de sortie corrects (0 pour le succès, non-zéro pour les erreurs)
  • Séparation nette entre la logique du script et les fonctions du module

40.6.4) Bonnes pratiques pour les blocs main

Gardez les blocs main ciblés : le code à l’intérieur de if __name__ == "__main__" devrait surtout gérer l’exécution du script, et ne pas contenir de logique complexe :

python
# Mauvais - logique complexe dans le bloc main
# ATTENTION : Mauvais style - uniquement pour démonstration
if __name__ == "__main__":
    data = []
    for i in range(100):
        if i % 2 == 0:
            data.append(i * 2)
    result = sum(data) / len(data)
    print(result)
 
# Bon - logique dans des fonctions, le bloc main coordonne
def generate_even_doubles(limit):
    """Generate doubled even numbers up to limit."""
    return [i * 2 for i in range(limit) if i % 2 == 0]
 
def calculate_average(numbers):
    """Calculate average of numbers."""
    return sum(numbers) / len(numbers)
 
if __name__ == "__main__":
    data = generate_even_doubles(100)
    result = calculate_average(data)
    print(result)  # Output: 99.0

Utilisez une fonction main() pour les scripts complexes : comme montré plus haut, définir une fonction main() rend votre script plus testable et mieux organisé.

Documentez l’usage du script : si votre script accepte des arguments en ligne de commande, documentez-les dans la docstring du module :

python
"""
file_processor.py - Process text files with various operations.
 
Usage:
    python file_processor.py <input_file> <output_file> [--uppercase]
 
Arguments:
    input_file: Path to input file
    output_file: Path to output file
    --uppercase: Convert text to uppercase (optional)
"""
 
import sys
 
def process_file(input_path, output_path, uppercase=False):
    """Process file with specified options."""
    with open(input_path) as f:
        content = f.read()
    
    if uppercase:
        content = content.upper()
    
    with open(output_path, 'w') as f:
        f.write(content)
 
if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(__doc__)  # Afficher la docstring du module
        sys.exit(1)
    
    input_file = sys.argv[1]
    output_file = sys.argv[2]
    uppercase = "--uppercase" in sys.argv
    
    process_file(input_file, output_file, uppercase)
    print(f"Processed {input_file} -> {output_file}")

Écrire du code propre et lisible est une compétence qui se développe avec la pratique. Les conventions et les motifs de ce chapitre ne sont pas des règles arbitraires — ce sont des pratiques éprouvées qui rendent le code plus facile à comprendre, à maintenir et à déboguer. À mesure que vous écrirez davantage de code Python, ces motifs deviendront naturels.

Rappelez-vous : le code est lu bien plus souvent qu’il n’est écrit. Les quelques secondes supplémentaires que vous passez à choisir un nom clair, ajouter un commentaire utile ou organiser correctement vos imports vous feront économiser des heures de confusion plus tard — pour vous-même et pour les autres qui travaillent avec votre code.

Dans le prochain chapitre, nous explorerons des techniques de débogage et de test qui s’appuient sur ces pratiques de code propre, afin de vous aider à écrire non seulement du code lisible, mais aussi du code correct et fiable.

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