À quoi ça sert

En théorie, on devrait toujours lancer Ruff, pytest, ou un check de formatage avant de pousser du code. En pratique, on oublie. La CI/CD finit par attraper le problème, mais on a déjà perdu 5 minutes à attendre les jobs GitHub et tout le monde voit passer le « fix lint » dans l'historique.

Pre-commit règle ça en plaçant un hook Git qui intercepte la commande git commit : il lance tes vérifications, et si l'une échoue, le commit est bloqué. Tu corriges, tu re-commit, ça passe. La CI ne voit jamais de problème de format.

  • Lancer Ruff / Black / isort sur les fichiers Python modifiés.
  • Vérifier le YAML, le TOML, le JSON de config (pas de fichier mal formé qui casse le build).
  • Bloquer les secrets committés par erreur (clés API, mots de passe…) avec detect-secrets.
  • Lancer pytest sur les tests rapides avant un commit important.
  • Auto-corriger ce qui est corrigible : trailing whitespace, fins de ligne, ordre des imports…
Hook Git ? C'est quoi ?

Un hook Git est un script que Git exécute à un moment précis : avant un commit (pre-commit), avant un push (pre-push), etc. Ils existent depuis toujours dans Git, mais sont relous à gérer à la main (un script par hook, par machine, par projet…). L'outil pre-commit (l'outil, à ne pas confondre avec le hook du même nom) industrialise tout ça : une config YAML versionnée dans le repo, un partage automatique entre l'équipe.

Un exemple d'usage

Tu actives Ruff (lint + format) sur ton projet. Tu modifies un fichier avec un import inutilisé et des espaces de fin de ligne, tu commits :

bash
git commit -m "feat: nouvelle route"

ruff (lint).............................................................Failed
- hook id: ruff
- exit code: 1
  src/api.py:3:1: F401 [*] `os` imported but unused
  Found 1 error.
trim trailing whitespace................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
  Fixing src/api.py

Le commit a été bloqué. Ruff a signalé l'import inutile, le hook trailing-whitespace a même corrigé le fichier tout seul. Tu corriges l'import, tu re-stage les changements, tu re-commit — cette fois tous les hooks passent et le commit est créé.

How-to : installer et utiliser pre-commit

  1. Installer l'outil

    pre-commit est un outil en ligne de commande, indépendant de ton projet (comme Ruff ou UV). On l'installe globalement avec uv tool :

    bash
    uv tool install pre-commit
    
    # Vérifier
    pre-commit --version

    Tu peux aussi l'ajouter comme dépendance dev de ton projet (uv add --dev pre-commit) si tu préfères qu'il soit installé automatiquement par uv sync.

  2. Créer le fichier de config

    Toute la configuration tient dans un fichier .pre-commit-config.yaml à la racine du repo. On y liste les repos de hooks à utiliser et leurs versions :

    yaml (.pre-commit-config.yaml)
    repos:
      - repo: https://github.com/pre-commit/pre-commit-hooks
        rev: v5.0.0
        hooks:
          - id: trailing-whitespace
          - id: end-of-file-fixer
          - id: check-yaml
          - id: check-added-large-files
    
      - repo: https://github.com/astral-sh/ruff-pre-commit
        rev: v0.8.0
        hooks:
          - id: ruff
            args: [--fix]
          - id: ruff-format

    Chaque repo est un dépôt Git contenant un ou plusieurs hooks. rev fige une version pour la reproductibilité (toute l'équipe utilise exactement la même).

  3. Installer les hooks dans ton repo

    Une seule fois par clone du repo, après avoir créé le YAML :

    bash
    pre-commit install

    Ça crée le fichier .git/hooks/pre-commit qui appelle l'outil. À partir de là, chaque git commit dans ce repo va lancer les hooks définis dans ton YAML.

    Et pour les nouveaux clones ?

    Le .pre-commit-config.yaml est versionné dans Git, mais le hook .git/hooks/pre-commit, non (Git n'inclut jamais son propre dossier). Chaque personne qui clone doit lancer pre-commit install une fois. C'est une bonne ligne à mettre dans le README.

  4. Lancer manuellement (sans commiter)

    Utile la première fois ou après avoir ajouté un hook, pour corriger d'un coup tout le code existant :

    bash
    # Sur les fichiers modifiés (= staged), comme pendant un commit
    pre-commit run
    
    # Sur TOUT le repo
    pre-commit run --all-files
    
    # Un seul hook
    pre-commit run ruff --all-files
  5. Mettre à jour les versions des hooks

    Les hooks listés dans le YAML sont figés à une version. Pour les passer aux dernières versions stables :

    bash
    pre-commit autoupdate

    Ça réécrit les rev: dans ton YAML. Tu commits le fichier modifié et toute l'équipe récupère les nouvelles versions au prochain pull.

  6. Quelques hooks utiles

    Le dépôt pre-commit-hooks officiel contient une collection de checks génériques très pratiques :

    • trailing-whitespace — enlève les espaces de fin de ligne.
    • end-of-file-fixer — assure une seule ligne vide à la fin du fichier.
    • check-yaml / check-toml / check-json — valide la syntaxe.
    • check-added-large-files — bloque les fichiers trop gros (modèles, datasets accidentels).
    • check-merge-conflict — détecte les <<<< oubliés.
    • detect-private-key — empêche de commiter une clé SSH par erreur.

    Pour Python, les incontournables :

    • ruff + ruff-format — lint et formatage (cf. Ruff).
    • mypy — vérification statique des types.
    • detect-secrets — bloque les mots de passe et tokens.
  7. Contourner ponctuellement (à utiliser avec parcimonie)

    Si tu dois absolument commiter sans déclencher les hooks (urgence, WIP local) :

    bash
    git commit --no-verify -m "WIP"
    Pourquoi pas systématiquement ?

    --no-verify contourne tous les hooks Git, y compris ceux qui vérifient les messages de commit ou la signature. Si tu te retrouves à l'utiliser souvent, c'est soit que tes hooks sont trop lents (réfléchir à ce qu'on lance pré-commit vs pré-push), soit qu'ils râlent à tort (ajuster la config).

  8. L'intégrer aussi en CI

    Même avec pre-commit installé localement, on relance les hooks dans la CI : ça protège contre les git commit --no-verify et les contributeurs qui n'ont pas pre-commit installé. Étape type dans un workflow GitHub Actions :

    yaml (.github/workflows/ci.yml)
    - uses: actions/setup-python@v5
      with:
        python-version: "3.12"
    - uses: pre-commit/action@v3.0.1

Aide-mémoire

bash (commandes)
uv tool install pre-commit
pre-commit install              # active les hooks Git
pre-commit run --all-files       # lance sur tout
pre-commit run <hook-id>         # un seul hook
pre-commit autoupdate            # bumper les versions
yaml (squelette minimal)
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

pre-commit et le reste de l'écosystème

  • Ruff — c'est le hook que tu installes en premier. Lint + formatage Python sur les fichiers modifiés, en quelques millisecondes.
  • pytest — on peut lancer les tests rapides en pre-commit, mais en général c'est trop lent pour bloquer chaque commit. Préfère un hook pre-push ou la CI/CD.
  • CI/CD — pre-commit local et CI sont complémentaires : le local évite les allers-retours, la CI est le filet de sécurité officiel. On y rejoue les mêmes hooks pour cohérence.
  • GitHub / GitLab — les actions officielles pre-commit/action facilitent l'intégration en CI. Sur GitLab CI, on appelle directement pre-commit run --all-files dans un job.
  • docstring — on peut ajouter un hook pydocstyle pour vérifier la présence et le format des docstrings.
Garder les hooks rapides

Un commit ne devrait pas attendre plus de 2-3 secondes. Si tes hooks dépassent ce seuil, tu vas finir par les contourner. Garde en pre-commit les checks rapides (lint, format), et bascule les checks lents (tests d'intégration, build Docker) en pre-push ou en CI.

Pour aller plus loin