Documenter du code, c'est écrire des commentaires qui expliquent le pourquoi, des docstrings qui décrivent ce que chaque fonction promet, puis laisser un outil produire la référence à partir d'elles. Le commentaire sert celui qui modifie le code, la docstring (le texte de description placé juste sous la définition d'une fonction) sert celui qui l'appelle, et la référence générée (une page HTML construite depuis les docstrings) évite de recopier la même information à la main. Cette leçon suit un petit script Python d'inventaire de serveurs de bout en bout ; Go et TypeScript n'apparaissent que dans des onglets de variantes, à lire seulement si vous pratiquez ces langages.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Écrire un commentaire qui apporte une information absente du code.
- Rédiger une docstring Python lisible, avec un exemple qui ne peut pas mentir.
- Générer une référence HTML avec
pdoc, et lire la même documentation en local avecpydoc,go docou TypeDoc. - Faire échouer un contrôle quand une documentation manque.
- Reconnaître et corriger les cinq erreurs que les essais ont réellement produites.
Les prérequis sont ceux du niveau : éditer un fichier texte, faire un commit. Lire un script Python de vingt lignes suffit, chaque exemple est expliqué. Pour choisir entre tutoriel, guide, référence et explication, voir Choisir le bon document.
Que doit dire un commentaire ?
Section intitulée « Que doit dire un commentaire ? »Un commentaire utile apporte ce que le code ne peut pas dire de lui-même : le pourquoi d'un choix, une contrainte extérieure, ou un lien vers le ticket ou la décision d'architecture qui l'explique. Le code dit déjà comment. Un commentaire qui répète le comment fait doubler la maintenance : la prochaine modification devra corriger deux endroits.
Voici la fonction cpu_total de l'exemple, qui additionne les cœurs d'un
environnement. Le premier commentaire paraphrase, le second justifie.
def cpu_total(serveurs: list[Serveur], environnement: str) -> int: # Mauvais commentaire (il paraphrase le code) : # on parcourt les serveurs et on additionne les cpu total = 0 for s in serveurs: # Bon commentaire (il dit pourquoi) : la comparaison est stricte, # "Prod" et "prod" sont deux environnements, car l'inventaire # source distingue la casse et on ne veut pas masquer une faute. if s.environnement == environnement: total += s.cpu return totalLe premier commentaire se supprime sans rien perdre : la boucle se lit
toute seule. Le second protège une décision. Sans lui, un développeur
bien intentionné ajouterait .lower() et masquerait des fautes de saisie.
Quand le motif est une décision documentée, un renvoi suffit, par exemple
# Voir ADR-0007 (lien vers une décision d'architecture).
Trois défauts reviennent presque toujours :
- Paraphraser : le commentaire répète l'instruction qui le suit.
- Garder du code mort : des lignes mises en commentaire « au cas où ». Git conserve l'historique, le fichier n'a pas à le faire.
- Mentir après modification : le code a changé, le commentaire non. Il est alors pire qu'absent, car le lecteur lui fait confiance.
Le remède au troisième défaut est de commenter peu, mais juste, et de relire le commentaire dans la même revue que la ligne qu'il décrit.
Qu'est-ce qu'une docstring et à quoi ressemble une bonne ?
Section intitulée « Qu'est-ce qu'une docstring et à quoi ressemble une bonne ? »Une docstring est une chaîne de caractères placée juste sous la
définition d'un module, d'une classe ou d'une fonction. Python la range dans
l'attribut __doc__, ce qui permet à des outils de la lire. La
leçon sur les fonctions Python
en montre la syntaxe de base ; ici on s'intéresse à son contenu.
La convention officielle, la PEP 257 (une proposition d'évolution de Python, ici consacrée aux docstrings), demande une première ligne de résumé qui tient sur une ligne et se termine par un point, suivie d'une ligne vide, puis du détail. Cette première ligne est ce que les outils affichent dans les listes : elle doit se comprendre seule.
Pour le détail, on retient le style Google : des sections nommées
Args:, Returns:, Raises: et Example:. Il reste lisible dans l'éditeur
et la plupart des outils savent l'interpréter.
def lire_inventaire(texte: str) -> list[Serveur]: r"""Transforme un texte d'inventaire en liste de serveurs.
Chaque ligne a la forme ``nom:environnement:cpu``. Les lignes vides et celles qui commencent par ``#`` sont ignorées.
Args: texte: Le contenu brut de l'inventaire.
Returns: La liste des serveurs, dans l'ordre du texte.
Raises: ValueError: Si une ligne n'a pas exactement trois champs.
Example: >>> lire_inventaire("web01:prod:4\n# commentaire\ndb01:prod:8") [Serveur(nom='web01', environnement='prod', cpu=4), Serveur(nom='db01', environnement='prod', cpu=8)] """Le résumé dit ce que fait la fonction. Args décrit chaque paramètre sans
répéter son type, que l'annotation str porte déjà. Raises annonce
l'erreur que l'appelant doit prévoir. Le r devant les guillemets rend la
docstring brute : sans lui, Python interpréterait le \n avant que
l'exemple ne soit lu (voir le diagnostic plus bas).
Comment prouver qu'un exemple dit vrai ?
Section intitulée « Comment prouver qu'un exemple dit vrai ? »Un exemple écrit à la main finit par devenir faux. Le module standard
doctest supprime ce risque : il repère les lignes qui commencent par
>>> dans les docstrings, les exécute et compare le résultat à la ligne
suivante.
Les deux extraits vus plus haut viennent du module inventaire.py, à créer
dans un dossier python/. Toute la leçon part d'un dossier de travail qui
contient python/ ; le dossier venv/ créé plus bas se place à côté de lui.
Voici le fichier complet, avec la classe Serveur
(une dataclass, c'est-à-dire une classe qui ne sert qu'à porter des
données) que les extraits utilisent :
"""Outils d'inventaire de serveurs.
Ce module lit un inventaire sous forme de lignes ``nom:environnement:cpu``et en tire des informations utiles à l'exploitation.
Exemple: >>> lire_inventaire("web01:prod:4") [Serveur(nom='web01', environnement='prod', cpu=4)]"""
from dataclasses import dataclass
@dataclassclass Serveur: """Un serveur de l'inventaire.
Attributes: nom: Nom court de la machine, par exemple ``web01``. environnement: ``prod``, ``recette`` ou ``dev``. cpu: Nombre de coeurs alloués. """
nom: str environnement: str cpu: int
def lire_inventaire(texte: str) -> list[Serveur]: r"""Transforme un texte d'inventaire en liste de serveurs.
Chaque ligne a la forme ``nom:environnement:cpu``. Les lignes vides et celles qui commencent par ``#`` sont ignorées.
Args: texte: Le contenu brut de l'inventaire.
Returns: La liste des serveurs, dans l'ordre du texte.
Raises: ValueError: Si une ligne n'a pas exactement trois champs.
Example: >>> lire_inventaire("web01:prod:4\n# commentaire\ndb01:prod:8") [Serveur(nom='web01', environnement='prod', cpu=4), Serveur(nom='db01', environnement='prod', cpu=8)] """ serveurs = [] for ligne in texte.splitlines(): ligne = ligne.strip() if not ligne or ligne.startswith("#"): continue champs = ligne.split(":") if len(champs) != 3: raise ValueError(f"ligne invalide : {ligne!r}") serveurs.append(Serveur(champs[0], champs[1], int(champs[2]))) return serveurs
def cpu_total(serveurs: list[Serveur], environnement: str) -> int: """Additionne les coeurs d'un environnement.
Args: serveurs: Les serveurs à compter. environnement: L'environnement visé, par exemple ``prod``.
Returns: Le total de coeurs, ou 0 si aucun serveur ne correspond.
Example: >>> parc = [Serveur("a", "prod", 4), Serveur("b", "dev", 2)] >>> cpu_total(parc, "prod") 4 >>> cpu_total(parc, "recette") 0 """ # Mauvais commentaire (il paraphrase le code) : # on parcourt les serveurs et on additionne les cpu total = 0 for s in serveurs: # Bon commentaire (il dit pourquoi) : la comparaison est stricte, # "Prod" et "prod" sont deux environnements, car l'inventaire # source distingue la casse et on ne veut pas masquer une faute. if s.environnement == environnement: total += s.cpu return totalDepuis le dossier qui contient python/, on lance le doctest :
cd pythonpython3 -m doctest -v inventaire.pycd ..5 tests in 7 items.5 passed and 0 failed.Test passed.Sans -v, la commande reste silencieuse et rend le code 0 quand tout passe,
ce qui convient à une CI (l'exécution automatique des contrôles à chaque
commit). Si un exemple devient faux, par exemple si l'on
attend 5 au lieu de 4, le doctest le dit précisément (extrait de la
sortie, qui précède aussi ce bloc d'une ligne File et d'un trait d'étoiles) :
Failed example: cpu_total(parc, "prod")Expected: 5Got: 4***Test Failed*** 1 failures.La sortie compare ce qui était attendu (Expected) à ce qui a été
obtenu (Got). Soit la docstring est fausse, soit le code a changé : dans
les deux cas, la documentation ne ment plus en silence. Pour des tests
complets, voir unittest ;
doctest ne remplace pas une suite de tests, il garantit que les exemples
affichés fonctionnent.
Comment générer la référence ?
Section intitulée « Comment générer la référence ? »La référence générée est une page HTML construite directement depuis les docstrings : elle ne peut pas dériver du code, puisqu'elle en vient. Dans le cadre Diátaxis, c'est le type « référence » (voir la leçon sur le choix du type de document), qu'on consulte pour un fait précis, sans la lire de bout en bout.
Regardez le schéma : le code source alimente un générateur qui produit le HTML ; en parallèle, un linter (un outil qui lit le code sans l'exécuter et signale ce qui s'écarte des règles) et le doctest contrôlent la même source et renvoient une erreur à l'auteur quand quelque chose manque ou ment.
Le geste principal de cette leçon utilise pdoc, un générateur Python
simple : il ne demande aucun fichier de configuration. On l'installe dans un
environnement virtuel (un dossier venv/ qui isole les paquets du projet de
ceux du système, voir
environnements virtuels Python)
avec la version épinglée. Les commandes se lancent depuis le dossier de
travail, celui qui contient python/.
python3 -m venv venv./venv/bin/pip install pdoc==16.0.0cd python../venv/bin/pdoc --docformat google inventaire.py -o build/La commande ne produit aucune sortie et rend 0. Le dossier build/ contient
inventaire.html (la référence du module), index.html et search.js, une
recherche intégrée. pdoc ajoute aussi le code source de chaque fonction avec
des ancres. Le dossier se publie tel quel sur n'importe quel hébergement statique.
L'option --docformat google est indispensable : pdoc lit les docstrings en
Markdown par défaut, et sans elle les sections Args: et Returns: sortent
à plat dans le texte courant. Avec l'option, elles deviennent des rubriques
« Arguments », « Returns » et « Raises ».
Pour un gros projet (plusieurs paquets, pages narratives, plusieurs
versions publiées), Sphinx avec les extensions sphinx.ext.autodoc et
sphinx.ext.napoleon est l'outil habituel. Testé avec Sphinx 9.1.0 :
sphinx-quickstart, deux lignes dans conf.py (le chemin du module et la
liste des extensions), une directive automodule dans index.rst, puis
sphinx-build -b html. Le résultat est plus riche mais demande plus de
fichiers à maintenir ; napoleon ne reconnaît par défaut que les titres
anglais (Example: oui, Exemple: non). Le guide
Documentations versionnées avec Sphinx
détaille ce cas.
Et en Go ou en TypeScript ?
Section intitulée « Et en Go ou en TypeScript ? »Le principe est identique : un commentaire placé juste au-dessus du symbole
devient la documentation. Seule la convention change. Les onglets ci-dessous
montrent la même fonction CPUTotal ou cpuTotal, sans qu'il faille
connaître le langage. Choisissez celui que vous pratiquez : le reste de la
leçon se suit sans eux.
En Go, le commentaire d'un symbole exporté commence par son nom, et un
commentaire placé avant package documente le paquet. La syntaxe est
décrite dans Go Doc Comments.
// CPUTotal additionne les coeurs des serveurs d'un environnement.// Il renvoie 0 si aucun serveur ne correspond.func CPUTotal(serveurs []Serveur, environnement string) int {L'outil go doc est fourni avec Go et lit ces commentaires. Pour
l'essayer, dans un dossier séparé du dossier python/, créez un
module Go (go mod init lab-inventaire) et le fichier inventaire/inventaire.go :
// Package inventaire lit un inventaire de serveurs au format// "nom:environnement:cpu".package inventaire
import ( "fmt" "strconv" "strings")
// Serveur représente une machine de l'inventaire.type Serveur struct { Nom string Environnement string CPU int}
// ErrLigneInvalide est renvoyée par [Lire] quand une ligne n'a pas// exactement trois champs.var ErrLigneInvalide = fmt.Errorf("ligne invalide")
// Lire transforme un texte d'inventaire en liste de serveurs.//// Les lignes vides et celles qui commencent par "#" sont ignorées.// Si une ligne est mal formée, Lire renvoie [ErrLigneInvalide].func Lire(texte string) ([]Serveur, error) { var serveurs []Serveur for _, ligne := range strings.Split(texte, "\n") { ligne = strings.TrimSpace(ligne) if ligne == "" || strings.HasPrefix(ligne, "#") { continue } champs := strings.Split(ligne, ":") if len(champs) != 3 { return nil, fmt.Errorf("%w : %q", ErrLigneInvalide, ligne) } cpu, err := strconv.Atoi(champs[2]) if err != nil { return nil, err } serveurs = append(serveurs, Serveur{champs[0], champs[1], cpu}) } return serveurs, nil}
// CPUTotal additionne les coeurs des serveurs d'un environnement.// Il renvoie 0 si aucun serveur ne correspond.func CPUTotal(serveurs []Serveur, environnement string) int { total := 0 for _, s := range serveurs { if s.Environnement == environnement { total += s.CPU } } return total}Depuis la racine du module :
go doc ./inventaire.Lirepackage inventaire // import "lab-inventaire/inventaire"
func Lire(texte string) ([]Serveur, error) Lire transforme un texte d'inventaire en liste de serveurs.
Les lignes vides et celles qui commencent par "#" sont ignorées. Si une ligne est mal formée, Lire renvoie ErrLigneInvalide.Go n'a pas de sections Args: : la prose décrit les paramètres et les
erreurs. Un exemple placé dans un fichier example_test.go, avec un
commentaire // Output:, est exécuté par go test comme un test, donc
c'est l'équivalent du doctest. go doc ne l'affiche pas dans le terminal.
En TypeScript, le commentaire est un bloc /** ... */ avec des balises
@param, @returns et @example.
/** * Additionne les coeurs d'un environnement. * * @param serveurs - Les serveurs à compter. * @param environnement - L'environnement visé, par exemple `prod`. * @returns Le total de coeurs, ou 0 si aucun serveur ne correspond. */export function cpuTotal(serveurs: Serveur[], environnement: string): number {Dans un autre dossier de projet, le fichier complet src/index.ts, avec l'interface Serveur et une
fonction volontairement non documentée, sansDoc, que la validation
signalera plus bas :
/** Un serveur de l'inventaire. */export interface Serveur { /** Nom court de la machine, par exemple `web01`. */ nom: string; /** `prod`, `recette` ou `dev`. */ environnement: string; /** Nombre de coeurs alloués. */ cpu: number;}
/** * Additionne les coeurs d'un environnement. * * @param serveurs - Les serveurs à compter. * @param environnement - L'environnement visé, par exemple `prod`. * @returns Le total de coeurs, ou 0 si aucun serveur ne correspond. */export function cpuTotal(serveurs: Serveur[], environnement: string): number { return serveurs .filter((s) => s.environnement === environnement) .reduce((somme, s) => somme + s.cpu, 0);}
export function sansDoc(x: number): number { return x;}Le générateur est TypeDoc. Les deux versions sont épinglées :
npm install --save-dev typedoc@0.28.20 typescript@6.0.3npx typedoc src/index.ts[info] html generated at ./docsLes types des paramètres sont repris du code TypeScript, on ne les répète donc pas dans le commentaire.
Comment faire respecter la documentation ?
Section intitulée « Comment faire respecter la documentation ? »Une règle que personne ne vérifie s'érode. Le contrôle doit échouer, comme un
test, quand une docstring manque. Pour Python, ruff inclut la famille de
règles D (reprise de pydocstyle). Sans configuration, ruff signale chaque
absence. Créez d'abord, à côté de inventaire.py (dans python/), un contre-exemple
sans_doc.py sans aucune docstring :
def f(x): return x
class C: def m(self): passInstallez ruff dans le même environnement virtuel que pdoc, puis
lancez-le depuis python/. L'option --isolated ignore toute configuration
existante, et ruff ajoute en tête deux lignes warning: qui préviennent que D203 et D211,
D212 et D213 sont incompatibles (voir plus bas) :
../venv/bin/pip install ruff==0.14.9../venv/bin/ruff check --isolated --select D sans_doc.py --output-format concisesans_doc.py:1:1: D100 Missing docstring in public modulesans_doc.py:1:5: D103 Missing docstring in public functionsans_doc.py:5:7: D101 Missing docstring in public classsans_doc.py:6:9: D102 Missing docstring in public methodFound 4 errors.Le code retour est 1, ce qui fait échouer une CI. Mais --select D seul active
des règles incompatibles entre elles (D203 et D211, D212 et D213) et des
règles de forme qui n'ont pas de sens ici. On obtient 4 erreurs de
bruit sur inventaire.py (trois D413, une ligne vide manquante après la
dernière section, et D401, qui exige un résumé à l'impératif, ce qui ne
convient pas à des docstrings françaises). Il faut donc choisir une
convention, dans un fichier ruff.toml placé à côté des modules :
[lint]select = ["D"]
[lint.pydocstyle]convention = "google"Avec ce fichier (testé avec ruff 0.14.9), ../venv/bin/ruff check inventaire.py affiche
All checks passed! et ../venv/bin/ruff check sans_doc.py signale 4 erreurs, avec le
code retour 1. Le guide
Ruff explique le reste de la
configuration.
Côté TypeScript, TypeDoc sait lui aussi s'arrêter :
npx typedoc src/index.ts --validation.notDocumented --treatValidationWarningsAsErrorsAvertir sur un symbole sans documentation (sansDoc ... does not have any documentation) ne coûte rien, mais sans le second drapeau le code retour
reste 0. Avec lui, il vaut 4, et la CI échoue. Le branchement de ces
contrôles dans un pipeline est approfondi au niveau suivant de la formation.
Que faire quand ça échoue ?
Section intitulée « Que faire quand ça échoue ? »Cinq erreurs sont apparues pendant les essais. Chacune a un symptôme, une cause et une parade.
| Symptôme | Cause | Parade |
|---|---|---|
ValueError: No modules found matching spec: inventaire | pdoc lancé depuis un autre dossier : le module n'est pas importable | Se placer à côté du fichier ou passer le chemin inventaire.py |
Args: texte: ... apparaît en texte courant dans le HTML | pdoc lit du Markdown par défaut | Ajouter --docformat google |
Avertissement ERESOLVE de npm (observé avec npm install typescript@7.0.2 --dry-run) | Au 13 octobre 2026, npm view typescript version donne la 7.0.2, que TypeDoc 0.28.20 ne déclare pas compatible (versions 5.0 à 5.9 et 6.0.x) | Épingler typescript@6.0.3 |
Test Failed avec Expected et Got | L'exemple n'est plus conforme au code | Corriger le code ou l'exemple, relancer python3 -m doctest -v |
Un exemple contenant \n échoue alors qu'il semble juste | Dans une docstring ordinaire, Python interprète \n avant le doctest, qui voit un vrai saut de ligne | Écrire la docstring brute r"""...""", comme plus haut ; \\n fonctionne mais ruff le signale (D301) |
Deux limites s'ajoutent à garder en tête. Sphinx exige de déclarer le chemin
du module dans conf.py (sys.path.insert), sinon il ne trouve pas le code.
Et go doc ne retrouve un symbole non exporté qu'avec l'option -u.
Comment cela s'articule avec le reste de la documentation ?
Section intitulée « Comment cela s'articule avec le reste de la documentation ? »Chaque document a son rôle. Le README est l'entrée du dépôt : il dit à quoi sert le projet et comment le lancer (README et changelog). Les ADR gardent les décisions que les commentaires ne font que pointer (ADR). La référence générée répond à « quelle est la signature exacte, que renvoie cette fonction ? ». Chacun s'appuie sur les autres sans les répéter : le README renvoie vers la référence, le commentaire vers l'ADR.
Le schéma résume ces trois renvois : le README pointe vers la référence, le code la génère, et un commentaire renvoie vers l'ADR qui garde la raison du choix.
Pour garder ces documents à jour, le principe est de les traiter comme du code, avec relecture et contrôle automatique, ce que détaille Documentation as Code.
À retenir
Section intitulée « À retenir »- Un commentaire dit le pourquoi, la contrainte ou le lien vers l'ADR ; il ne paraphrase pas le code, ne conserve pas de code mort et se corrige avec la ligne qu'il décrit.
- Une docstring Python suit la PEP 257 (résumé d'une ligne, ligne vide,
détail) ; le style Google ajoute
Args,Returns,Raises,Example. - Le doctest (
python3 -m doctest -v) exécute les exemples de la docstring : un exemple faux fait échouer la commande. - pdoc (
--docformat google, version épinglée, module importable) génère la référence HTML ;go docet TypeDoc jouent le même rôle en Go et en TypeScript. - Une règle non vérifiée s'érode : ruff avec
convention = "google"ou--validation.notDocumenteden TypeDoc font échouer le contrôle quand la documentation manque.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Écrire pour les IA : GEO, structure citable et llms.txt
Continuer le cours : README et CHANGELOG : l'entrée et l'historique d'un dépôt
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications