Vous connaissez certainement des outils de qualité de code comme Flake8 pour repérer les erreurs, Black pour le formatage et isort pour les imports.
Vous en avez certainement entendu parler, mais il existe un outil qui permet de faire tout ça (et même plus) entre 10 et 100 fois plus rapidement : Ruff. Si vous avez lu notre article sur uv, l'histoire va vous sembler familière : même éditeur, même langage, même philosophie.
Documentation de Ruff
Qu'est-ce que Ruff ?
Ruff est à la fois un linter et un formateur de code Python écrit en Rust et développé par Astral, les créateurs de uv.
Petit rappel de vocabulaire :
-
Un linter analyse votre code sans l'exécuter pour repérer les erreurs, les imports inutiles, les variables inutilisées ou les entorses aux conventions
-
Un formateur réécrit votre code pour lui donner une présentation homogène
L'idée derrière Ruff est de remplacer toute une pile d'outils derrière une seule interface.
Pourquoi utiliser Ruff ?
Trois arguments suffisent à comprendre l'engouement :
-
La vitesse. Ruff annonce être 10 à 100 fois plus rapide que les outils existants comme Flake8 ou Black. Sur une grosse base de code on passe facilement de plusieurs secondes à une fraction de seconde
-
Un seul outil au lieu de dix. Plus besoin de jongler entre Flake8, Black, isort ou autre avec chacun son fichier de configuration. Ruff centralise tout
-
La correction automatique. En plus de pointer les problèmes, Ruff permet de corriger des problèmes tout seul grâce à l'option
--fix
Installer Ruff
Vous pouvez installer Ruff via uv, pip, pipx ou bien encore d'autres gestionnaires. La liste complète est disponible ici.
Vous pouvez l'installer globalement sur votre ordinateur :
uv tool install ruff
L'ajouter comme dépendance à un projet :
# Avec uv uv add ruff # Via pip pip install ruff
Le linter
ruff check est la commande principale du linter. Prenons un fichier tout simple avec un import inutilisé :
import os # cet import ne sert à rien ici, c'est volontaire def saluer(nom): return f"Bonjour {nom}" print(saluer("Patrick"))
Si je lance le linter sur le dossier courant de cette manière :
ruff check
Ruff a identifié le coupable 👮♂️ :
F401 [*] `os` imported but unused --> main.py:1:8 | 1 | import os # cet import ne sert à rien ici, c'est volontaire | ^^ 2 | 3 | def saluer(nom): | help: Remove unused import: `os` Found 1 error. [*] 1 fixable with the `--fix` option.
F401 est le code de l'erreur. La lettre F indique qu'elle vient de Pyflakes (le composant qui identifie les erreurs de logique comme les imports inutiles), et 401 désigne la règle au sein de cette famille. À la ligne 2, l'import os n'est pas utilisé. Le petit [*] nous indique que Ruff sait corriger l'erreur tout seul, alors profitons-en pour le faire ☺️ :
ruff check --fix # Résultat : Found 1 error (1 fixed, 0 remaining).
L'import a maintenant disparu de votre fichier.
Sans argument, ruff check analyse le dossier courant en descendant automatiquement dans les sous-dossiers. Mais vous pouvez tout aussi bien cibler un dossier ou un fichier en particulier :
ruff check src/ # Lint tous les fichiers de src/ (et ses sous-dossiers) ruff check saluer.py # Lint uniquement ce fichier
Ruff distingue deux types de corrections : les corrections sûres (safe) et risquées (unsafe). Par défaut, seules les corrections sûres sont appliquées. Les corrections risquées, celles qui peuvent changer le comportement de votre code, nécessitent d'ajouter le drapeau --unsafe-fixes.
Voici un exemple concret de code qui n'est pas optimisé :
premier = list(range(99999999))[0]
La règle RUF015 repère ce genre de code non optimisé. Les règles propres à Ruff ne sont pas activées par défaut ; il faut donc activer les règles Ruff :
ruff check --select RUF
RUF015 Prefer `next(iter(range(99999999)))` over single element slice --> main.py:1:11 | 1 | premier = list(range(99999999))[0] | ^^^^^^^^^^^^^^^^^^^^^^^^ | help: Replace with `next(iter(range(99999999)))` Found 1 error. No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
Ruff propose de remplacer le code par next(iter(range(99999999))), bien plus rapide puisqu'on évite de construire toute une liste juste pour en récupérer le premier élément.
Mais la correction est jugée risquée : sur une collection vide, list(...)[0] lève une IndexError, alors que next(iter(...)) lève une StopIteration. C'est un comportement différent.
À noter
Il existe une option bien pratique qui relance l'analyse automatiquement à chaque modification : ruff check --watch.
Le formateur
Nous avons vu comment repérer les erreurs ; maintenant, occupons-nous de la présentation du code. Prenons un code un peu négligé avec des espaces mal placés, des guillemets qui ne sont pas homogènes et des parenthèses superflues (oui, je sais, j'ai envie de faire n'importe quoi).
def saluer(nom, message = 'Bonjour'): resultat = message+", "+nom+' !' return( resultat ) utilisateurs = [ "Patrick","Sebastien" ] for u in utilisateurs: print(saluer(u))
Si je lance la commande :
ruff format
Résultat, un code propre :
def saluer(nom, message="Bonjour"): resultat = message + ", " + nom + " !" return resultat utilisateurs = ["Patrick", "Sebastien"] for u in utilisateurs: print(saluer(u))
Ruff a homogénéisé les guillemets (doubles par défaut), ajouté les espaces manquants autour des opérateurs, supprimé les parenthèses autour du return et même ajouté deux lignes vides attendues par la PEP 8 après notre fonction.
À noter
Vous pouvez vérifier le format de votre code sans le modifier avec la commande ruff format --check.
Configurer Ruff
Ruff peut se configurer dans deux fichiers : soit directement dans le pyproject.toml de votre projet s'il existe, soit dans un fichier dédié ruff.toml à la racine de votre projet.
Le pyproject.toml est un fichier partagé par tout l'écosystème Python : Ruff ne fait que s'y incruster 😛. Vous pouvez retrouver des informations sur ce fichier dans notre article sur uv. Chaque outil doit donc préfixer sa configuration pour être reconnu via une section [tool.XXX]. C'est pour cette raison que, concernant Ruff, tout commence par [tool.ruff].
En revanche, dans le fichier ruff.toml, le fichier n'appartient qu'à Ruff. Ce préfixe est alors inutile.
Voici un exemple minimal avec un pyproject.toml :
# pyproject.toml [tool.ruff] # Longueur de ligne line-length = 88 # Version de Python ciblée target-version = "py310" [tool.ruff.lint] # Les règles que l'on active # B = flake8-bugbear, qui traque des pièges classiques (ex : argument par défaut mutable) select = ["E", "F", "B"] # Les règles que l'on ignore ignore = ["E501"]
À noter
Si un fichier ruff.toml cohabite avec un fichier pyproject.toml dans le même dossier, c'est le fichier ruff.toml qui l'emporte.
La section [tool.ruff] regroupe les réglages généraux. D'ailleurs, target-version indique à Ruff la version minimale de Python que vise votre projet. Si votre fichier pyproject.toml déclare déjà requires-python, Ruff en déduit automatiquement la bonne valeur.
La sous-section [tool.ruff.lint] concerne spécifiquement le linter. select active des familles de règles, tandis qu'ignore les désactive.
Attention
select ne s'ajoute pas aux règles par défaut : il les remplace entièrement. Sans le moindre réglage, Ruff active déjà des règles par défaut : les erreurs de logique (F, Pyflakes) et une partie des conventions de style (E, pycodestyle). Dans notre exemple, select = ["E", "F", "B"] fonctionne parce qu'on y réinclut E et F. Mais si vous aviez écrit seulement select = ["B"], vous auriez perdu au passage la détection des imports inutiles. Pour ajouter une famille de règles sans perdre le socle par défaut, mieux vaut utiliser extend-select.
[tool.ruff.lint] # Ajoute flake8-bugbear (B) aux règles par défaut sans les perdre extend-select = ["B"]
Pour avoir au moins un petit exemple, voici ce que ça donnerait avec un ruff.toml :
# ruff.toml [lint] extend-select = ["B"]
Comme ce fichier est propre à Ruff, pas besoin du préfixe tool.ruff.
Comprendre les codes de règles
C'est le point qui embrouille un peu tout le monde au début 😅. Ruff reprend le système de codes de Flake8 : chaque règle est identifiée par un préfixe d'une à trois lettres, suivi de trois chiffres. Le préfixe indique la source de la règle.
À noter
Ce système de codes vient exclusivement du monde des linters (Flake8 et ses plugins), jamais de Black par exemple. Un linter propose une multitude de règles que l'on doit pouvoir activer/désactiver.
Quelques exemples :
-
F: Pyflakes (erreurs de logique, imports inutiles...) -
E: pycodestyle (conventions de style de la PEP 8) -
UP: pyupgrade (modernisation de la syntaxe) -
B: flake8-bugbear (pièges et mauvaises pratiques) -
I: isort (tri des imports)
F401 désigne une règle bien précise (import non utilisé), tandis que F tout seul active toutes les règles Pyflakes. On peut donc mélanger codes complets et préfixes dans select et ignore, et même ne sélectionner qu'une partie d'une famille.
À noter
Il existe aussi le code spécial ALL, qui active toutes les règles disponibles (plus de 900 🙀). La documentation conseille d'activer les familles de règles au fur et à mesure de vos besoins.
Ignorer une erreur ponctuelle
Parfois, une règle a raison sur le principe, mais vous savez ce que vous faites. Il est alors possible de désactiver une règle sur une seule ligne en utilisant le commentaire # noqa suivi du code concerné :
import os # noqa: F401
Et pour ignorer une règle dans tout un fichier, on passe par la configuration per-file-ignores. Par exemple, si je veux autoriser les imports en dehors du haut du fichier dans les fichiers __init__.py :
[tool.ruff.lint.per-file-ignores] "__init__.py" = ["E402"]
Je pensais parler de Ruff et de pre-commit ici, mais cela fera un article à part entière. On pourrait encore parler de Ruff, mais les bases sont déjà posées ; il n'y a plus qu'à tester et expérimenter.
Avant de se quitter, il est bon de savoir qu'il est aussi possible d'intégrer Ruff à son éditeur de code via l'extension officielle pour VS Code (et d'autres éditeurs). Le linting et le formatage se font alors directement pendant que vous codez, avec un retour immédiat.
Ruff dans VS Code