Rust pour les développeurs Python : guide pratique
Si vous connaissez Python, vous connaissez déjà beaucoup d’idées de Rust. Rust a des boucles for sur des itérateurs, des tuples, des closures (les lambdas de Python, en plus utiles), une instruction match qui fonctionne comme le match de Python, et des chaînes d’itérateurs qui font le travail des compréhensions de liste. Rust partage même certains goûts de Python : une seule façon évidente de formater le code, un outil de paquets agréable, et des messages d’erreur qui font tout pour vous aider.
Ce qui change, c’est surtout le compilateur. Python vérifie les types pendant l’exécution du programme ; Rust les vérifie tous avant. Python libère la mémoire avec un ramasse-miettes (garbage collector) ; Rust utilise l’ownership (possession) et le borrowing (emprunt), vérifiés à la compilation. Python a None et les exceptions ; Rust a Option et Result. C’est sur ces trois points que vous passerez le plus de temps d’apprentissage.
Alors pourquoi les développeurs Python apprennent-ils Rust ? Voici les raisons, en toute honnêteté :
- La vitesse. Rust se compile en code machine natif. Le travail gourmand en CPU qui est lent en Python pur (analyse de texte, calcul numérique, grosses boucles) est en général bien plus rapide en Rust, sans astuce d’optimisation.
- De vrais threads. Dans CPython standard, le GIL ne laisse qu’un seul thread exécuter du code Python à la fois. Les threads Rust tournent sur tous les cœurs de votre processeur en même temps, et le compilateur empêche les accès concurrents aux données (data races) avant même l’exécution.
- Des bugs trouvés plus tôt. Une faute de frappe dans un nom d’attribut, un
Noneoublié, une fonction appelée avec le mauvais type : en Python, vous les découvrez quand la ligne s’exécute. En Rust, le programme ne compile pas tant qu’ils ne sont pas corrigés. - Pas besoin de choisir. Avec PyO3 et maturin, vous pouvez écrire la partie lente d’un projet Python en Rust et l’importer (
import) depuis Python comme n’importe quel autre module. Des outils Python populaires comme Polars, Ruff et uv sont écrits en Rust.
Si vous cherchez une comparaison plus générale de Rust avec Python, Go et C++, voyez Rust vs C++, Go et Python.
Les concepts du langage : de Python à Rust
Section intitulée « Les concepts du langage : de Python à Rust »Utilisez ce tableau comme un guide de conversation. La dernière colonne renvoie à la leçon de ce site qui explique le côté Rust.
| Python | Rust | Pour en savoir plus |
|---|---|---|
| Types dynamiques, vérifiés à l’exécution | Types statiques, vérifiés à la compilation. Rust en déduit la plupart, donc vous écrivez moins de types que vous ne le pensez | Types de données |
Annotations de type (def f(x: int) -> str) |
Les types sont obligatoires dans la signature des fonctions, et le compilateur les fait respecter | Fonctions |
x = 5 |
let x = 5; (non modifiable). let mut x = 5; si vous voulez le modifier |
Variables |
int (grandit autant que nécessaire) |
Entiers de taille fixe : i32, i64, u64, usize, i128… Ils peuvent déborder |
Types de données, Dépassement d’entier |
float |
f64 (ou f32) |
Types de données |
str |
String (possédée, qui peut grandir) et &str (une vue empruntée sur du texte) |
Chaînes |
f-strings : f"Hi {name}" |
format!("Hi {name}") et println!("Hi {name}") |
Afficher du texte |
print(repr(x)) / __repr__ |
println!("{x:?}") avec #[derive(Debug)] |
Afficher du texte, Traits courants |
__str__ |
Implémenter Display (et .to_string() marche aussi) |
Traits courants |
__eq__, __hash__, __lt__ |
Dériver PartialEq, Eq, Hash, PartialOrd, Ord |
Traits courants |
None, Optional[T] |
Option<T> : soit Some(value), soit None |
Option |
x or default |
x.unwrap_or(default) |
Option |
raise, try/except |
Result<T, E> + l’opérateur ? |
Result, Erreurs personnalisées |
Une exception pour un vrai bug (assert, IndexError) |
panic! (arrête le thread) |
Panic |
class avec __init__ et des méthodes |
struct pour les données + un bloc impl pour les méthodes ; new est une fonction ordinaire |
Structs |
@dataclass |
struct avec #[derive(Debug, Clone, PartialEq)] |
Structs |
self |
&self (lecture), &mut self (modification) ou self (prise d’ownership) |
Structs |
| Héritage | Pas d’héritage. Utilisez la composition (une struct dans une struct) + des traits avec des méthodes par défaut | Traits, Patrons de conception |
Duck typing, Protocol, ABC |
trait |
Traits |
| Une liste d’objets de classes différentes | Vec<Box<dyn Trait>> (un trait object) |
Trait objects |
TypeVar / fonctions génériques |
Génériques avec des contraintes de trait : fn largest<T: Ord>(items: &[T]) |
Génériques |
Classe Enum |
enum dont chaque variante peut porter ses propres données |
Enums |
match / case (3.10+) |
match (le compilateur vérifie que vous avez couvert tous les cas) |
match, Pattern matching |
if my_list: (valeur de vérité) |
Soyez explicite : if !my_list.is_empty(). Seul un bool peut aller dans un if |
If…Else |
for i in range(10) |
for i in 0..10 |
Boucles for |
enumerate(xs), zip(a, b) |
xs.iter().enumerate(), a.iter().zip(b.iter()) |
Itérateurs |
lambda x: x + 1 |
Closures : |x| x + 1 (et elles peuvent tenir sur plusieurs lignes) |
Closures |
Compréhensions de liste, map, filter |
Adaptateurs d’itérateurs : filter, map, puis collect() |
Itérateurs |
Générateurs et yield |
Votre propre type qui implémente Iterator, ou std::iter::from_fn |
Itérateurs personnalisés |
with open(...) as f: |
Rien à écrire : les fichiers et les verrous se ferment quand leur propriétaire sort de la portée (Drop) |
Portée, Lire et écrire des fichiers |
Arguments par défaut, *args, **kwargs |
Non pris en charge. Utilisez des paramètres Option, des slices (&[T]), une struct avec Default, ou un builder |
Fonctions, Patrons de conception |
| Décorateurs | Pas d’équivalent direct. Enveloppez avec des closures, ou utilisez des macros d’attribut (comme #[tokio::main]) |
Closures, Macros procédurales |
import et modules |
mod et use |
Modules |
| Paquet sur PyPI | Crate sur crates.io | Cargo et les crates |
if __name__ == "__main__": |
fn main() |
Débuter avec Rust |
threading (avec le GIL) |
std::thread : de vrais threads parallèles |
Concurrence |
multiprocessing, concurrent.futures |
Des threads, ou la crate rayon pour des itérateurs parallèles |
Concurrence, État partagé |
asyncio, async def, await |
async fn et .await, avec un runtime comme tokio |
Async/Await |
7 // 2, 2 ** 10 |
7 / 2 (division entière sur des entiers), 2_i64.pow(10) |
Opérateurs |
int("42"), str(42) |
"42".parse::<i32>() (renvoie un Result), 42.to_string() |
Conversion de types, Conversions |
Les collections
Section intitulée « Les collections »Toutes les collections intégrées de Python ont leur place dans la bibliothèque standard de Rust. Les plus grandes différences : une collection Rust contient un seul type (un Vec<i32> ne contient que des i32), et une HashMap ne retient pas l’ordre d’insertion.
| Python | Rust | Pour en savoir plus |
|---|---|---|
list |
Vec<T> |
Vecteurs |
tuple |
Tuples (i32, String), ou une petite struct avec des champs nommés |
Tuples |
dict |
HashMap<K, V> (pas d’ordre fixe !) |
HashMap |
dict quand il vous faut des clés triées |
BTreeMap<K, V> |
HashMap |
dict quand il vous faut l’ordre d’insertion |
La crate indexmap |
|
set, frozenset |
HashSet<T>, BTreeSet<T> |
HashSet |
collections.Counter, defaultdict |
HashMap avec l’API entry : *map.entry(k).or_insert(0) += 1 |
HashMap |
collections.deque |
VecDeque<T> |
VecDeque et BinaryHeap |
heapq (le plus petit d’abord) |
BinaryHeap<T> (le plus grand d’abord ; enveloppez les éléments dans Reverse pour avoir le plus petit d’abord) |
VecDeque et BinaryHeap |
namedtuple |
struct |
Structs |
bytes, bytearray |
&[u8], Vec<u8> |
Slices |
Découpage : xs[1:3] |
Slices : &xs[1..3] (pas d’indices négatifs ; utilisez xs.len() - 1) |
Slices |
L’outillage : de pip à cargo
Section intitulée « L’outillage : de pip à cargo »C’est là que beaucoup de développeurs Python se détendent un peu. Rust a un seul outil officiel, Cargo, qui fait le travail de pip, venv, setuptools, pytest et d’autres encore. Pas de débat sur « quel outil de packaging faut-il utiliser ? ».
| Python | Rust | Remarques |
|---|---|---|
Interpréteur python |
Compilateur rustc, en général appelé via cargo |
Vous compilez d’abord, puis vous exécutez un binaire natif. Voir Débuter avec Rust |
| pyenv | rustup (et un fichier rust-toolchain.toml pour fixer une version) |
Voir Les outils de Rust |
| pip, poetry, uv | cargo |
Compile, exécute, teste, ajoute des paquets, publie. Voir Cargo et les crates |
pip install requests |
cargo add reqwest |
Ajoute la crate au Cargo.toml de ce projet uniquement |
pyproject.toml / requirements.txt |
Cargo.toml |
|
poetry.lock / uv.lock |
Cargo.lock |
Créé pour vous |
| venv / virtualenv | Inutile | Chaque projet a ses propres dépendances et se compile dans son propre dossier target/ |
| PyPI | crates.io | La documentation de l’API de chaque crate publiée est sur docs.rs |
python main.py |
cargo run (cargo run --release pour la pleine vitesse) |
Les builds debug sont beaucoup plus lents à l’exécution ; mesurez les performances en release |
pipx install ruff |
cargo install ripgrep |
Installe un outil en ligne de commande |
| PyInstaller | cargo build --release |
Vous obtenez un seul binaire natif ; vos utilisateurs n’ont pas besoin d’installer Rust |
| black | cargo fmt |
Un seul style standard pour tout le monde |
| ruff, pylint, flake8 | cargo clippy |
Des centaines de lints utiles, officiels |
| mypy, pyright | Le compilateur lui-même | La vérification des types n’est ni optionnelle ni une étape à part |
| pytest, unittest | #[test] et cargo test intégrés (cargo nextest est un lanceur plus rapide très populaire) |
Voir Les tests |
Fixtures pytest et parametrize |
rstest |
|
| hypothesis | proptest |
Tests basés sur les propriétés. Voir Tests avancés |
unittest.mock |
mockall, ou de fausses implémentations écrites à la main qui implémentent votre trait |
|
| timeit, pytest-benchmark | criterion |
Voir Performance |
| cProfile, py-spy | cargo flamegraph, perf, ou n’importe quel profileur natif |
|
| Docstrings + Sphinx | Commentaires de doc /// en Markdown + cargo doc |
Les exemples de code dans la doc sont exécutés comme des tests. Voir Documentation |
| Le REPL Python, Jupyter | Le Rust Playground ; evcxr_repl et evcxr_jupyter |
Rust n’a pas de REPL intégré. Pour expérimenter, on crée en général de petits projets cargo run |
| VS Code + Pylance, PyCharm | VS Code + rust-analyzer, ou JetBrains RustRover | rust-analyzer offre la complétion, l’accès à la définition et les erreurs dans le code |
Voici un petit Cargo.toml, pour que vous puissiez le comparer à un pyproject.toml :
[package]name = "weather-cli"version = "0.1.0"edition = "2021"
[dependencies]reqwest = { version = "0.13", features = ["blocking", "json"] }serde = { version = "1", features = ["derive"] }clap = { version = "4", features = ["derive"] }Bibliothèques et frameworks : « on utilise quoi à la place de… ? »
Section intitulée « Bibliothèques et frameworks : « on utilise quoi à la place de… ? » »« J’utilise requests, FastAPI et pandas. C’est quoi, la version Rust ? » Voici les équivalents les plus proches. L’écosystème Python est bien plus ancien et plus vaste : dans certains domaines (la data science surtout), Rust a des options moins nombreuses et plus jeunes.
Web et réseau
Section intitulée « Web et réseau »| Python | Rust | Remarques |
|---|---|---|
| Flask, FastAPI | axum (nous en construisons une dans Projet : API web avec Axum), actix-web | Les handlers sont de simples async fn ; les types de requête et de réponse viennent de vos structs, un peu comme FastAPI avec pydantic |
| Django | Pas d’équivalent complet. Loco (loco-rs) est un framework « tout compris » ; la plupart des équipes combinent axum + un ORM + un moteur de templates |
Pas de site d’administration intégré |
| requests, httpx | reqwest (async, avec un client bloquant en option), ureq (simple et bloquant) |
Voir Réseau en Rust |
| aiohttp | reqwest + tokio |
|
| Jinja2 | minijinja (par l’auteur de Jinja lui-même), tera, askama (templates vérifiés à la compilation) |
|
| BeautifulSoup | scraper |
Données et bases de données
Section intitulée « Données et bases de données »| Python | Rust | Remarques |
|---|---|---|
| ORM SQLAlchemy | Diesel (requêtes vérifiées par le système de types à la compilation), SeaORM (async, à base d’entités) | Les bases de données en Rust montre les deux avec du code qui fonctionne |
| psycopg, SQL brut | sqlx : du SQL brut, vérifié à la compilation contre votre vraie base de données | Async, très populaire, pas un ORM |
| sqlite3 | rusqlite |
|
| redis-py | redis |
|
| Celery | apalis pour les files de tâches ; tokio::spawn pour des tâches en arrière-plan dans le processus |
Data science et machine learning
Section intitulée « Data science et machine learning »| Python | Rust | Remarques |
|---|---|---|
| pandas | Polars | Écrit en Rust ; son API Python, que vous connaissez peut-être déjà, repose sur la même bibliothèque Rust |
| NumPy | ndarray |
Tableaux à N dimensions avec découpage et broadcasting ; bien moins d’extras que NumPy |
| scikit-learn | linfa, smartcore |
Moins d’algorithmes et des communautés bien plus petites |
| PyTorch | candle (crate candle-core), burn |
Bien pour exécuter des modèles ; l’entraînement se fait encore surtout en Python |
| matplotlib | plotters |
|
Module csv |
Crate csv + serde |
Les briques du quotidien
Section intitulée « Les briques du quotidien »| Python | Rust | Remarques |
|---|---|---|
json |
serde + serde_json |
La référence ; gère aussi TOML, YAML, et d’autres formats. Voir JSON avec Serde |
| pydantic | serde pour l’analyse + validator ou garde pour les règles de validation |
Votre struct est le schéma |
| argparse, click, typer | clap |
Définissez vos arguments sous forme de struct. Voir Projet : outil CLI avec clap |
logging |
tracing, ou log + env_logger |
Voir Journalisation |
re |
regex |
Pas de lookaround ni de références arrière, mais un temps linéaire garanti ; fancy-regex les propose. Voir Expressions régulières |
datetime |
chrono ou jiff |
Voir Dates et heures |
random |
rand |
Voir Nombres aléatoires |
asyncio |
tokio |
Voir Async/Await |
multiprocessing.Pool.map |
rayon : remplacez .iter() par .par_iter() |
|
itertools |
L’essentiel est intégré à Iterator ; la crate itertools ajoute le reste |
|
os, pathlib, subprocess |
std::env, std::path, std::fs, std::process |
Voir Lire et écrire des fichiers, Environnement et processus |
Côte à côte : une classe devient une struct + impl
Section intitulée « Côte à côte : une classe devient une struct + impl »Voici une petite classe Python :
class Account: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance
def deposit(self, amount): self.balance += amount
def __str__(self): return f"{self.owner} a {self.balance}"
account = Account("Ada", 100)account.deposit(50)print(account)Et la même chose en Rust :
use std::fmt;
struct Account { owner: String, balance: i64,}
impl Account { // `new` est juste une convention, pas un mot-clé. Pas d'arguments par défaut : // l'appelant passe donc toujours les deux valeurs. fn new(owner: &str, balance: i64) -> Self { Account { owner: owner.to_string(), balance } }
// `&mut self` : cette méthode modifie le compte. fn deposit(&mut self, amount: i64) { self.balance += amount; }}
// Voici le __str__ de Rust.impl fmt::Display for Account { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} a {}", self.owner, self.balance) }}
fn main() { let mut account = Account::new("Ada", 100); account.deposit(50); println!("{account}");}Sortie :
Ada a 150Quelques différences sautent aux yeux. Les champs sont déclarés d’emblée avec leur type : vous ne pouvez donc pas ajouter un nouvel attribut plus tard, comme Python vous laisse écrire account.nickname = "A". Les données (la struct) et le comportement (l’impl) vivent dans deux blocs séparés. Chaque méthode annonce si elle se contente de lire (&self) ou si elle modifie (&mut self) la valeur, et comme deposit modifie le compte, la variable doit être déclarée avec let mut. Rust n’a pas d’arguments par défaut : si vous voulez un compte qui démarre à zéro, ajoutez une deuxième fonction, comme fn empty(owner: &str) -> Self.
Côte à côte : le duck typing devient des traits
Section intitulée « Côte à côte : le duck typing devient des traits »Python ne s’intéresse pas à la classe d’un objet, seulement au fait qu’il a la bonne méthode :
class Duck: def speak(self): return "Coin-coin"
class Robot: def speak(self): return "Bip bop"
def introduce(thing): print(f"Il dit : {thing.speak()}")
for thing in [Duck(), Robot()]: introduce(thing)
introduce(42) # AttributeError, mais seulement quand cette ligne s'exécuteEn Rust, « a une méthode speak » s’écrit noir sur blanc sous la forme d’un trait, et chaque type y adhère avec un bloc impl :
trait Speak { fn speak(&self) -> String;}
struct Duck;struct Robot;
impl Speak for Duck { fn speak(&self) -> String { "Coin-coin".to_string() }}
impl Speak for Robot { fn speak(&self) -> String { "Bip bop".to_string() }}
// Accepte n'importe quel type qui implémente Speak.fn introduce(thing: &dyn Speak) { println!("Il dit : {}", thing.speak());}
fn main() { let things: Vec<Box<dyn Speak>> = vec![Box::new(Duck), Box::new(Robot)]; for thing in &things { introduce(thing.as_ref()); }
// introduce(&42); // erreur de compilation : `{integer}` n'implémente pas Speak}Sortie :
Il dit : Coin-coinIl dit : Bip bopC’est toujours du duck typing dans l’esprit, mais vérifié par le compilateur : introduce(&42) est une erreur avant même que le programme ne s’exécute, pas une AttributeError en production. Si vous avez utilisé typing.Protocol, les traits vous sembleront naturels. Une liste de types mélangés a besoin de Box<dyn Speak>, parce que Rust doit savoir comment appeler la méthode sur chaque élément (voir Trait objects). Et vous pouvez implémenter votre propre trait pour des types que vous n’avez pas écrits, comme String ou i32.
Côte à côte : None devient Option
Section intitulée « Côte à côte : None devient Option »En Python, n’importe quelle valeur peut être None en douce, et vous le découvrez au moment de l’utiliser :
def find_email(user_id): return emails.get(user_id) # renvoie None si la clé manque
print(find_email(1).upper())print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'En Rust, une valeur qui peut manquer a le type Option<T>. Vous ne pouvez pas l’utiliser comme une valeur ordinaire tant que vous n’avez pas traité le cas None :
fn find_email(user_id: u32) -> Option<String> { match user_id { _ => None, }}
fn main() { for id in [1, 3] { match find_email(id) { Some(email) => println!("{id} : {}", email.to_uppercase()), None => println!("{id} : aucune adresse e-mail enregistrée"), } }
// Comme `find_email(3) or "inconnue"` en Python. let email = find_email(3).unwrap_or("inconnue".to_string()); println!("{email}");
// Transformer la valeur à l'intérieur, s'il y en a une. let upper = find_email(2).map(|e| e.to_uppercase()); println!("{upper:?}");}Sortie :
3 : aucune adresse e-mail enregistréeinconnueSome("[email protected]")Le compilateur Rust refuse que vous appeliez .to_uppercase() sur une Option<String> : l’erreur « 'NoneType' object has no attribute » ne peut donc tout simplement pas se produire. Une String Rust est toujours une vraie chaîne. Découvrez les méthodes utiles sur la page Option, et la forme courte if let Some(email) = ... sur If Let / While Let.
Côte à côte : les exceptions deviennent Result et ?
Section intitulée « Côte à côte : les exceptions deviennent Result et ? »En Python, int() lève une exception, et vous l’attrapez quelque part plus haut dans la pile d’appels :
def parse_port(text): port = int(text) # peut lever ValueError if not 1 <= port <= 65535: raise ValueError(f"le port {port} est hors limites") return port
for text in ["8080", "http", "70000"]: try: print(f"port {parse_port(text)}") except ValueError as e: print(f"erreur : {e}")En Rust, une fonction qui peut échouer le dit dans son type de retour. L’opérateur ? veut dire « si ça a échoué, renvoie l’erreur à mon appelant tout de suite ». Il fait le même travail qu’une exception qui remonte, mais vous le voyez dans le code :
fn parse_port(text: &str) -> Result<u32, String> { let port: u32 = text .parse() .map_err(|e| format!("{text:?} n'est pas un nombre ({e})"))?;
if !(1..=65535).contains(&port) { return Err(format!("le port {port} est hors limites")); } Ok(port)}
fn main() { for text in ["8080", "http", "70000"] { match parse_port(text) { Ok(port) => println!("port {port}"), Err(e) => println!("erreur : {e}"), } }}Sortie :
port 8080erreur : "http" n'est pas un nombre (invalid digit found in string)erreur : le port 70000 est hors limitesLe message « invalid digit found in string » (chiffre invalide trouvé dans la chaîne) vient de la bibliothèque standard de Rust : il reste donc en anglais. La grande différence : en Python, n’importe quelle fonction peut lever une exception, et rien dans sa signature ne vous le dit. En Rust, Result fait partie de la signature, donc les appelants ne peuvent pas l’oublier. Une String comme type d’erreur convient très bien pendant l’apprentissage ; les vrais projets utilisent leurs propres enums d’erreur (Erreurs personnalisées) ou la crate anyhow, qui ressemble beaucoup à « lever n’importe quelle exception » (anyhow et thiserror).
Côte à côte : compréhensions et dicts deviennent itérateurs et HashMap
Section intitulée « Côte à côte : compréhensions et dicts deviennent itérateurs et HashMap »Les compréhensions de liste et le comptage dans un dict font partie du Python de tous les jours :
text = "the cat sat on the mat the end"
loud = [w.upper() for w in text.split() if len(w) == 3]print(loud)
counts = {}for word in text.split(): counts[word] = counts.get(word, 0) + 1
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:3]for word, n in top: print(f"{word}: {n}")En Rust, une compréhension devient une chaîne d’itérateurs qui se termine par collect(), et le dict devient une HashMap :
use std::collections::HashMap;
fn main() { let text = "the cat sat on the mat the end";
// [w.upper() for w in text.split() if len(w) == 3] let loud: Vec<String> = text .split_whitespace() .filter(|w| w.len() == 3) .map(|w| w.to_uppercase()) .collect(); println!("{loud:?}");
// counts[word] = counts.get(word, 0) + 1 let mut counts: HashMap<&str, u32> = HashMap::new(); for word in text.split_whitespace() { *counts.entry(word).or_insert(0) += 1; }
// Les plus fréquents d'abord ; en cas d'égalité, par ordre alphabétique. let mut top: Vec<(&str, u32)> = counts.into_iter().collect(); top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0))); for (word, n) in top.iter().take(3) { println!("{word}: {n}"); }}Sortie :
["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]the: 3cat: 1end: 1La phrase reste en anglais ici, parce que l’exemple filtre les mots selon leur longueur en octets et les trie par ordre alphabétique : des mots accentués changeraient le résultat. La chaîne se lit de gauche à droite au lieu de l’intérieur vers l’extérieur, mais l’idée est la même. Comme une expression génératrice, elle est paresseuse : rien ne se passe tant que collect(), sum(), count() ou une boucle for ne fait pas défiler les valeurs. collect() doit savoir quoi construire, c’est pourquoi loud a le type Vec<String>. Et remarquez l’étape de tri : un dict Python retient l’ordre d’insertion, mais pas une HashMap Rust, donc on trie toujours avant d’afficher. Le tri est expliqué sur la page Trier et rechercher.
Côte à côte : le GIL contre de vrais threads
Section intitulée « Côte à côte : le GIL contre de vrais threads »Dans CPython standard, le Global Interpreter Lock (GIL) ne laisse qu’un seul thread exécuter du bytecode Python à la fois. Les threads aident quand on attend le réseau, mais un travail gourmand en CPU comme celui-ci ne va pas plus vite avec plus de threads ; les développeurs Python se tournent donc vers multiprocessing :
from concurrent.futures import ThreadPoolExecutor
def is_prime(n): if n < 2: return False d = 2 while d * d <= n: if n % d == 0: return False d += 1 return True
def count_primes(lo, hi): return sum(1 for n in range(lo, hi) if is_prime(n))
ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)]with ThreadPoolExecutor(4) as pool: counts = list(pool.map(lambda r: count_primes(*r), ranges))print("total :", sum(counts))Rust n’a pas de GIL. Chaque thread::spawn est un vrai thread du système d’exploitation, et les threads tournent sur des cœurs différents en même temps :
use std::thread;
fn is_prime(n: u32) -> bool { if n < 2 { return false; } let mut d = 2; while d * d <= n { if n % d == 0 { return false; } d += 1; } true}
fn count_primes(lo: u32, hi: u32) -> usize { (lo..hi).filter(|&n| is_prime(n)).count()}
fn main() { let ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)];
// Chaque intervalle a son propre thread système. Ils tournent vraiment en même temps. let counts: Vec<usize> = thread::scope(|s| { let handles: Vec<_> = ranges .iter() .map(|&(lo, hi)| s.spawn(move || count_primes(lo, hi))) .collect(); handles.into_iter().map(|h| h.join().unwrap()).collect() });
for ((lo, hi), count) in ranges.iter().zip(&counts) { println!("{lo}..{hi} : {count} nombres premiers"); } println!("total : {}", counts.iter().sum::<usize>());}Sortie :
0..25000 : 2762 nombres premiers25000..50000 : 2371 nombres premiers50000..75000 : 2260 nombres premiers75000..100000 : 2199 nombres premierstotal : 9592thread::scope attend la fin de chaque thread avant de rendre la main : les threads peuvent donc emprunter des données locales sans danger. Sans GIL, vous craignez peut-être que deux threads modifient la même valeur en même temps. Rust règle ça à la compilation : les données partagées modifiables doivent être enveloppées dans quelque chose comme Arc<Mutex<T>>, sinon le programme ne compile pas (voir L’état partagé). Dans les vrais projets, la crate rayon rend tout ça encore plus court, un peu comme multiprocessing.Pool.map, mais avec des threads :
use rayon::prelude::*;
let total = (0..100_000u32).into_par_iter().filter(|&n| is_prime(n)).count();Python évolue aussi sur ce point : Python 3.13 a ajouté une version optionnelle « free-threaded », sans GIL. Mais le CPython standard que la plupart des gens installent et déploient a toujours le GIL, et beaucoup d’extensions C ne sont pas encore prêtes pour le free threading.
Écrire des extensions Python en Rust (PyO3 + maturin)
Section intitulée « Écrire des extensions Python en Rust (PyO3 + maturin) »Vous n’êtes pas obligé de quitter Python pour utiliser Rust. PyO3 vous permet d’écrire un module Python en Rust, et maturin le compile en un wheel normal que pip peut installer. C’est ainsi que sont faites Polars, pydantic-core et bien d’autres bibliothèques Python rapides.
Créez un projet (dans un environnement virtuel activé) :
pip install maturinmaturin new --bindings pyo3 fastmathcd fastmathmaturin écrit le Cargo.toml et le pyproject.toml pour vous. Placez ensuite vos fonctions Rust dans src/lib.rs :
use pyo3::prelude::*;
/// Un module Python écrit en Rust.#[pymodule]mod fastmath { use pyo3::prelude::*;
/// Additionne les carrés d'une liste de nombres. #[pyfunction] fn sum_of_squares(numbers: Vec<i64>) -> i64 { numbers.iter().map(|n| n * n).sum() }
/// Compte les mots d'un texte. #[pyfunction] fn word_count(text: &str) -> usize { text.split_whitespace().count() }}Compilez-le dans votre environnement virtuel avec maturin develop (ajoutez --release quand la vitesse compte), puis utilisez-le depuis Python comme n’importe quel autre module :
import fastmath
print(fastmath.sum_of_squares([1, 2, 3, 4])) # 30print(fastmath.word_count("the quick brown fox")) # 4fastmath.sum_of_squares(["a"]) # TypeError: 'str' object cannot be interpreted as an integerPyO3 convertit les types pour vous : une list Python d’entiers devient un Vec<i64>, une str devient un &str, et les commentaires de doc deviennent des docstrings Python. Les mauvais types se transforment en TypeError Python ordinaire. Quand vous êtes prêt à le partager, maturin build --release produit un wheel, et maturin publish l’envoie sur PyPI. Une bonne règle : mesurez d’abord, puis ne déplacez en Rust que la boucle critique. Passer de Python à Rust a un petit coût : un seul appel qui fait beaucoup de travail vaut mieux qu’un million de petits appels.
Les pièges quand on vient de Python
Section intitulée « Les pièges quand on vient de Python »Tous les développeurs qui viennent de Python butent sur les mêmes obstacles. Les connaître à l’avance aide.
L’affectation déplace la valeur
Section intitulée « L’affectation déplace la valeur »En Python, b = a vous donne deux noms pour la même liste. En Rust, let b = a; déplace (move) l’ownership d’un Vec ou d’une String vers b, et a ne peut plus être utilisé :
fn main() { let a = vec![1, 2, 3]; let mut b = a; b.push(4); println!("{a:?}");}error[E0382]: borrow of moved value: `a` --> src/main.rs:5:16 |2 | let a = vec![1, 2, 3]; | - move occurs because `a` has type `Vec<i32>`, which does not implement the `Copy` trait3 | let mut b = a; | - value moved here4 | b.push(4);5 | println!("{a:?}"); | ^ value borrowed here after move |help: consider cloning the value if the performance cost is acceptable |3 | let mut b = a.clone(); | ++++++++Le compilateur vous dit (en anglais) que a a été déplacé à la ligne 3, puis emprunté à la ligne 5 alors qu’il n’est plus valide, et il propose même de le cloner. La solution : emprunter (let b = &a;, si vous avez seulement besoin de lire) ou faire volontairement une vraie copie (a.clone()). Cette seule règle, l’ownership, est la nouvelle idée la plus importante pour vous. Lisez cette page et Le borrowing sans vous presser.
Le reste de la liste
Section intitulée « Le reste de la liste »- Les entiers ont une taille. L’
intde Python ne déborde jamais. Uni32Rust s’arrête à 2 147 483 647 ; aller au-delà provoque un panic dans un build debug. Choisissezi64ouu64pour les grands nombres, et voyez Le dépassement d’entier. - La division et le reste fonctionnent différemment.
7 / 2vaut3en Rust, parce que les deux côtés sont des entiers (le//de Python). Écrivez7.0 / 2.0pour obtenir3.5. Autre chose :-7 % 3vaut-1en Rust mais2en Python ; utilisez(-7_i32).rem_euclid(3)pour obtenir la réponse de Python. - Pas de mélange des types numériques.
let x: f64 = 2.5 * 3;ne compile pas, parce que3est un entier. Écrivez3.0, ou convertissez avecas f64(voir Conversion de types). - On ne peut pas indexer une chaîne.
word[0]est une erreur de compilation, parce que les chaînes Rust sont en UTF-8 et qu’un « caractère » peut occuper plusieurs octets. Utilisezword.chars().next()ouword.chars().nth(i). Voir Les chaînes. Stringcontre&str. Python a un seul typestr. Rust en a deux :Stringpossède son texte,&strl’emprunte. Les fonctions prennent en général un&stret les structs stockent en général uneString.- Les variables sont immuables par défaut. Écrivez
let mutquand une valeur doit changer. Le compilateur vous prévient si vous l’avez oublié. - Les accolades et les points-virgules. L’indentation ne compte pas pour Rust ; les
{ }, si. Et la dernière ligne d’une fonction sans point-virgule est sa valeur de retour :fn double(x: i32) -> i32 { x * 2 }. - Chaque
ifet chaquematchdoit être complet et bien typé. Pas de valeur de vérité implicite :if countne compile pas, vous écrivezif count > 0. Unmatchdoit couvrir tous les cas. - Une HashMap n’a pas d’ordre. Le
dictde Python retient l’ordre d’insertion ; laHashMapde Rust, non. Triez avant d’afficher, utilisez uneBTreeMap, ou ajoutez la crateindexmap. - Il y a une étape de compilation. Pas de REPL, et une compilation complète d’un projet avec beaucoup de crates prend du temps. Utilisez
cargo checkpendant que vous modifiez votre code (il saute la génération de code et va bien plus vite), etcargo run --releasequand vous mesurez les performances. - Le borrow checker va vous résister. Des schémas normaux en Python, comme plusieurs objets qui détiennent et modifient tous un même objet partagé, ou des objets qui pointent vers leur parent, demandent des pointeurs intelligents ou une conception différente. Attendez-vous à quelques semaines frustrantes. Ça devient beaucoup plus facile ensuite.
Un parcours d’apprentissage pour les développeurs Python
Section intitulée « Un parcours d’apprentissage pour les développeurs Python »Vous n’êtes pas obligé de lire ce site dans l’ordre. En tant que développeur Python, ce parcours prend son temps là où Rust diffère le plus de ce que vous connaissez :
- Débuter avec Rust et Les outils de Rust : installez Rust et lancez
cargo new. - Les variables, Les types de données et Le dépassement d’entier : les types sont la première nouvelle habitude.
- Les fonctions, If…Else et Les boucles : vite lus ; remarquez les expressions et les points-virgules absents.
- L’ownership, puis Le borrowing, puis Les slices : les pages les plus importantes pour vous. Prenez votre temps.
- Les chaînes :
Stringcontre&str, et pourquoi on ne peut pas indexer une chaîne. - Les structs et Les enums, puis Match et Le pattern matching.
- Option et Result, puis Erreurs personnalisées.
- Les vecteurs, HashMap et HashSet.
- Les closures et Les itérateurs : vos compréhensions et vos générateurs.
- Les traits, Les génériques, Les traits courants et Trait objects.
- Les modules, Cargo et les crates et Les tests.
- Les lifetimes et Pointeurs intelligents.
- La concurrence, L’état partagé et Async/Await.
- JSON avec Serde, puis construisez un outil CLI avec clap ou une API web.
Exercices
Section intitulée « Exercices »Les deux exercices vous donnent un extrait de code Python. Portez-le en Rust.
1. Porter une compréhension et un dict
Section intitulée « 1. Porter une compréhension et un dict »Portez ce code Python. Indice : pour afficher les noms dans l’ordre, collectez-les dans un Vec<&str>, appelez .sort(), puis .join(", "). Pour la moyenne, convertissez avec as f64 avant de diviser.
scores = {"Ada": 92, "Bob": 47, "Cy": 75, "Dee": 38}
passed = sorted(name for name, score in scores.items() if score >= 50)print("reçus :", ", ".join(passed))
odd_squares = [n * n for n in range(1, 10) if n % 2 == 1]print(odd_squares)
average = sum(scores.values()) / len(scores)print(f"moyenne : {average:.1f}")Voir la solution
use std::collections::HashMap;
fn main() { let scores: HashMap<&str, u32> = HashMap::from([("Ada", 92), ("Bob", 47), ("Cy", 75), ("Dee", 38)]);
let mut passed: Vec<&str> = scores .iter() .filter(|(_, score)| **score >= 50) .map(|(name, _)| *name) .collect(); passed.sort(); println!("reçus : {}", passed.join(", "));
let odd_squares: Vec<u32> = (1..10).filter(|n| n % 2 == 1).map(|n| n * n).collect(); println!("{odd_squares:?}");
let total: u32 = scores.values().sum(); let average = total as f64 / scores.len() as f64; println!("moyenne : {average:.1}");}Sortie :
reçus : Ada, Cy[1, 9, 25, 49, 81]moyenne : 63.0Parcourir une HashMap vous donne des paires de références, c’est pourquoi vous voyez **score et *name : ils suivent les références jusqu’aux valeurs. Le sort() n’est pas optionnel ici : sans lui, les noms pourraient sortir dans n’importe quel ordre. Et total as f64 est nécessaire, parce que Rust ne transforme jamais un entier en flottant dans votre dos.
2. Porter try/except vers Result
Section intitulée « 2. Porter try/except vers Result »Portez ce code Python. Chaque ligne doit avoir la forme nom: âge. Indice : line.split_once(':') renvoie une Option<(&str, &str)> ; transformez-la en Result avec .ok_or(...), puis utilisez ?. Faites renvoyer Result<(String, u32), String> à votre fonction.
def parse_line(line): if ":" not in line: raise ValueError("':' manquant") name, age = line.split(":", 1) try: return name.strip(), int(age.strip()) except ValueError as e: raise ValueError(f"âge invalide : {e}")
ages = {}for line in ["Ada: 36", "Grace 45", "Linus: quarante"]: try: name, age = parse_line(line) ages[name] = age print(f"ok : {name} a {age} ans") except ValueError as e: print(f"ignorée {line!r} : {e}")print(len(ages), "âge(s) enregistré(s)")Voir la solution
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> { let (name, age) = line.split_once(':').ok_or("':' manquant")?; let age: u32 = age.trim().parse().map_err(|e| format!("âge invalide : {e}"))?; Ok((name.trim().to_string(), age))}
fn main() { let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: quarante"] { match parse_line(line) { Ok((name, age)) => { println!("ok : {name} a {age} ans"); ages.insert(name, age); } Err(e) => println!("ignorée {line:?} : {e}"), } }
println!("{} âge(s) enregistré(s)", ages.len());}Sortie :
ok : Ada a 36 ansignorée "Grace 45" : ':' manquantignorée "Linus: quarante" : âge invalide : invalid digit found in string1 âge(s) enregistré(s)Les deux raise sont devenus des valeurs Err, et le try/except est devenu un match. Le ? après ok_or("':' manquant") fonctionne même si l’erreur est un &str alors que la fonction renvoie des erreurs de type String, parce que ? convertit l’erreur avec From. Remarquez l’ordre dans la branche Ok : on affiche d’abord, puis insert déplace name dans la map. Inversez ces deux lignes et le compilateur vous dira que name a été déplacé.
Récapitulatif
Section intitulée « Récapitulatif »- Une grande partie de Python se retrouve en Rust : les boucles sur des itérateurs, les tuples, les closures,
match, et le traitement de données à la manière des compréhensions (sous forme de chaînes d’itérateurs). - Les grands changements : des types statiques vérifiés avant l’exécution, l’ownership au lieu d’un ramasse-miettes,
Optionau lieu deNone, etResult+?au lieu des exceptions. - Les classes deviennent une
struct+impl, le duck typing devient des traits, et les blocswithdeviennent unDropautomatique. UneHashMapn’a pas d’ordre, et les entiers ont une taille fixe. - Rust n’a pas de GIL : les threads s’exécutent en parallèle, et le compilateur empêche les accès concurrents aux données.
- Cargo remplace pip, venv et compagnie ;
cargo fmt,cargo clippyetcargo testremplacent black, ruff et pytest. - Vous pouvez mélanger les deux langages : PyO3 et maturin transforment du code Rust en module Python.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Qu'est-ce que Rust utilise à la place du None de Python ?
2. En Python, b = a donne deux noms à la même liste. Que fait let b = a; avec un Vec en Rust ?
3. Que vaut 7 / 2 en Rust, quand les deux nombres sont des entiers ?
4. Vous voulez accélérer une fonction lente d'un projet Python avec Rust. Quels outils utilisez-vous ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- The Rust Programming Language (en anglais) : le livre officiel. Les chapitres 4 (ownership), 6 (enums et
Option), 9 (erreurs) et 13 (closures et itérateurs) sont les plus importants pour les développeurs Python. - PyO3 user guide (en anglais) : tout sur l’appel de Rust depuis Python (et de Python depuis Rust).
- Maturin user guide (en anglais) : compiler, tester et publier des paquets Python propulsés par Rust.
- Rust by Example (en anglais) : de courts exemples exécutables, parfaits si vous apprenez mieux en lisant du code.
- Are We Learning Yet? (en anglais) : un panorama des crates Rust pour le machine learning et les données.