46  Qualité du code : PEP 8, Black, linting

Un code qui marche n’est qu’un début. Un code de qualité est lisible, cohérent, maintenable. La communauté Python a établi des conventions (PEP 8) et des outils automatiques (Black, Flake8, Ruff, mypy) qui vous aident à écrire comme un professionnel. C’est un sujet qui compte au TOSA Expert — et qui change la vie en entreprise.

46.1 Pourquoi la qualité de code compte

Trois constats :

  1. On lit le code beaucoup plus souvent qu’on l’écrit. Vos collègues (et vous-même dans 6 mois) passeront plus de temps à le comprendre qu’à le taper.
  2. Un code propre a moins de bugs. La clarté révèle les erreurs ; l’obscurité les cache.
  3. Une cohérence globale dans une équipe réduit les frictions. Plus de guerres d’accolades.

La PEP 8 est le guide de style officiel. Les outils modernes l’appliquent automatiquement.

46.2 La PEP 8 en 10 règles

La PEP 8 est longue. Voici les règles que vous devez connaître par cœur.

1. Indentation : 4 espaces

# ✅
def f():
    if condition:
        action()

Pas de tabulations. Configurez votre éditeur pour que Tab insère 4 espaces.

2. Longueur de ligne : 79 caractères max (ou 88 avec Black)

# ✅ Couper les longues lignes
resultat = fonction_avec_long_nom(
    argument_1,
    argument_2,
    argument_3,
)

La PEP 8 stricte dit 79. Black (voir plus bas) utilise 88 par défaut, considéré comme un compromis moderne.

3. Nommage

Élément Style Exemple
Variable, fonction, méthode snake_case ma_variable, calculer_moyenne()
Classe PascalCase CompteBancaire
Constante UPPER_SNAKE_CASE MAX_ESSAIS, PI
Privé (convention) préfixe _ _solde, _methode_interne
Module/package lowercase, court mon_module, utils

4. Espacement

# ✅ Autour des opérateurs
x = 1 + 2
y = a * b - c

# ✅ Après une virgule
def f(a, b, c):
    ...

# ❌ Pas d'espaces autour des = pour valeurs par défaut
def f(x=10): ...          # ✅
def f(x = 10): ...        # ❌

# ✅ Pas d'espace à l'intérieur des parenthèses
f(x, y)                   # ✅
f( x, y )                 # ❌

5. Lignes vides

# Deux lignes vides entre les fonctions/classes top-level
def fonction_1():
    ...


def fonction_2():
    ...


# Une ligne vide entre les méthodes d'une classe
class MaClasse:
    def methode_1(self):
        ...

    def methode_2(self):
        ...

6. Imports

# ✅ Un import par ligne
import os
import sys

# ❌ À éviter
import os, sys

# Ordre des imports (séparés par des lignes vides) :
# 1. Bibliothèque standard
import os
from pathlib import Path

# 2. Bibliothèques tierces
import requests
import numpy as np

# 3. Modules locaux
from .utils import fonction

7. Comparaisons

# ✅ Utilisez is avec None
if x is None:
    ...

# ❌
if x == None:
    ...

# ✅ Négation claire
if not x:
    ...

# ❌ Double négation
if not x != 10:
    ...

8. Chaînes

Les guillemets simples et doubles sont équivalents. Choisissez-en un et tenez-vous-y. Black utilise "..." par défaut.

# Cohérence : tous simples ou tous doubles
nom = "Alice"
salutation = "Bonjour"

# Double si la chaîne contient un simple
citation = "Il a dit : 'salut'"

9. Docstrings

Pour chaque module, classe, fonction publique, écrivez une docstring (vu chapitre 2 partie 2).

def calculer_moyenne(notes):
    """Calcule la moyenne d'une liste de notes.

    Arguments:
        notes: liste de nombres (int ou float).

    Renvoie:
        La moyenne arithmétique, ou 0 si la liste est vide.
    """
    if not notes:
        return 0
    return sum(notes) / len(notes)

10. Commentaires

  • Expliquez le pourquoi, pas le comment (le code montre le comment).
  • Un commentaire inexact est pire que pas de commentaire.
  • Mettez les commentaires à jour quand vous modifiez le code.
# ❌ Inutile : le code dit exactement ça
x = x + 1     # on ajoute 1 à x

# ✅ Utile : explique le "pourquoi"
x = x + 1     # on compense l'offset de l'index 0-based

46.3 L’outil phare : Black, le formateur « sans compromis »

Black est un formateur de code automatique. Vous exécutez black mon_fichier.py, et votre code est reformaté selon des règles fixes et non négociables.

Installation

pip install black

Utilisation

# Formater un fichier
black mon_script.py

# Formater tout un dossier
black .

# Vérifier sans modifier
black --check .

# Voir les changements sans les appliquer
black --diff mon_script.py

Avant/après Black

Code mal formaté :

def calculer_total(  prix, quantite  ,tva = 0.20 ):
    return prix*quantite*( 1+tva )

liste=[1 ,2,3,  4,5]

Après black :

def calculer_total(prix, quantite, tva=0.20):
    return prix * quantite * (1 + tva)


liste = [1, 2, 3, 4, 5]
La philosophie de Black

Black se résume en une phrase : « toutes les discussions sur le style sont terminées ».

Plus de débats pour savoir s’il faut mettre un espace ici ou là, aligner les signes =, préférer les simples ou doubles guillemets. Black décide, on ne discute pas, on code.

Cette approche économise un temps fou en équipe et en revue de code.

46.4 Les linters : détecter les problèmes

Un linter analyse votre code pour détecter :

  • Violations de style (PEP 8).
  • Variables non utilisées.
  • Imports inutilisés.
  • Noms mal orthographiés.
  • Bugs potentiels.

Flake8

Historiquement, le plus populaire.

pip install flake8
flake8 mon_script.py

Exemple de sortie :

mon_script.py:3:1: F401 'os' imported but unused
mon_script.py:10:80: E501 line too long (85 > 79 characters)
mon_script.py:15:5: E225 missing whitespace around operator

Codes :

  • E : erreurs de style (PEP 8).
  • W : warnings.
  • F : erreurs logiques (pyflakes).

Ruff — l’alternative moderne et ultra-rapide

Ruff (écrit en Rust) est 10 à 100 fois plus rapide que Flake8 et combine plusieurs outils. C’est l’outil en ascension dans les projets modernes.

pip install ruff
ruff check mon_script.py         # lint
ruff format mon_script.py        # formatage (comme Black)

Pylint

Plus strict et plus verbeux que Flake8. Vérifie aussi des aspects comme la longueur des fonctions, la complexité, la documentation.

pip install pylint
pylint mon_script.py

Pylint donne un score sur 10 à votre code — parfois décourageant au début, toujours instructif.

Comparatif des outils

Outil Rôle Vitesse
Black Formatage auto Rapide
Ruff format Formatage auto (Black-compatible) Ultra-rapide
Flake8 Lint (style + erreurs) Moyen
Ruff check Lint (multi-outils) Ultra-rapide
Pylint Lint strict Lent
mypy Vérification de types Moyen
isort Tri des imports Rapide

Setup moderne minimal : Ruff (format + check) + mypy si vous utilisez les types.

46.5 Les type hints et mypy

Python est dynamiquement typé : on n’annonce pas les types. Mais on peut les annoter pour la documentation et le contrôle statique.

Annotations de base

def additionner(a: int, b: int) -> int:
    """Additionne deux entiers."""
    return a + b


# Python ignore les annotations à l'exécution — aucun check à l'appel
print(additionner(3, 5))
8

Vous verrez la typologie complète au chapitre suivant (Type hints et mypy).

Pour l’instant retenez : a: int signale qu’on attend un int. mypy peut vérifier cela sans exécuter le code.

46.6 Conventions Pythoniques

Au-delà de la PEP 8, il y a le « Pythonique » — des façons d’écrire idiomatiques.

Préférer les compréhensions aux boucles

# ❌ Verbeux
pairs = []
for n in range(10):
    if n % 2 == 0:
        pairs.append(n)

# ✅ Pythonique
pairs = [n for n in range(10) if n % 2 == 0]

Utiliser enumerate plutôt qu’un compteur manuel

# ❌
i = 0
for item in liste:
    print(i, item)
    i += 1

# ✅
for i, item in enumerate(liste):
    print(i, item)

Utiliser zip pour itérer sur plusieurs listes

noms = ["Alice", "Bob", "Eve"]
ages = [30, 25, 28]

# ❌
for i in range(len(noms)):
    print(noms[i], ages[i])

# ✅
for nom, age in zip(noms, ages):
    print(nom, age)

with pour les ressources

# ❌
f = open("fichier.txt")
# ... traitement
f.close()

# ✅
with open("fichier.txt") as f:
    # ... traitement

dict.get plutôt qu’un try/except

# ❌ Verbeux
try:
    x = d["cle"]
except KeyError:
    x = "défaut"

# ✅ Pythonique
x = d.get("cle", "défaut")

Tester avec in plutôt qu’une chaîne de or

# ❌
if code == 200 or code == 201 or code == 204:
    ...

# ✅
if code in (200, 201, 204):
    ...

Les f-strings au lieu de % et .format

nom, age = "Alice", 30

# ❌ Anciens styles
msg = "Bonjour %s, %d ans" % (nom, age)
msg = "Bonjour {}, {} ans".format(nom, age)

# ✅ f-string (Python 3.6+)
msg = f"Bonjour {nom}, {age} ans"

46.7 The Zen of Python

Python a une philosophie officielle, connue sous le nom de Zen of Python — écrit par Tim Peters. Affichez-le :

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Quelques aphorismes à méditer :

  • Beautiful is better than ugly.
  • Explicit is better than implicit.
  • Simple is better than complex.
  • Readability counts.
  • There should be one — and preferably only one — obvious way to do it.

Ces principes guident les choix de conception de Python et de ses bibliothèques. Ils devraient aussi guider les vôtres.

46.8 Un workflow qualité typique

Voici ce qu’un projet pro met en place :

  1. Pre-commit hooks (Git) : Ruff + Black exécutés avant chaque commit. Le code non conforme est refusé.

  2. CI/CD : le pipeline d’intégration continue lance les linters et tests à chaque push.

  3. Revues de code : un collègue relit avant le merge.

  4. Tests unitaires : voir chapitre 7 — chaque bug trouvé donne un test pour ne pas le reproduire.

Setup minimal avec pre-commit :

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.1.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

Puis :

pip install pre-commit
pre-commit install
# Désormais, chaque git commit vérifie et formate

46.9 Exemple concret : refactoring qualité

Voyons un morceau de code mauvais, puis sa version améliorée.

Version originale (à éviter)

def  calcDuCoutTOTAL(l,t=0.2):
    r=0
    for i in range(len(l)):
     r=r+l[i]['prix']*l[i]['qte']
    return r*(1+t)

Problèmes :

  • Indentation à 1 espace (illégal PEP 8).
  • Noms obscurs : l, t, r, calcDuCoutTOTAL.
  • Mélange de styles (camelCase + rien).
  • Double espace après def.
  • for i in range(len(l)) au lieu d’itérer directement.
  • Accès l[i]['prix'] au lieu d’utiliser une variable.
  • Pas de docstring.

Version refactorisée

def calculer_cout_total(articles, tva=0.20):
    """Calcule le coût TTC d'un panier d'articles.

    Arguments:
        articles: liste de dicts avec les clés 'prix' et 'qte'.
        tva: taux de TVA (défaut 20%).

    Renvoie:
        Le total TTC sous forme de float.
    """
    total_ht = sum(article["prix"] * article["qte"] for article in articles)
    return total_ht * (1 + tva)


# Test
panier = [
    {"prix": 10, "qte": 2},
    {"prix": 5,  "qte": 3},
]
print(f"Total TTC : {calculer_cout_total(panier):.2f} €")
Total TTC : 42.00 €

Améliorations :

  • Noms clairs en snake_case.
  • Docstring explicite.
  • Itération directe (pas range(len(...))).
  • Compréhension génératrice dans sum() — concis et efficace.
  • Espacement correct.

Même logique, code 10 fois plus lisible.


🧩 Quiz 6.1 — Qualité de code

Question 1

Quel est le style de nommage recommandé par PEP 8 pour les fonctions ?

  1. camelCase
  2. snake_case
  3. PascalCase
  4. UPPER_SNAKE_CASE

b) snake_case — les fonctions et variables en minuscules avec underscores (calculer_moyenne, ma_variable). Les classes en PascalCase, les constantes en UPPER_SNAKE_CASE.

Question 2

Combien d’espaces pour l’indentation selon PEP 8 ?

  1. 2
  2. 4
  3. 8
  4. Une tabulation

b) 4 espaces — pas de tabulations, cohérence dans tout le code.

Question 3

Que fait Black ?

  1. Lint le code et affiche les erreurs
  2. Reformate le code automatiquement selon des règles fixes
  3. Exécute le code en mode debug
  4. Convertit Python 2 en Python 3

b) — Black est un formateur (pas un linter) qui applique un style non négociable. Élimine les débats de style.

Question 4

Quelle expression est la plus Pythonique ?

  1. for i in range(len(liste)): print(liste[i])
  2. i = 0; while i < len(liste): print(liste[i]); i += 1
  3. for item in liste: print(item)
  4. liste.map(print)

c) — itérer directement sur la liste. Si on a besoin de l’index, enumerate(liste).

Question 5

Que recommande PEP 8 pour comparer avec None ?

  1. if x == None:
  2. if x is None:
  3. if x == null:
  4. Les deux sont équivalents

b) if x is None:None est un singleton, utiliser is est idiomatique et recommandé.

Question 6

Quelle est la différence entre Black et Flake8 ?

  1. Aucune
  2. Black formate automatiquement, Flake8 détecte les problèmes sans modifier
  3. Flake8 est plus récent
  4. Black est un linter

b)Black = formateur (transforme), Flake8 = linter (signale seulement). Utilisés ensemble : Black pour le style, Flake8 pour les erreurs logiques.

Question 7

Parmi ces expressions, laquelle est la plus Pythonique ?

if statut == "OK" or statut == "VALID" or statut == "DONE":
    ...
  1. C’est déjà la forme idéale
  2. if statut in ("OK", "VALID", "DONE"):
  3. if any(statut == s for s in ("OK", "VALID", "DONE")):
  4. Utiliser match/case

b) if statut in ("OK", "VALID", "DONE"): — concis et clair. L’option c) fonctionne mais est plus lourde. match/case est overkill pour ce test simple.

Question 8

Qu’affiche import this ?

  1. Rien, c’est une erreur
  2. La version de Python
  3. Le Zen of Python (aphorismes philosophiques)
  4. La liste des modules installés

c) — easter egg classique. import this affiche 19 aphorismes guidant la philosophie Python.


✏️ Exercice 6.1 — Renommer selon PEP 8

Réécrivez ces identifiants selon les conventions PEP 8.

Original Rôle
ListeDesClients variable
calculTotal fonction
compte_bancaire classe
max_essais constante
__variable_privee__ attribut privé
Original Rôle Correct
ListeDesClients variable liste_des_clients
calculTotal fonction calcul_total
compte_bancaire classe CompteBancaire
max_essais constante MAX_ESSAIS
__variable_privee__ attribut privé _variable_privee (pas double sauf dunder)

Note sur les doubles underscores

Les __nom__ (double de chaque côté) sont réservés à Python (dunders : __init__, __str__). N’en créez pas.

Le __nom (double au début seulement) déclenche le name mangling — usage rare et avancé.

Le _nom (simple underscore au début) est la convention usuelle pour « privé ».


✏️ Exercice 6.2 — Améliorer ce code

Voici un code fonctionnel mais peu qualitatif. Refactorisez-le.

def f(L,x):
  res=[]
  for i in range(len(L)):
    if L[i]>x:
      res.append(L[i])
  if len(res)==0: return None
  return res

print(f([1,5,10,2,15,8],7))

Appliquez :

  1. Nommage PEP 8.
  2. Indentation correcte.
  3. Docstring.
  4. Compréhension au lieu de boucle.
  5. Test idiomatique du « vide ».
  6. f-string si pertinent.
def elements_superieurs_a(valeurs, seuil):
    """Renvoie les éléments strictement supérieurs au seuil.

    Arguments:
        valeurs: liste de nombres à filtrer.
        seuil: valeur plancher (exclue).

    Renvoie:
        La liste des éléments retenus, ou None si elle est vide.
    """
    retenus = [v for v in valeurs if v > seuil]
    return retenus or None


# Test
print(elements_superieurs_a([1, 5, 10, 2, 15, 8], 7))
print(elements_superieurs_a([1, 2, 3], 10))     # None
[10, 15, 8]
None

Améliorations apportées

  • Nom explicite : felements_superieurs_a.
  • Paramètres parlants : L, xvaleurs, seuil.
  • Indentation à 4 espaces (au lieu de 2).
  • Docstring avec args et retour.
  • Compréhension au lieu de for range(len) + append.
  • retenus or None : idiome Pythonique (si liste vide → None grâce au court-circuit).

Passez ce code dans Black : rien à changer. Dans Flake8 : zéro warning.


✏️ Exercice 6.3 — Rendre ce code Pythonique

Transformez ces extraits en versions Pythoniques.

# Code 1
noms = ["Alice", "Bob"]
ages = [30, 25]
for i in range(len(noms)):
    print(noms[i] + " a " + str(ages[i]) + " ans")

# Code 2
d = {"a": 1}
if "b" in d:
    x = d["b"]
else:
    x = 0

# Code 3
liste = []
for n in range(20):
    if n % 3 == 0 and n > 5:
        liste.append(n ** 2)

# Code 4
if x != None:
    if x > 0 and x < 100:
        print("OK")
# Code 1 : zip + f-string
noms = ["Alice", "Bob"]
ages = [30, 25]
for nom, age in zip(noms, ages):
    print(f"{nom} a {age} ans")

# Code 2 : dict.get
d = {"a": 1}
x = d.get("b", 0)
print(x)

# Code 3 : compréhension
liste = [n ** 2 for n in range(20) if n % 3 == 0 and n > 5]
print(liste)

# Code 4 : is not None + comparaison chaînée
x = 50
if x is not None and 0 < x < 100:
    print("OK")
Alice a 30 ans
Bob a 25 ans
0
[36, 81, 144, 225, 324]
OK

Points à retenir

  • zip pour parcourir en parallèle.
  • f-strings pour le formatage.
  • dict.get(clé, défaut) au lieu de if in ... else.
  • Compréhensions pour construire des listes.
  • x is not None (pas != None).
  • Comparaisons chaînées : 0 < x < 100 est lisible et Pythonique.

✏️ Exercice 6.4 — Détection de code non Pythonique

Pour chacun des extraits suivants, dites quel est le problème et proposez une correction.

# Extrait A
lst = [1, 2, 3]
for i in range(0, len(lst), 1):
    print(lst[i])

# Extrait B
x = 5
if x == True:
    print("ok")

# Extrait C
def f(x):
    return True if x > 0 else False

# Extrait D
for k in d.keys():
    print(d[k])

A — Usage inutile de range(len()) et du step=1.

lst = [1, 2, 3]
for x in lst:
    print(x)
1
2
3

B== True est redondant. Un test booléen suffit.

x = 5
if x:        # (ou `if bool(x):` si on veut être explicite)
    print("ok")

# Si on veut vraiment tester l'égalité stricte à True :
if x is True:
    print("c'est exactement True")
ok

C — Renvoyer un booléen en utilisant if/else est tautologique.

def f(x):
    return x > 0

print(f(5), f(-3))
True False

D — Itérer sur un dict donne déjà les clés ; d.keys() est redondant. Et pour itérer sur les valeurs, on a plus direct :

d = {"a": 1, "b": 2}

# Pour les clés
for k in d:
    print(k)

# Pour les valeurs
for v in d.values():
    print(v)

# Pour les paires
for k, v in d.items():
    print(k, v)
a
b
1
2
a 1
b 2

À retenir

Points clés du chapitre
  1. PEP 8 : 4 espaces, 79/88 caractères/ligne, nommage cohérent.
  2. Nommage : snake_case (variables/fonctions), PascalCase (classes), UPPER_SNAKE_CASE (constantes), _private (convention).
  3. is None pour comparer à None, pas == None.
  4. Black : formateur automatique sans compromis. Exécutez avant chaque commit.
  5. Flake8 / Ruff / Pylint : linters détectant bugs et violations.
  6. Ruff est 10-100× plus rapide que les outils historiques.
  7. Pythonique : compréhensions, enumerate, zip, with, dict.get, f-strings, comparaisons chaînées.
  8. Évitez range(len()), == None, == True, return True if x else False.
  9. import this pour le Zen of Python.
  10. Setup pro : pre-commit + Ruff + mypy + tests.

← Chapitre précédent : sys, os, pathlibChapitre suivant : Tests unitaires →