Vos docs, vos tests et votre code sont trois fichiers qui racontent la même histoire mal.

Vous mettez à jour la signature de la fonction dans le source. Vous oubliez l’exemple du README. Une semaine plus tard, un nouveau recrute copie l’extrait obsolète en production. Votre fichier de test encode encore l’ancien comportement comme résultat attendu. Maintenant vous avez deux bugs et un ticket de documentation.

C’est le problème de synchronisation. Toute codebase l’a. La plupart des équipes le résolvent avec de la discipline, ce qui échoue dès que quelqu’un est pressé.

La programmation littéraire le résout en faisant du fichier de prose la source de vérité. Vous écrivez un document Markdown que les humains lisent. Un petit outil extrait les blocs de code en fichiers Python exécutables. Les tests, l’implémentation et l’explication vivent tous au même endroit. Changez la prose, et vous changez le code.

Qu’est-ce que la programmation littéraire ?

Donald Knuth a inventé le terme en 1984. L’idée était simple : écrivez un programme comme vous écrivez un essai. Expliquez la logique en langage naturel, entrelacez le code, et laissez un outil appelé tangle extraire le source exécutable pendant que weave produit la documentation formatée.

Les outils classiques, comme le système WEB de Knuth, étaient étroitement couplés à Pascal et TeX. Ils ne se sont jamais imposés dans le développement logiciel mainstream. Le workflow semblait étranger, le tooling était lourd, et la plupart des programmeurs préféraient lire du code dans un IDE, pas dans un PDF.

Mais l’idée fondamentale reste valable. La prose et le code devraient partager une source unique de vérité. Les langages modernes et Markdown rendent cela plus facile que Knuth aurait pu l’imaginer. Vous n’avez pas besoin d’un compilateur spécial. Il vous faut environ trente lignes de Python et un parser Markdown.

Comment un seul fichier Markdown devient une suite de tests

Le mécanisme est simple. Vous écrivez un fichier .md avec des fenced code blocks. Un commentaire d’en-tête à l’intérieur de chaque bloc indique à l’extracteur à quel fichier il appartient. L’extracteur concatène les blocs ayant le même en-tête en un seul module Python. Ensuite vous exécutez pytest sur le résultat.

Voici un exemple complet. Enregistrez-le sous calculator.md :

# Calculator: addition and its properties

The `add` function is trivial. That is the point. Even trivial code deserves context about why it exists and what invariants it preserves.

```python
# calc.py
def add(a: int, b: int) -> int:
    """Return the sum of two integers."""
    return a + b

Addition commutes. We verify this explicitly because it is a property future maintainers might break by accident if they switch to a different implementation.

# test_calc.py
from calc import add

def test_addition_commutes():
    assert add(2, 3) == add(3, 2)

def test_identity():
    assert add(7, 0) == 7

Les commentaires `# calc.py` et `# test_calc.py` ne sont pas une syntaxe magique. Ce sont des conventions que notre script comprend.

## Le script d'extraction

C'est l'outil entier. Enregistrez-le sous `tangle.py` :

```python
import re
import tempfile
import subprocess
from pathlib import Path


def tangle(md_path: Path, out_dir: Path) -> Path:
    """Extract code blocks from Markdown into runnable Python files."""
    content = md_path.read_text()
    files: dict[str, list[str]] = {}

    # Extract all python code blocks
    for block in re.findall(r"```python\n(.*?)```", content, re.DOTALL):
        lines = block.strip().split("\n")

        # First line like '# filename.py' sets the target file
        if lines and lines[0].startswith("# "):
            filename = lines[0][2:].strip()
            code = lines[1:]
        else:
            filename = "module.py"
            code = lines

        files.setdefault(filename, []).extend(code)

    # Write extracted files
    out_dir.mkdir(parents=True, exist_ok=True)
    for filename, code_lines in files.items():
        (out_dir / filename).write_text("\n".join(code_lines) + "\n")

    return out_dir


if __name__ == "__main__":
    out = tangle(Path("calculator.md"), Path("build"))
    result = subprocess.run(
        ["python", "-m", "pytest", str(out), "-v"],
        capture_output=False,
    )
    raise SystemExit(result.returncode)

Exécutez-le :

$ python tangle.py
============================= test session starts ==============================
build/test_calc.py::test_addition_commutes PASSED
build/test_calc.py::test_identity PASSED
============================== 2 passed in 0.01s

Le répertoire build/ contient maintenant calc.py et test_calc.py. Vous pouvez les importer, faire du type-checking, ou les livrer comme un paquet. Le fichier Markdown est la source canonique. Tout le reste est généré.

Quand un fichier aide, et quand il nuit

Cette approche brille pour les libraries, les algorithmes et tout ce où le pourquoi compte autant que le quoi. La documentation d’API, le code de recherche et les pipelines de configuration bénéficient tous d’une prose qui reste liée à l’implémentation.

Elle ne brille pas pour le code d’application lourd en boilerplate. Une vue Django avec quinze imports de décorateurs n’a pas besoin d’un essai. Si votre module est principalement du plumbing de framework, le surcoût de la structure littéraire ajoute de la friction sans clarté.

L’autre limitation est le support des outils. Les IDEs s’attendent à trouver votre code dans des fichiers .py. Jump-to-definition, inline linting et autocomplete ont tous besoin que les fichiers générés existent avant de fonctionner. Vous pouvez résoudre cela en exécutant tangle.py comme un pre-commit hook ou comme partie de votre build step. Mais c’est une étape supplémentaire. Si votre équipe a déjà du mal avec la complexité du build, ajouter une custom extraction pipeline pourrait ne pas valoir le coup.

Des outils réels qui font cela

Le script de trente lignes ci-dessus suffit pour commencer. Si vous voulez quelque chose de production-ready, il existe des options matures.

Entangled est un outil moderne de programmation littéraire qui fonctionne avec n’importe quel langage. Il utilise une syntaxe légèrement différente, mais l’idée est identique. Il suit les dépendances entre blocs de code et supporte plusieurs fichiers de sortie.

Jupyter notebooks résolvent un problème similaire pour la data science. Ils mélangent prose, code et output dans un seul fichier. L’inconvénient est que les notebooks sont terribles pour le version control. Les diffs sont illisibles et les merge conflicts sont fréquents.

Org-mode avec Babel est l’implémentation la plus puissante. Si vous vivez déjà dans Emacs, il est imbattable. Si ce n’est pas le cas, la courbe d’apprentissage est raide.

Pour la plupart des équipes, un simple extracteur Markdown est le juste milieu pragmatique. Il utilise des outils que tout le monde comprend déjà.

FAQ

Cela fonctionne-t-il avec les type checkers ?

Oui. Générez d’abord les fichiers .py, puis exécutez mypy ou pyright contre le répertoire de build.

Et pour les langages autres que Python ?

Le script tangle.py est language-agnostic. Changez le regex de python à rust ou go et cela fonctionne de la même manière. Vous pouvez même mélanger les langages dans un seul document.

Comment gérer les imports entre blocs ?

L’extracteur concatène tous les blocs avec le même nom de fichier dans l’ordre où ils apparaissent dans le Markdown. Gardez vos imports dans le premier bloc pour ce fichier, ou répétez-les. Python gère les imports dupliqués avec grâce.

Commencez par un module

Vous n’avez pas besoin de réécrire toute votre codebase. Choisissez un petit module où la documentation semble toujours traîner derrière le code. Convertissez-le en fichier Markdown, ajoutez le script d’extraction à votre CI pipeline, et exécutez vos tests contre le output généré.

Si la documentation commence à rester à jour sans rappels, vous saurez que l’approche fonctionne. Si cela ressemble à du surcoût, abandonnez-la. La programmation littéraire est un outil, pas une religion. L’objectif n’est pas d’impressionner Knuth. L’objectif est d’arrêter de vous mentir à vous-même dans trois fichiers différents.