Leçon 08-4 — Refactoring, docstrings et README
1. Objectif
À la fin de cette leçon tu sauras : refactorer du code existant — renommer, extraire une fonction, supprimer un doublon — sans en changer le comportement, en utilisant ta suite de tests comme filet de sécurité ; écrire des docstrings utiles (quoi / paramètres / retour / erreurs) ; distinguer un commentaire qui explique un pourquoi d'un commentaire-bruit ; et écrire le README qui permet à un inconnu d'installer et d'utiliser ton projet.
2. Pourquoi c'est important
C'est la leçon qui boucle le niveau : les leçons 08-1 à 08-3 t'ont fait écrire des tests, celle-ci te fait toucher les dividendes. Refactorer sans tests, c'est marcher sur un fil sans filet — la plupart des débutants ne retouchent jamais leur code qui marche, par peur justifiée de le casser. Avec une suite verte, cette peur disparaît : tu modifies, tu relances, la suite te dit si le comportement a bougé. C'est ce cycle qui fait progresser un code réel (le premier jet n'est JAMAIS le bon découpage). Quant aux docstrings et au README : ton code sera lu par d'autres — des collègues, des recruteurs sur GitHub, et surtout toi-dans-six-mois, qui est un parfait inconnu. Un projet sans README est un projet mort à l'arrivée : personne ne devine comment le lancer.
3. Explication simple
Refactorer = changer la forme, pas le fond. Le programme fait exactement la même chose avant et après ; seuls les noms, le découpage, l'organisation changent. Le cycle, toujours le même :
- Vert — lance les tests AVANT de toucher quoi que ce soit (le filet doit être en place et intact).
- Un seul geste — renomme UNE fonction, ou extrais UNE fonction, ou supprime UN doublon. Pas trois choses à la fois.
- Vert à nouveau — relance. Vert : le geste est validé, commit. Rouge : c'est TON geste qui a cassé (dernier geste = seul suspect), répare ou annule.
La docstring = le mode d'emploi embarqué. Une chaîne entre triples guillemets, première ligne du corps de la fonction :
def cart_total(prices):
"""Return the sum of all prices in the cart."""
return sum(prices)
Elle dit ce que la fonction FAIT (pour l'appelant), pas comment elle le fait. C'est elle que help(cart_total) affiche, et que VS Code montre en survol.
Le commentaire = le pourquoi. Le code dit déjà ce qu'il fait (surtout avec les noms de la leçon 08-1). Un bon commentaire explique ce que le code ne peut pas dire : un choix, une raison, un piège.
i += 1 # incremente i ← bruit : paraphrase
total = round(total, 2) # les prix IBKR arrivent avec 6 decimales,
# on n'affiche jamais plus que le centime ← utile
Le README = la porte d'entrée. Un fichier README.md à la racine, qui répond dans l'ordre aux questions d'un nouvel arrivant : c'est quoi ? comment je l'installe ? comment je m'en sers ? à quoi ressemble le résultat ?
4. Explication approfondie
a) Pourquoi « un seul geste à la fois » est LA règle du refactoring. Si tu renommes trois fonctions, extrais un doublon ET « corriges un petit truc au passage », puis que la suite vire au rouge — le coupable est introuvable : quatre suspects, zéro certitude. Pire : le « petit truc corrigé au passage » est un changement de comportement camouflé en refactoring, et si un test le signale, tu seras tenté de « corriger le test » — détruisant ton filet. La discipline stricte : refactoring et modification de comportement sont deux activités séparées, deux commits séparés. Pendant un refactoring, un test rouge signifie TOUJOURS « j'annule ou je répare mon geste », jamais « je modifie le test ». (Un test ne se modifie pendant un refactoring que si le geste change une interface — un renommage public, cf. point b.)
b) Les trois gestes fondamentaux.
- Renommer : le plus rentable. Attention : renommer une fonction PUBLIQUE (utilisée par d'autres fichiers, dont les tests) impose de mettre à jour tous les appelants — recherche globale (Ctrl+Shift+F), puis tests. L'oubli se manifeste en
ImportError/NameErrorimmédiat (section 7-b) : brutal mais franc. - Extraire une fonction : quand un bloc de lignes fait un sous-travail nommable (souvent signalé par un commentaire
# etape 2 : ...— le nom de la fonction qui demande à naître), on le sort dans une fonction et on le remplace par un appel. C'est le remède à la fonction-tunnel de la leçon 08-1. - Supprimer un doublon : deux blocs quasi identiques = un futur bug (on corrigera l'un en oubliant l'autre). On extrait la partie commune, les différences deviennent des paramètres.
c) La docstring honnête : quoi, paramètres, retour, erreurs. Le format complet (pour les fonctions non triviales — une ligne suffit pour cart_total) :
def apply_discount(price, percent):
"""Return the price after applying a percentage discount.
Args:
price: original price (positive number).
percent: discount between 0 and 100.
Returns:
The discounted price, as a float.
Raises:
ValueError: if percent is outside [0, 100].
"""
Remarque la section Raises : c'est le contrat de la leçon 08-3, écrit noir sur blanc pour l'appelant. Règle d'or : une docstring qui répète le nom de la fonction sans rien ajouter ("""Applies discount.""") est du bruit ; une docstring fausse (pas mise à jour après un changement) est pire que pas de docstring — elle ment avec autorité. On met à jour la docstring dans le même geste que le code, ou on la supprime.
d) Le README minimal qui marche. Cinq blocs, dans cet ordre :
- Titre + une phrase : ce que fait le projet, pour qui.
- Installation : les commandes exactes, copiables (
git clone,python -m pip install pytestsi les tests en ont besoin…). - Usage : LA commande pour lancer (
python main.py), et les options. - Exemple : une session réelle copiée du terminal — c'est le bloc que tout le monde lit en premier, il vaut dix paragraphes.
- Tests (bonus qui fait sérieux) :
python -m pytest.
Le test d'acceptation d'un README : quelqu'un qui n'a jamais vu le projet peut-il le faire tourner en suivant uniquement le README, sans te poser une question ? Chaque question qu'on te pose est un trou dans le README.
Difficulté honnête : le refactoring demande du courage au début — on touche à du code qui marche, et le premier test rouge en plein geste fait peur. C'est le moment clé de tout le niveau : ce rouge-là n'est pas un échec, c'est le filet qui vient de te RATTRAPER. Sans lui, ce bug partait en production. Attends-toi aussi à un effet pervers : une fois à l'aise, tu voudras TOUT refactorer, tout le temps. Résiste : on refactore ce qu'on s'apprête à modifier ou ce qui gêne la lecture, pas le code stable qui ne dérange personne.
5. Exemples commentés
Exemple 1 — extraire une fonction (le doublon paie la facture)
Avant : deux blocs quasi identiques dans report.py :
def weekly_report(tasks):
done = 0
for task in tasks:
if task["done"]:
done += 1
return f"Semaine : {done}/{len(tasks)} taches terminees"
def monthly_report(tasks):
done = 0
for task in tasks:
if task["done"]:
done += 1
return f"Mois : {done}/{len(tasks)} taches terminees"
Cycle : tests verts d'abord, puis UN geste — extraire le comptage :
def count_done(tasks):
"""Return the number of completed tasks."""
done = 0
for task in tasks:
if task["done"]:
done += 1
return done
def weekly_report(tasks):
return f"Semaine : {count_done(tasks)}/{len(tasks)} taches terminees"
def monthly_report(tasks):
return f"Mois : {count_done(tasks)}/{len(tasks)} taches terminees"
Relance : vert (les tests de weekly_report et monthly_report n'ont pas bougé d'une ligne — le comportement non plus). Commit. Bonus immédiat : count_done est désormais testable et réutilisable seule.
Exemple 2 — le filet attrape une régression en plein refactoring
En « simplifiant » grade_to_mention (leçon 08-3), on réordonne les if — et on met le seuil le plus BAS en premier :
def grade_to_mention(grade):
if grade >= 10: # bug : 16 est aussi >= 10...
return "passable"
if grade >= 12:
return "assez bien"
...
test_grades.py F.. [100%]
================================== FAILURES ===================================
_________________________ test_grade_16_is_tres_bien __________________________
def test_grade_16_is_tres_bien():
> assert grade_to_mention(16) == "tres bien"
E AssertionError: assert 'passable' == 'tres bien'
E
E - tres bien
E + passable
test_grades.py:5: AssertionError
Dix secondes après le geste, le filet a rattrapé : 16 tombe dans le premier if rencontré. C'est LE moment que tout le niveau préparait — sans test, ce bug dormait jusqu'à la prochaine vraie note de 16. Réaction correcte : annuler ou réparer le geste (les seuils descendants : 16, 14, 12, 10). Réaction interdite : modifier le test.
Exemple 3 — la docstring et son retour sur investissement
>>> help(apply_discount)
Help on function apply_discount in module cart:
apply_discount(price, percent)
Return the price after applying a percentage discount.
Args:
price: original price (positive number).
percent: discount between 0 and 100.
Returns:
The discounted price, as a float.
Raises:
ValueError: if percent is outside [0, 100].
C'est la docstring du point 4-c, servie par help() — et par VS Code au survol de chaque appel. Écrite une fois, lue à chaque utilisation : c'est le meilleur ratio effort/service de toute la leçon.
Exemple 4 — triage de commentaires
# --- bruit : paraphrase ce que le code dit deja -------------------------
tasks = [] # cree une liste vide
done += 1 # ajoute 1 a done
return None # renvoie None
# --- utile : explique un pourquoi invisible dans le code ----------------
time.sleep(1) # the API rejects more than 1 request per second
prices = prices[:] # copy: caller's list must not be modified (level 03!)
if len(text) > 47: # 47 = 50 (max width) minus 3 for the "..." suffix
Test rapide pour trancher : masque le commentaire — si le code seul dit la même chose, supprime ; si une information disparaît (une contrainte externe, une raison, un piège de mutabilité), garde. Note aussi : les trois commentaires « utiles » vieilliront — ils décrivent des contraintes qui peuvent changer. C'est accepté : un commentaire pourquoi se relit à chaque modification du bloc qu'il annote.
Exemple 5 — un README complet et court
# Task Manager
Gestionnaire de taches en ligne de commande : ajout, completion, suppression,
avec sauvegarde automatique dans un fichier JSON.
## Installation
git clone https://github.com/jtron/task-manager.git
cd task-manager
Aucune dependance externe (Python 3.10+). Pour lancer les tests :
python -m pip install pytest
## Usage
python main.py
## Exemple
=== Task Manager ===
1. Ajouter 2. Terminer 3. Lister 4. Quitter
> 3
[x] write tests
[ ] refactor storage module
## Tests
python -m pytest
Vingt-cinq lignes, zéro blabla, et l'inconnu du test d'acceptation s'en sort seul. Remarque l'exemple : une session RÉELLE copiée du terminal, pas une promesse.
6. Erreurs fréquentes
1) Refactorer ET changer le comportement dans le même geste
Le piège capital (section 4-a). Symptôme : « pendant que j'extrayais la fonction, j'ai aussi corrigé l'arrondi ». Le test rouge est alors ambigu : régression ou correction voulue ? Un geste à la fois, un commit par geste — refactor: extract count_done PUIS fix: round totals to 2 decimals.
2) Partir refactorer sans vérifier que la suite est verte
Tu refactores, la suite est rouge... était-elle déjà rouge avant ? Impossible à dire, ton filet était troué d'entrée. TOUJOURS python -m pytest avant le premier geste : on ne refactore que sur du vert.
3) « Corriger le test » pour faire passer le refactoring
def test_grade_16_is_tres_bien():
assert grade_to_mention(16) == "passable" # « corrige » pour etre vert...
Le test de l'exemple 2 gênait, on l'a aligné sur le bug. La suite est verte et le programme est faux : tu as scié ton filet. Pendant un refactoring, le test est le juge, pas l'accusé.
4) La docstring qui ment
def cart_total(prices):
"""Return the sum of all prices, rounded to 2 decimals."""
return sum(prices) # l'arrondi a ete retire... la docstring non
Aucun outil ne signale une docstring périmée — elle trompera chaque lecteur avec assurance. Règle : le geste qui change le comportement d'une fonction inclut la mise à jour de sa docstring (et du README si l'usage change).
5) Le commentaire-bruit qui enterre le commentaire utile
total = 0 # initialise le total
for p in prices: # boucle sur les prix
total += p # ajoute le prix
total = round(total, 2) # round because floats accumulate errors (lesson 01-2)
Trois paraphrases pour une vraie information : le lecteur a désappris à lire les commentaires avant d'arriver à celui qui compte. Supprime le bruit, le signal ressort.
6) Le README aspirationnel
Un README qui documente l'export CSV « prévu pour bientôt », une commande python app.py alors que le fichier s'appelle main.py, une installation jamais re-testée depuis un clone frais. Le README décrit ce qui EST, au présent, vérifié. Méthode : rejoue toi-même chaque commande du README dans un dossier vide (clone compris) avant de le déclarer fini.
7. Lire les messages d'erreur
a) Le rouge de régression : ton meilleur ennemi. Pendant un refactoring, l'échec type est celui de l'exemple 2 :
E AssertionError: assert 'passable' == 'tres bien'
E
E - tres bien
E + passable
Lecture spéciale refactoring : peu importe le détail du diff — l'information capitale est que le comportement a bougé alors qu'il ne devait pas. Le suspect est unique : ton dernier geste. Deux issues, dans cet ordre de préférence : (1) si le geste est petit, trouve l'erreur et répare (ici, réordonner les seuils) ; (2) si tu ne vois pas en deux minutes, ANNULE (Ctrl+Z ou git checkout -- fichier — le refactoring n'était pas commité, n'est-ce pas ?) et rejoue le geste en plus petit. Ne jamais partir en débogage-fleuve sur un refactoring non commité : annuler coûte 10 secondes.
b) ImportError après un renommage : l'appelant oublié.
=================================== ERRORS ====================================
________________________ ERROR collecting test_cart.py ________________________
Traceback:
test_cart.py:1: in <module>
from cart import calc
E ImportError: cannot import name 'calc' from 'cart' (C:\...\cart.py)
Tu as renommé calc en compute_cart_total dans cart.py, mais test_cart.py (ou main.py) importe encore l'ancien nom. ERROR collecting (leçon 08-2) : rien n'a tourné. La ligne E est limpide : le nom calc n'existe plus dans cart. Correction : recherche globale de l'ancien nom, mise à jour de TOUS les appelants, relance. C'est le coût normal d'un renommage public — bruyant, immédiat, donc sans danger. (Le renommage dangereux, c'est celui qui ne casse rien de visible : une variable locale mal renommée à la main dans une fonction de 60 lignes… d'où l'intérêt des fonctions courtes.)
8. Exercices — faciles
Règles : sous-dossiers
ex-08-4-x/danslessons/level-08-tests-bonnes-pratiques/exercices/mes-reponses/. Pour chaque exercice de refactoring : suite verte AVANT, un geste, suite verte APRÈS — c'est la consigne implicite de toute la série.
a) Écris les docstrings des trois fonctions de median/average/ grade_to_mention telles que tu les as laissées à la leçon 08-3 (format complet : résumé, Args, Returns, Raises quand il y a un contrat d'erreur). Vérifie chacune avec help() dans le REPL.
b) Triage : recopie ce bloc et, pour chaque commentaire, note BRUIT (et supprime) ou UTILE (et garde), en justifiant d'un mot :
MAX_TITLE_LENGTH = 50 # longueur maximale d'un titre
def add_task(tasks, title):
title = title.strip() # enleve les espaces
if not title: # si le titre est vide
raise ValueError("task title cannot be empty")
if len(title) > MAX_TITLE_LENGTH:
title = title[:MAX_TITLE_LENGTH - 3] + "..." # -3 : place pour "..."
tasks.append({"title": title, "done": False}) # ajoute la tache
return tasks # renvoie la liste
c) Reprends grades.py et sa suite de la leçon 08-3 (suite verte). Renomme grade_to_mention en mention_from_grade PROPREMENT : recherche globale, mise à jour des appelants et des tests, suite verte. Puis provoque volontairement l'ImportError de la section 7-b (annule la mise à jour du test), lis l'erreur, re-corrige.
9. Exercices — moyens
a) Refactoring sous filet, gestes séparés. Ce module est moche mais correct. Écris d'abord sa suite de tests (nominal + limites, leçon 08-3), verte. Puis trois gestes SÉPARÉS, suite relancée entre chaque : (1) renomme tout ce qui doit l'être, (2) extrais le doublon, (3) remplace les nombres magiques par des constantes.
def f1(t):
r = []
for x in t:
if len(x) > 30:
r.append(x[:27] + "...")
else:
r.append(x)
return r
def f2(t):
r = []
for x in t:
if len(x) > 30:
r.append(x[:27] + "...")
else:
r.append(x)
return sorted(r)
(Contexte : des titres de tâches à tronquer pour l'affichage. Piège : après l'extraction, f2 doit-elle appeler f1 ou la fonction extraite ? Les deux donnent du vert — argumente ton choix dans un commentaire pourquoi.)
b) Écris le README complet de ton gestionnaire de tâches (les cinq blocs de la section 4-d), puis fais-lui passer le test d'acceptation POUR DE VRAI : clone ton propre dépôt dans un dossier temporaire, suis ton README à la lettre, commande par commande, dans un terminal frais. Chaque accroc = une correction du README, pas une explication orale.
10. Exercices — difficiles
a) Le grand ménage. Prends la PLUS LAIDE fonction de tes projets du niveau 07 (critères : > 25 lignes, ou mélange calcul/affichage résiduel, ou commentaires # etape N). Mission, dans l'ordre strict : (1) écris sa suite de tests SI elle n'existe pas — si la fonction est intestable, c'est le refactoring 08-1 d'abord (séparer calcul/affichage), avec vérification manuelle avant/après puisque sans filet ; (2) suite verte ; (3) démonte-la en gestes atomiques — une extraction par commentaire # etape, renommages, constantes — suite relancée entre CHAQUE geste ; (4) docstrings sur chaque fonction née du démontage ; (5) un commit par geste, messages refactor: ....
- Indice 1 : si tu ne sais pas par où commencer, commence par les noms — c'est le geste le moins risqué et il révèle la structure.
- Indice 2 : chaque commentaire
# etape 2 : calcul du scoreest une fonctioncompute_score(...)qui attend de naître. Le commentaire meurt, le nom le remplace. - Indice 3 : si un geste vire au rouge et que tu ne vois pas pourquoi en deux minutes :
git checkout -- fichier, et rejoue le geste en deux demi-gestes. Annuler n'est pas échouer, c'est la procédure.
11. Mini-projet lié — le projet présentable
Le mini-projet final du niveau : transforme ton MEILLEUR projet du niveau 07 en projet montrable à un inconnu (recruteur, collègue), en 45 min :
- Filet : sa suite de tests (leçons 08-2/08-3) est verte. Sinon, commence par là.
- Refactoring final : passe en revue chaque fonction avec la grille — nom PEP 8 ? une seule responsabilité ? testable ? Applique les gestes nécessaires, un par un, sous filet.
- Docstrings : chaque fonction publique a la sienne (une ligne pour les triviales, format complet pour celles qui ont des Args/Raises non évidents). Une docstring de MODULE en tête de chaque fichier (une phrase : ce que contient le module).
- Commentaires : triage complet — chaque commentaire survivant explique un pourquoi que tu peux défendre.
- README : les cinq blocs, testés par rejeu dans un dossier vide.
- Commit final et relecture du diff complet : c'est la photo avant/après de tout le niveau 08.
Critère de réussite : montre le dépôt à quelqu'un (ou relis-le à froid demain) : il doit pouvoir dire ce que fait le projet, le lancer et lancer les tests, sans une seule question. C'est le standard à maintenir pour tout projet à partir du niveau 09.
12. Correction / méthode de correction
Les solutions détaillées sont dans solutions/serie-08-4-solutions.md.
Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) sur les refactorings, compare la séquence de gestes autant que le résultat — arriver au même code par des gestes non protégés (sans relancer la suite) n'est pas une réussite ; (3) sur les docstrings et le README, compare ce que la solution a JUGÉ digne d'être dit — c'est le jugement qui s'apprend, pas la syntaxe ; (4) refais de tête l'exercice raté le lendemain.
13. À retenir
- Refactorer = changer la forme, jamais le fond. Vert → UN geste → vert → commit.
- Un test rouge en plein refactoring : le suspect est ton dernier geste. Réparer ou annuler — jamais « corriger » le test.
- Les trois gestes : renommer (recherche globale !), extraire une fonction (chaque
# etapeen cache une), supprimer un doublon (les différences deviennent des paramètres). - Docstring : quoi / Args / Returns / Raises, pour l'appelant. Une docstring fausse est pire que pas de docstring.
- Commentaire utile = un pourquoi invisible dans le code. Paraphrase = bruit à supprimer.
- README : titre-phrase, installation, usage, exemple réel, tests. Test d'acceptation : un inconnu s'en sort sans te parler.
- On refactore ce qu'on va modifier ou ce qui gêne — pas le code stable qui ne dérange personne.
14. Questions de révision
- Définis le refactoring en une phrase, et cite le cycle en trois temps.
- Pourquoi « un seul geste à la fois » ? Donne les deux problèmes causés par les gestes multiples.
- Pendant un refactoring, un test vire au rouge. Quelles sont les deux réactions correctes, et LA réaction interdite ?
- Que contient une docstring complète, et pour qui est-elle écrite ?
- Pourquoi une docstring périmée est-elle pire qu'absente ?
- Donne un exemple de commentaire-bruit et un exemple de commentaire pourquoi, de ta propre main.
- Quels sont les cinq blocs d'un README, et quel est son test d'acceptation ?
- Après un renommage, quelle erreur signale un appelant oublié, et quel geste l'évite ?
15. Checklist de compréhension
- Je sais dérouler le cycle vert → geste → vert sans qu'on me le rappelle.
- J'ai vécu au moins un test rouge en plein refactoring, et j'ai réparé ou annulé le GESTE (pas le test).
- Je sais extraire une fonction d'un doublon, différences en paramètres.
- Je sais écrire une docstring complète (Args/Returns/Raises) et la vérifier avec
help(). - Je sais justifier ou supprimer chaque commentaire d'un de mes fichiers.
- Mon meilleur projet du niveau 07 a un README qui a survécu au rejeu dans un dossier vide.
Si une case reste vide : refais les exercices de la section correspondante demain, AVANT de clore le niveau (et relis les critères de sortie du README.md du niveau).
16. Commit conseillé
git add lessons/level-08-tests-bonnes-pratiques/exercices/mes-reponses/
git commit -m "exercises: complete lesson 08-4 refactoring docstrings readme"
Puis, si c'est ta fin de session : entrée de journal + git push.