Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Documenter le code : commentaires qui disent pourquoi, docstrings, référence générée

Par mis à jour le

Améliorer cette page

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.

  • É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 avec pydoc, go doc ou 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.

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 total

Le 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).

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
@dataclass
class 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 total

Depuis le dossier qui contient python/, on lance le doctest :

Fenêtre de terminal
cd python
python3 -m doctest -v inventaire.py
cd ..
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:
5
Got:
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.

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.

Du code source à la référence publiée : les commentaires et docstrings alimentent un générateur qui produit du HTML ; un linter et le doctest contrôlent la même source et renvoient un échec quand la documentation manque ou mentdoc manquante : échecexemple faux : échecCode source :commentaires et docstringsGénérateur :pdoc, go doc, TypeDocRéférence HTML publiéeLinter : ruff règle D,validation TypeDocDoctest ou exemple Go :l'exemple est exécuté
Le code source est la seule source ; la référence est un produit dérivé, et deux contrôles protègent la source. Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

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/.

Fenêtre de terminal
python3 -m venv venv
./venv/bin/pip install pdoc==16.0.0
cd 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.

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 :

Fenêtre de terminal
go doc ./inventaire.Lire
package 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.

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):
pass

Installez 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) :

Fenêtre de terminal
../venv/bin/pip install ruff==0.14.9
../venv/bin/ruff check --isolated --select D sans_doc.py --output-format concise
sans_doc.py:1:1: D100 Missing docstring in public module
sans_doc.py:1:5: D103 Missing docstring in public function
sans_doc.py:5:7: D101 Missing docstring in public class
sans_doc.py:6:9: D102 Missing docstring in public method
Found 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 :

Fenêtre de terminal
npx typedoc src/index.ts --validation.notDocumented --treatValidationWarningsAsErrors

Avertir 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.

Cinq erreurs sont apparues pendant les essais. Chacune a un symptôme, une cause et une parade.

SymptômeCauseParade
ValueError: No modules found matching spec: inventairepdoc lancé depuis un autre dossier : le module n'est pas importableSe placer à côté du fichier ou passer le chemin inventaire.py
Args: texte: ... apparaît en texte courant dans le HTMLpdoc lit du Markdown par défautAjouter --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 GotL'exemple n'est plus conforme au codeCorriger le code ou l'exemple, relancer python3 -m doctest -v
Un exemple contenant \n échoue alors qu'il semble justeDans 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.

Quatre documents reliés : le README (à quoi sert le projet, comment le lancer) renvoie vers la référence générée (signatures, valeurs de retour) ; le code source, avec ses commentaires et docstrings, génère cette référence ; un commentaire du code renvoie en pointillé vers l'ADR, qui garde la décision et ses raisons.renvoie versgénère« voir ADR-0007 »READMEà quoi sert le projet, comment le lancerCode sourcecommentaires et docstringsRéférence généréesignatures, valeurs de retourADRla décision et ses raisons
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

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.

  • 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 doc et 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.notDocumented en TypeDoc font échouer le contrôle quand la documentation manque.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

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

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn