Des bibliothèques exécutent Rust dans Python (avec PyO3), mais la frontière fixe la vitesse
PyO3 a relancé un débat parmi les développeurs après qu’un exemple de parseur a montré que la vitesse native seule ne garantit pas une bibliothèque Python plus rapide.
La démonstration du 13 septembre explique comment des bibliothèques exécutent Rust dans Python avec PyO3, en s’appuyant sur un parseur JSON construit à la main. Rust analyse d’abord l’entrée. PyO3 convertit ensuite l’arbre obtenu en dictionnaires, listes, chaînes, nombres, booléens et exceptions Python.
C’est cette seconde étape qui crée la tension. Un algorithme Rust rapide peut terminer avant que la couche d’intégration ait fini son travail. Pour des résultats volumineux, la conversion de valeurs natives en objets Python peut prendre plus de temps que l’opération que les développeurs cherchaient à accélérer.
Il ne s’agit ni d’une nouvelle capacité de Python ni d’une nouvelle version de PyO3. CPython prend en charge les extensions natives depuis des décennies, y compris les modules écrits en C, C++ et Fortran. L’intérêt actuel reflète la manière dont Rust a rendu cette architecture ancienne attrayante pour une nouvelle génération d’auteurs de bibliothèques.
Pydantic, Polars, cryptography et d’autres projets ont déjà placé Rust derrière des interfaces Python familières. Leur succès pousse les mainteneurs à réévaluer les chemins internes lents, mais soulève aussi des questions plus difficiles sur l’empaquetage et la couverture des plateformes.
L’enjeu important n’oppose donc pas Rust à Python. Il oppose le calcul natif au surcoût de la frontière. C’est cette confrontation qui détermine quels portages apportent des gains significatifs et lesquels ne font que déplacer la complexité dans un paquet compilé.
Les bibliothèques exécutent Rust dans Python avec PyO3 via un import familier
PyO3 transforme Rust compilé en extension native que CPython peut importer, sans demander aux développeurs d’applications d’abandonner la syntaxe Python.
Bob Belderbos a démontré cette approche avec un parseur JSON écrit en Rust et exposé via une fonction Python. Son guide du parseur ramène le processus à quatre étapes.
Un développeur écrit un module Rust ordinaire, ajoute des attributs PyO3, construit le paquet avec maturin, puis importe l’extension obtenue depuis Python. Maturin est un outil de compilation et d’empaquetage destiné aux modules Python basés sur Rust.
L’artefact compilé est du code machine natif contenu dans une bibliothèque partagée. Selon le système d’exploitation, ce fichier se termine généralement par .so, .dylib ou .dll. Python le charge via le même mécanisme général d’extensions utilisé par les anciens modules natifs.
Cette formulation est importante. Python n’interprète pas le code source Rust à l’exécution. Le compilateur Rust produit du code machine, et CPython appelle les fonctions exportées par son interface binaire applicative native.
PyO3 fournit la couche de liaison. Ses macros génèrent une grande partie du code d’interface nécessaire aux appels de fonctions, à la gestion des références, à l’extraction des arguments, aux valeurs de retour et au traitement des exceptions.
Dans la démonstration, #[pyfunction] marque une fonction Rust que Python peut appeler. La macro #[pymodule] définit le module d’extension que Python initialise lors de l’import.
L’expérience publique reste du Python ordinaire. Un appelant importe un module et transmet une chaîne à une fonction d’analyse. Rien dans cette interaction n’exige de comprendre la propriété, les traits, les durées de vie ou Cargo de Rust.
L’implémentation suit une autre voie. L’entrée passe d’une chaîne Python à une référence de chaîne Rust. Rust effectue l’analyse et construit une enum représentant l’arbre JSON.
Une enum est un type Rust qui peut contenir l’une de plusieurs variantes définies. Dans ce cas, ces variantes représentent les valeurs nulles, les booléens, les nombres, les chaînes, les tableaux et les objets.
L’arbre obtenu appartient d’abord entièrement à Rust. Python ne peut pas utiliser directement cette structure, car son interpréteur attend des objets régis par les systèmes de mémoire et de types de Python.
Le guide officiel de PyO3 décrit les deux directions prises en charge par le projet. Les développeurs peuvent créer des modules Python en Rust ou intégrer un interpréteur Python dans une application Rust.
La première direction est au cœur de cette histoire. Elle permet aux mainteneurs de préserver une interface tournée vers Python tout en déplaçant certains traitements dans du code compilé.
Ce modèle est déjà courant dans l’écosystème Python. NumPy a établi ce schéma plus large en proposant des opérations Python pratiques soutenues par du calcul natif. PyO3 change le langage et les outils employés pour construire l’extension, et non l’architecture fondamentale.
La discussion récente compte parce qu’elle rend visible la frontière cachée. L’événement intéressant n’est pas que Python ait soudain appris à exécuter du code natif. C’est que davantage de mainteneurs peuvent désormais construire ces extensions sans écrire manuellement chaque couche d’interface CPython.
Cette barrière d’implémentation plus basse élargit l’ensemble des fonctions qui méritent d’être envisagées pour un portage natif. Elle n’élimine pas la nécessité de mesurer l’appel complet, y compris tout ce qui entre dans Rust et en sort.
Les mainteneurs Python subissent une pression pour déplacer les chemins critiques vers Rust
Le succès des paquets soutenus par Rust a fait passer les extensions natives d’une technique de spécialistes à une stratégie de maintenance crédible pour les projets Python grand public.
Pydantic constitue le point de référence le plus clair. Sa deuxième version majeure a déplacé la validation dans pydantic-core, un paquet distinct implémenté en Rust.
Le premier document de conception de Pydantic V2 du projet indiquait que le cœur réécrit apportait d’importants gains de performances par rapport à la première version. Ces chiffres provenaient des propres benchmarks préliminaires de Pydantic ; les charges de travail déterminent donc toujours le résultat.
Le signal architectural compte davantage qu’un seul benchmark. Le code Python définit les modèles et génère les schémas. Le cœur Rust exécute la validation et la sérialisation sur le chemin sensible aux performances.
Polars applique une version plus large du même modèle. Son cœur est écrit en Rust et exposé par des interfaces destinées à plusieurs langages, dont Python.
L’architecture de Polars maintient la planification des requêtes, les opérations colonnaires et l’exécution parallèle au plus près des structures de données natives. Les utilisateurs Python écrivent toujours des expressions et reçoivent des résultats de haut niveau familiers.
Ces projets exercent une pression sur les mainteneurs de parseurs, validateurs, tokeniseurs, outils de compression, clients de bases de données et moteurs de données. Les utilisateurs savent désormais qu’un paquet Python peut conserver une interface accessible tout en remplaçant certains de ses composants internes.
Cette pression ne concerne pas seulement les classements de benchmarks. Le code natif peut réduire le temps CPU, améliorer le débit et utiliser la mémoire de manière plus prévisible pour des opérations bien définies.
Rust ajoute un autre attrait pour les mainteneurs. Ses systèmes de propriété et de types détectent des catégories d’erreurs mémoire lors de la compilation, même si le code unsafe et les défauts des dépendances restent possibles.
PyO3 fournit également des conversions entre de nombreux types Python et Rust courants. Il traduit les erreurs Rust en exceptions Python et participe à la gestion des références Python.
Ces outils peuvent rendre une extension plus facile à maintenir qu’une interface C écrite à la main. Ils ne rendent pas l’intégration native automatique, mais réduisent la quantité de mécanismes personnalisés nécessaires.
Les mainteneurs font ainsi face à une décision différente entre construire et acheter. Auparavant, une fonction Python lente pouvait rester en Python, car une réécriture native exigeait une expertise C rare.
Désormais, une équipe disposant d’une expérience Rust peut exposer un cœur natif plus réduit via PyO3. Maturin peut ensuite compiler l’extension et l’intégrer dans un paquet Python.
Cette voie fonctionne le mieux lorsqu’une fonction étroite consomme une entrée compacte, effectue un calcul substantiel et renvoie un résultat compact. La compression, le hachage, les résumés d’analyse, la validation et les noyaux numériques correspondent souvent à ce profil.
Elle fonctionne de manière moins prévisible lorsqu’une fonction traverse à répétition la frontière entre les langages. De nombreux appels minuscules peuvent perdre du temps en vérification d’arguments, répartition, allocation et conversion.
Un résultat volumineux crée un problème connexe. L’algorithme peut s’exécuter efficacement en Rust, mais l’appelant attend toujours des objets Python ordinaires.
Cette attente fait de la représentation des données le facteur décisif. Une bibliothèque qui conserve des buffers colonnaires en mémoire native a un profil de coût différent de celui d’un parseur renvoyant des milliers de dictionnaires imbriqués.
Les mainteneurs sont donc poussés vers des choix architecturaux, et non de simples réécritures dans un autre langage. Ils doivent décider de quel côté résident les données et à quel moment les objets Python doivent exister.
Cette pression perdurera parce que les utilisateurs comparent le comportement de bout en bout. Ils s’intéressent au délai entre l’appel d’une fonction et la réception d’un résultat exploitable, pas à la vitesse isolée de sa boucle interne.
Le trajet de retour peut effacer le gain de vitesse natif
Le mécanisme central est la matérialisation des objets : convertir un volumineux résultat Rust en objets Python peut dominer l’opération terminée.
Le parseur de Belderbos crée d’abord un arbre Rust contenant chaque valeur du document JSON. Cette étape peut bénéficier de l’exécution compilée de Rust et de son modèle de mémoire explicite.
L’étape suivante parcourt à nouveau l’arbre entier. Chaque objet Rust devient un dictionnaire Python, chaque tableau devient une liste et chaque feuille devient une valeur Python.
Ce processus est appelé matérialisation. Il crée les objets Python concrets que l’appelant s’attend à examiner, modifier, sérialiser ou transmettre ailleurs.
Le trait IntoPyObject de PyO3 coordonne cette conversion. Un trait définit un comportement que les types peuvent implémenter, permettant à chaque variante JSON de décrire sa représentation Python correspondante.
La conversion est récursive. Un objet nécessite un nouveau dictionnaire, puis des entrées pour chaque clé et chaque valeur. Un tableau nécessite une liste contenant des enfants convertis.
Chaque objet Python entre également dans le système de gestion mémoire de CPython. L’allocation, les métadonnées de type et le comptage de références impliquent des coûts qui n’existaient pas dans l’arbre Rust.
Les versions traditionnelles de CPython ajoutent une autre contrainte. Le code qui manipule des objets Python a généralement besoin d’un accès à l’interpréteur associé au verrou global de l’interpréteur, ou GIL.
Le GIL n’autorise qu’un seul thread à piloter l’interpréteur Python traditionnel à la fois. Le travail Rust détaché des objets Python peut s’exécuter sans conserver ce verrou.
PyO3 documente un mécanisme Python::detach permettant de libérer l’accès à l’interpréteur pendant que Rust effectue un calcul indépendant. Son guide sur l’exécution parallèle explique comment les autres threads Python peuvent alors continuer.
Cette technique n’aide que tant que l’opération Rust reste éloignée des objets Python. Reconstruire un dictionnaire ou une liste Python ramène l’exécution à la frontière de l’interpréteur.
L’exemple du parseur estime qu’un document contenant 100 000 valeurs nécessite un nombre de créations d’objets Python du même ordre de grandeur. Il s’agit d’une relation illustrative, et non d’un benchmark de performances universel.
La forme compte autant que la taille. Un buffer numérique plat peut parfois traverser la frontière via une représentation partagée. Un document profondément imbriqué exige davantage d’allocations d’objets et de parcours de pointeurs.
La fréquence des appels crée un autre axe. Un appel natif unique qui traite un grand buffer contigu peut amortir son coût de préparation. Des milliers d’appels traitant chacun une seule valeur ne le peuvent souvent pas.
Le traitement des erreurs traverse également la frontière. Une erreur d’analyse Rust doit devenir une exception Python avec la classe, le message et les informations de localisation attendus.
PyO3 peut mapper une erreur Rust typée vers PyErr. Une chaîne mal formée peut ainsi apparaître pour l’appelant Python comme ValueError, tandis qu’un fichier indisponible peut devenir FileNotFoundError.
Cette traduction est précieuse, car elle protège l’API publique. Les utilisateurs ne devraient pas avoir besoin d’un modèle d’erreur distinct simplement parce que les mainteneurs ont changé le langage d’implémentation.
Cependant, préserver la sémantique de Python demande du travail. Une réécriture native doit reproduire les cas limites, les types d’exception, le comportement d’itération, les règles de propriété et parfois les interactions entre sous-classes.
La véritable cible d’optimisation est donc l’interface complète. Analyser plus vite ne suffit pas lorsque la conversion de représentation reste inchangée et domine le temps d’exécution réel.
Les bibliothèques disposent de plusieurs réponses architecturales. Elles peuvent renvoyer des résumés plus compacts, exposer des itérateurs, traiter les rappels par lots ou conserver un objet opaque soutenu par Rust.
Une vue paresseuse est particulièrement pertinente pour les grands arbres. Au lieu de construire immédiatement chaque objet Python, l’extension peut ne matérialiser que les valeurs demandées par l’appelant.
Cette conception réduit les conversions inutiles lorsqu’une application ne lit qu’une petite partie du résultat. Elle déplace aussi la complexité vers la durée de vie des objets, la mise en cache, les mutations et la conception de l’API.
Les bibliothèques orientées colonnes peuvent éviter certains coûts en conservant les données dans des tampons natifs contigus. Python reçoit un objet léger qui référence le stockage sous-jacent au lieu de dupliquer chaque élément.
Cette stratégie aide à comprendre pourquoi Polars représente une architecture native plus solide qu’une réécriture mécanique fonction par fonction. Son interface Python pilote un moteur Rust qui conserve ensemble une part substantielle du travail et des données.
Un analyseur qui renvoie des dictionnaires ordinaires se heurte à une frontière moins favorable. Son format de sortie oblige l’extension à créer précisément le graphe d’objets attendu par le code Python.
La leçon n’est pas que la conversion PyO3 serait particulièrement inefficace. Toute interface de fonctions étrangères doit concilier représentations, durées de vie, erreurs et propriété entre ses deux côtés.
PyO3 facilite l’expression de ces obligations. Il ne peut pas en abolir le coût.
Rust et Python sont partenaires, mais le packaging fait les comptes
Une extension rapide crée une obligation de distribution, car les wheels compilées doivent correspondre aux systèmes d’exploitation, aux processeurs, aux interpréteurs et aux interfaces binaires.
Les paquets Python purs bénéficient d’un énorme avantage en matière de portabilité. Une unique wheel générique peut souvent fonctionner sur différents systèmes d’exploitation et architectures de processeur.
Les extensions compilées produisent du code machine spécifique à chaque plateforme. Les mainteneurs de paquets doivent fournir des artefacts compatibles ou demander aux utilisateurs de compiler le projet localement.
Une wheel est le format de distribution binaire de Python. Elle contient des fichiers de paquet installables et porte des étiquettes de compatibilité pour l’interpréteur, l’interface binaire applicative et la plateforme.
Le guide de packaging Python décrit la matrice par défaut. Les mainteneurs ont souvent besoin de builds couvrant les versions de Python, les systèmes d’exploitation et les architectures.
L’intégration continue peut automatiser une grande partie de ce travail. Maturin et des outils associés peuvent construire et publier des wheels, tandis que des projets tels que cibuildwheel coordonnent les builds dans différents environnements cibles.
L’automatisation n’élimine pas les tests. Une wheel peut s’installer correctement tout en échouant à cause d’une fonctionnalité CPU non prise en charge, d’une bibliothèque système manquante ou d’une attente d’exécution incompatible.
Linux crée une complexité particulière, car les distributions fournissent différentes versions de composants système. Les spécifications manylinux offrent aux projets des environnements de build standardisés pour des wheels largement compatibles.
Windows et macOS nécessitent leurs propres artefacts. La séparation d’Apple entre le matériel Intel et Arm a ajouté une dimension supplémentaire, même si les binaires universels peuvent parfois combiner les architectures.
L’ABI stable peut réduire la dimension liée aux versions d’interpréteur. Une ABI est le contrat de bas niveau qui régit la manière dont le code compilé appelle un environnement d’exécution binaire.
L’ABI stable abi3 d’origine de CPython permet aux extensions admissibles de cibler plusieurs versions de Python 3 avec une seule wheel par plateforme et architecture. L’extension doit se limiter à la Limited API.
Cette restriction échange une partie de l’accès aux API et de l’optimisation potentielle contre une compatibilité plus large. PyO3 permet de sélectionner une version minimale appropriée de Python lors de la construction d’une extension abi3.
Le problème du packaging évolue de nouveau avec CPython sans GIL. Les builds sans GIL retirent la configuration GIL traditionnelle, modifiant les hypothèses faites par les extensions natives.
La documentation actuelle de PyO3 distingue les wheels abi3 traditionnelles du chemin ABI stable plus récent pour les builds sans GIL. Les mainteneurs doivent valider les configurations d’interpréteur que leurs artefacts prennent réellement en charge.
Les questions de plateforme sont vite apparues dans les échanges publics autour de l’article sur l’analyseur. Des développeurs ont demandé si les dépendances soutenues par Rust fonctionnent encore partout où Python peut s’exécuter.
La réponse courte est non. Une dépendance compilée fonctionne là où ses mainteneurs publient une wheel compatible ou là où les utilisateurs peuvent en construire une avec une chaîne d’outils prise en charge.
Cette limite s’applique également aux extensions natives écrites dans d’autres langages. Rust modifie les cibles de compilation et les dépendances de build disponibles, mais ne résout pas le problème fondamental de portabilité.
Le paquet cryptography illustre l’expérience utilisateur. Sa documentation indique que la plupart des utilisateurs reçoivent une wheel précompilée et n’ont pas besoin d’installer Rust.
Les utilisateurs situés hors de l’ensemble des wheels publiées peuvent avoir besoin d’un compilateur Rust et d’autres dépendances natives. Ce repli peut surprendre ceux qui s’attendaient à ce que pip install reste indépendant du langage.
Python hébergé dans un navigateur présente un autre cas limite. Pyodide exécute Python via WebAssembly, de sorte que les wheels conventionnelles pour ordinateurs de bureau et serveurs n’y fonctionnent pas automatiquement.
L’écosystème a progressé pour les builds WebAssembly de paquets contenant du Rust. Pydantic-core propose désormais des artefacts pertinents, d’après les liens cités dans la discussion Hacker News.
Néanmoins, la prise en charge de WebAssembly exige un packaging et des tests délibérés. Le comportement des entrées et sorties peut également se heurter aux limites des navigateurs concernant les threads, les fichiers, les sockets et les environnements d’exécution asynchrones.
Les systèmes mobiles, les environnements embarqués, les processeurs inhabituels et les anciennes distributions d’entreprise exercent une pression similaire. Un repli en Python pur peut préserver la couverture, mais maintenir deux implémentations ajoute du travail.
Les équipes de bibliothèques doivent donc intégrer le coût de la portabilité à chaque réécriture native. Le chemin d’exécution peut être plus rapide tandis que le projet devient plus difficile à distribuer auprès de l’ensemble de son public.
Pour les paquets populaires, ce compromis peut valoir la peine d’être accepté. Une large base de contributeurs et une chaîne de publication mature peuvent prendre en charge une vaste matrice de wheels.
Les petits projets font face à une autre équation. Les builds natifs peuvent transformer une bibliothèque compacte en un engagement d’ingénierie de publication sur plusieurs environnements.
PyO3 et maturin réduisent considérablement cet engagement. Ils ne l’effacent pas, et les utilisateurs vivent chaque cible non construite comme un échec d’installation.
Le point de vue sceptique commence par une mesure de bout en bout
« Écrit en Rust » est un fait d’implémentation, pas un résultat de performance, et les mainteneurs ont besoin de benchmarks qui incluent la conversion et la consommation.
Rust améliore souvent la vitesse de code intensif en CPU par rapport à une boucle Python équivalente. Cette comparaison seule dit peu de chose sur la charge de travail complète d’une application.
Un appel d’extension comprend la conversion des arguments, la validation, la répartition native, le calcul, la conversion de sortie, l’allocation et la gestion des erreurs. L’appelant peut ensuite transformer à nouveau le résultat.
Un benchmark utile mesure l’ensemble de ce parcours. Il part de la représentation utilisée par l’application et se termine avec des données que l’étape suivante peut consommer.
Les microbenchmarks conservent leur rôle. Ils identifient l’étape qui consomme du temps et révèlent si un algorithme s’est amélioré après une modification du code.
Ils deviennent trompeurs lorsque les équipes ne présentent que l’étape interne la plus rapide. Un chiffre de débit d’analyse peut masquer le coût ultérieur de construction d’un grand graphe d’objets Python.
Les benchmarks ont également besoin d’entrées représentatives. Les petits documents peuvent surestimer le coût fixe des appels, tandis que des données synthétiques uniformes peuvent ignorer les schémas d’allocation rencontrés en production.
Les caches chauds, les builds de production, les options du compilateur et les fonctionnalités CPU peuvent modifier les résultats. Les comparaisons doivent rendre ces conditions visibles et tester le même comportement public.
La correction mérite un poids égal. Les analyseurs et validateurs rencontrent des encodages malformés, une imbrication extrême, des clés dupliquées, des cas limites numériques et une consommation de ressources inattendue.
Un portage qui modifie les exceptions ou accepte des entrées différentes peut être plus rapide parce qu’il effectue un travail différent. Les tests de compatibilité doivent accompagner les tests de performance.
La consommation mémoire peut inverser un gain apparent. Conserver à la fois un arbre Rust complet et un arbre Python nouvellement matérialisé peut temporairement exiger deux représentations.
Les conceptions en flux ou paresseuses peuvent réduire cette duplication. Elles peuvent aussi modifier le moment où les erreurs surviennent et la durée pendant laquelle les tampons natifs restent alloués.
Les affirmations concernant la concurrence exigent une prudence similaire. Le code Rust peut utiliser plusieurs threads, et PyO3 peut libérer l’accès à l’interpréteur pendant un travail natif approprié.
L’extension ne peut pas manipuler en toute sécurité des objets Python ordinaires depuis des threads natifs arbitraires. Elle doit revenir aux règles d’interpréteur de PyO3 chaque fois qu’elle interagit avec eux.
Python sans GIL modifie une partie de ce tableau, mais n’élimine pas la synchronisation des données de l’application. La sûreté des threads reste une responsabilité explicite de la bibliothèque.
La sécurité résiste également aux slogans simplistes. Le sous-ensemble sûr de Rust empêche plusieurs catégories de mauvaise gestion de la mémoire, mais les extensions natives peuvent contenir des blocs unsafe et des dépendances vulnérables.
Un défaut dans du code compilé peut faire planter le processus de l’interpréteur plutôt que de lever une exception Python normale. Cette conséquence s’applique à tous les langages d’extensions natives.
L’argument le plus solide en faveur de PyO3 est donc précis. Il permet aux mainteneurs de combiner une interface Python avec des implémentations Rust lorsque le calcul et la disposition des données justifient cette frontière.
L’argument le plus faible est une réécriture cosmétique. Remplacer un code Python concis par une mécanique native apporte peu lorsque la fonction effectue un travail limité ou passe l’essentiel de son temps en E/S.
Un projet devrait d’abord profiler l’application existante. Il devrait identifier un chemin chaud stable, définir les exigences de compatibilité et mesurer la taille et la forme des valeurs échangées.
L’équipe peut ensuite prototyper une frontière. Si la conversion domine, la décision suivante concerne la représentation des données plutôt que les instructions de l’analyseur ou l’optimisation du compilateur.
Ce test sceptique ne plaide pas contre Python soutenu par Rust. Il protège cette approche contre sa transformation en réponse par défaut à toute plainte concernant les performances.
Une extension native réussie masque son implémentation aux utilisateurs ordinaires. L’installation fonctionne, les exceptions restent reconnaissables, les annotations de types demeurent utiles et les performances s’améliorent dans la charge de travail réelle.
Les utilisateurs ne devraient pas avoir à célébrer le choix du langage. Ils devraient simplement constater que leur opération Python existante se termine plus vite.
Trois signaux montreront où PyO3 ira ensuite
La prochaine phase dépend d’API conscientes des frontières, d’une couverture de wheels plus large et d’extensions fonctionnant à la fois avec Python traditionnel et Python sans GIL.
Le premier signal sera une évolution des benchmarks publics. Davantage de projets devraient rapporter des temps de bout en bout incluant la création d’objets, et pas seulement le débit des noyaux natifs.
Cette évolution renforcerait l’argument en faveur de PyO3, car elle relierait le choix du langage à des résultats observables dans les applications. Elle mettrait également en évidence les portages dont les gains disparaissent lors de la conversion.
Recherchez des benchmarks comparant plusieurs conceptions de retour. Une vue soutenue par du natif, un itérateur, un résultat par lots et un dictionnaire entièrement matérialisé peuvent produire des résultats très différents.
Les profils mémoire devraient apparaître à côté des mesures de temps. Ils peuvent montrer si une extension conserve brièvement des structures Rust et Python dupliquées.
Le deuxième signal est une couverture de wheels plus large. Les projets qui réussissent publieront des artefacts fiables pour les principaux systèmes d’exploitation, familles de processeurs et versions de Python prises en charge.
WebAssembly mérite une attention particulière. De meilleurs outils et davantage de wheels WebAssembly hébergées sur PyPI réduiraient l’objection de portabilité soulevée par les utilisateurs de Python dans le navigateur.
Les architectures Linux mobiles et moins courantes restent des tests utiles. Chaque cible prise en charge élargit la signification pratique d’une dépendance Python classique.
Les projets devraient également documenter leur procédure de compilation depuis les sources. L’absence d’une wheel devient moins problématique lorsque les messages d’erreur, les exigences de compilateur et les instructions de compilation reproductible sont clairs.
Si les paquets natifs abandonnent régulièrement des cibles prises en charge par les versions purement Python, la préoccupation liée à la portabilité se renforce. Si l’automatisation de la compilation comble ces lacunes, elle s’atténue.
Le troisième signal est la prise en charge stable de CPython sans GIL. Les extensions doivent adapter leurs hypothèses concernant le verrouillage de l’interpréteur, l’état partagé et l’accès aux objets.
Les API évolutives de PyO3 et la prise en charge de l’ABI stable façonneront cette transition. Les mainteneurs ont besoin de plus qu’une compilation réussie ; ils doivent effectuer des tests de concurrence sous des charges de travail réalistes.
Un résultat mature permettrait à un projet de prendre en charge les interpréteurs traditionnels et sans GIL sans multiplier la complexité des versions au-delà de ce qui est maîtrisable.
L’échec aurait une autre forme. Des ensembles de wheels fragmentés, des balises ABI peu claires ou des goulots d’étranglement de sérialisation cachés rendraient les paquets reposant sur Rust plus difficiles à considérer comme des dépendances par défaut.
L’orientation générale reste convaincante. Python apporte une vaste base d’utilisateurs, une orchestration lisible et une couche applicative productive. Rust apporte des noyaux compilés, des structures de données explicites et des outils natifs plus sûrs.
Toutefois, le partenariat ne réussit que lorsque la frontière devient partie intégrante de la conception. Les bibliothèques exécutent Rust dans Python avec PyO3, mais les utilisateurs consomment des objets Python et des artefacts de paquets Python.
Les développeurs qui évaluent un portage devraient commencer par une question concrète : que doit franchir cette frontière après le retour de la fonction Rust rapide ?
Analysez le chemin de retour avant de vous engager dans une réécriture. Testez la wheel sur chaque cible annoncée. Comparez ensuite l’opération complète avec l’implémentation Python dont les utilisateurs dépendent déjà.
Si la version native conserve son avantage, PyO3 a gagné sa place. Si la conversion absorbe ce gain, modifiez l’interface avant de modifier davantage de code.



