Boostez votre qualité de code Python avec ruff

Au fil des années et de sa professionnalisation, l’écosystème Python s’est progressivement doté d’une multitude d’outils d’analyse statique de code destinés à aider les équipes à maintenir un code de qualité : Flake8, pylint, isort, Mypy, Pyright, Black, Bandit… Ces outils remplissent des rôles variés, allant de la vérification des styles de code à la détection d’erreurs potentielles. Cependant, leur utilisation simultanée peut parfois ralentir les flux de travail et augmenter la complexité de la configuration.

C’est là que ruff intervient, avec l’ambition de devenir un outil unique pour les remplacer tous.

xkcd CC-BY-NC

ruff est un outil apparu en 2022 qui vise à remplacer les linters et les formatters existants, avec l’accent mis sur la rapidité d’exécution, grâce à une écriture en Rust plutôt qu’en Python. Il implémente à ce jour déjà plus de 800 règles de vérification, reprises de Pyflakes, pycodestyle, mccabe, isort, pydocstyle, pyupgrade, Flake8 (et plusieurs dizaines de ses plugins), Bandit, eradicate, Pylint, tryceratops, flynt, refurb, pydoclint… Et comme si ce n’était pas assez, il commence aussi à intégrer ses propres règles supplémentaires, notamment pour cibler des frameworks populaires, comme FastAPI ou NumPy.

Malgré ce grand nombre de règles, il s’exécute extrêmement rapidement, et est capable de traiter la plupart des bases de codes en moins d’une seconde. Ainsi, il analyse toute la base de CPython en à peine 160ms, contre près de 12s pour Pyflakes et plus d’une minute pour Pylint.

Côté formatage, l’écosystème Python était déjà largement unifié derrière Black, qui s’était imposé notamment grâce au choix radical de faire un formatter qui impose ses choix, quasiment sans aucune configuration possible, évitant ainsi aux équipes de toujours longues et souvent stériles discussion sur les règles de formatage à adopter… ruff reprend ce précepte et propose un formatage quasiment identique à celui de Black (les écarts étant volontaires et documentés), mais avec une vitesse d’exécution décuplée (littéralement !). Il est très légèrement plus configurable, en ajoutant principalement un paramètre qui pourrait aider à convaincre certaines équipes de l’adopter : il est possible de lui faire privilégier les ‘simple-quotes’ plutôt que les « double-quotes » pour délimiter les chaînes, mais aussi de lui faire préserver au cas par cas le ‘type’ de « quotes » choisis par le développeur. Le fait d’imposer un style unique était en effet un point souvent reproché à Black.

À noter également qu’une partie des divergences par rapport à Black restent rétrocompatibles avec Black : ruff introduit des modifications dans le code, mais ces modifications sont acceptées par Black, qui n’imposera pas un retour en arrière. Ce qui permet d’envisager dans certains cas de faire cohabiter les deux outils si nécessaire. C’est une situation que j’ai par exemple rencontrée sur un projet où ruff commençait à être adopté du côté des développeurs, tandis que Black restait la référence dans les workflows de CI/CD.

To ruff or not to ruff?

D’un point de vue fonctionnel, la décision d’adopter ou non ruff dès maintenant dépendra beaucoup des outils que vous utilisez actuellement.

Vous n’utilisez pas encore de linter ?

Il serait temps de mettre en place un linter dans votre chaîne de production logicielle ! ruff est alors un choix pertinent, puisque vous n’aurez pas à gérer les écarts par rapport à vos outils existants, et les excellentes performances de ruff vous permettront de l’ajouter à vos workflows sans impact significatif sur leurs vitesse. Attention toutefois à ne pas être trop gourmand dès le début : commencez par n’activer que les règles par défaut de ruff, puis une fois votre code mis en conformité ajoutez progressivement les règles qui vous intéresse. Vous éviterez ainsi d’être découragés par la masse de travail que peut représenter l’adaptation d’une base de code ayant déjà un long historique…

Vous utilisez principalement Flake8 ou des outils proches ?

Là encore, basculer vers ruff est un choix pertinent. Vous gagnerez en vitesse, tout en gardant une excellente compatibilité avec votre existant.

Vous utilisez beaucoup Pylint ?

Face à Pylint, le choix est plus difficile. Même si ruff a commencé à reprendre des règles de Pylint, il n’ambitionne pas, au moins à court terme, de le remplacer totalement. En effet, Pylint n’est pas qu’un simple linter, il effectue aussi un travail de vérification de type (avec la particularité de le faire en grande partie sans tenir compte des annotations de type), ce que ruff a fait le choix de ne pas réaliser pour l’instant.

Si votre code est proprement typé, la solution peut alors être d’utiliser une combinaison de ruff pour le lint et de Mypy pour la vérification du typage.

Dans le cas contraire, restez plutôt sur Pylint. Vous pouvez aussi faire un compromis en utilisant ruff dans les environnements de développement, pour une analyse plus rapide, et Pylint dans la CI/CD, pour une analyse plus complète.

Et pour le formatage ?

Que vous utilisiez déjà ou pas un outil de formatage de code, le remplacer par ruff peut être intéressant pour gagner en confort grâce à ses performances. Je mettrai toutefois un bémol dans le cas où votre code ne bénéficierait pas d’une couverture de test élevé : alors que Black garanti une exécution du code à l’identique après reformatage, grâce à une comparaison rigoureuses des AST produits par le code original et le code formaté, ruff n’offre à priori pas ce type de garantie. Il y a donc un léger risque lors de sa mise en place sur une large base de code, même si l’objectif de production de code quasi identique à celui produit par Black devrait éviter de trop mauvaises surprises.

Pourquoi ne pas adopter ruff ?

L’outil parfait n’existant pas, il y a malheureusement aussi quelques bonnes raisons de ne pas adopter ruff. Ces raisons sont principalement sa gouvernance et le choix du langage Rust.

Pour ce qui est de la gouvernance, contrairement à la plupart des outils que ruff ambitionne de remplacer, ruff n’est pas un projet purement communautaire. Il est développé sous l’égide d’Astral, une entreprise new-yorkaise. Même si Astral semble à priori sincère dans sa mise en avant de son goût pour l’ouverture et sa volonté de continuer à proposer des outils open-source et gratuits, il viendra forcément un moment où la question du modèle économique devra se poser.

Bien sûr, le choix d’une licence très permissive (MIT) garanti qu’en cas de divergence entre les aspirations d’Astral et les volontés de la communauté, un fork sera toujours possible. Et c’est là que le choix du langage Rust pose question. S’il est indéniable que ce choix était nécessaire pour atteindre les performances voulues, il rendra aussi plus difficile le fork, puisque, de fait, bon nombre de pythonistas ne seront pas aptes à reprendre un projet écrit en Rust. Ce défaut est toutefois tempéré par la popularité croissante de Rust dans la communauté Python, où il remplace de plus en plus souvent le C et le C++ pour le développement de bibliothèques hautes performances. On peut notamment citer le cas de Pydantic, en grande partie réécrit en Rust, ou encode de tokenizers, la très populaire bibliothèque de tokenization d’Hugging Face.

Enfin, il se pose la question de l’avenir des outils existants. ruff s’en inspire largement pour se développer rapidement, mais son succès pourrait justement les menacer, en détournant leur base utilisateur. À cet égard, il pourrait donc être intéressant que la gouvernance de ruff soit à l’avenir partagée avec la PSF et PyCQA pour aider à pérenniser l’expérience acquise avec les autres outils.

Pour ma part, j’ai d’ores et déjà adopté ruff sur la plupart de mes projets, et notamment ceux d’OBI Partner, même si je reste vigilant sur les trois points ci-dessus. Alors si vous aussi vous souhaitez sauter le pas et avez besoin d’assistance pour la réalisation, n’hésitez pas à nous contacter pour en discuter.

Vous êtes prêt ? Alors pipx install ruff !