Salta ai contenuti

Rust per sviluppatori Python: guida pratica

Se conosci Python, conosci già molte idee di Rust. Rust ha cicli for sugli iteratori, tuple, closure (le lambda di Python, ma più utili), un match che funziona come il match di Python, e catene di iteratori che fanno quello che fanno le list comprehension. Rust ha perfino preso un po’ del gusto di Python: un solo modo ovvio di formattare il codice, uno strumento per i pacchetti amichevole e messaggi di errore che fanno di tutto per aiutarti.

La novità riguarda soprattutto il compilatore. Python controlla i tipi mentre il programma gira; Rust li controlla tutti prima che parta. Python libera la memoria con un garbage collector; Rust usa l’ownership (possesso) e il prestito (borrowing), controllati in compilazione. Python ha None e le eccezioni; Rust ha Option e Result. È su queste tre cose che passerai la maggior parte del tempo di studio.

Ma perché uno sviluppatore Python dovrebbe imparare Rust? Le ragioni oneste sono queste:

  • Velocità. Rust viene compilato in codice macchina nativo. Il lavoro pesante per la CPU che in Python puro è lento (parsing, calcoli numerici, grandi cicli) di solito in Rust è molto più veloce, senza trucchi di ottimizzazione.
  • Thread veri. Nel CPython standard, il GIL lascia eseguire codice Python a un solo thread alla volta. I thread di Rust girano su tutti i core della CPU nello stesso momento, e il compilatore blocca le data race prima che il programma parta.
  • Bug scoperti prima. Un errore di battitura nel nome di un attributo, un None che avevi dimenticato, una funzione chiamata con il tipo sbagliato: in Python li scopri quando quella riga viene eseguita. In Rust il programma non compila finché non li correggi.
  • Non devi scegliere. Con PyO3 (in inglese) e maturin puoi scrivere in Rust la parte lenta di un progetto Python e importarla con import da Python come qualsiasi altro modulo. Strumenti Python molto diffusi come Polars, Ruff e uv sono scritti in Rust.

Se invece cerchi un confronto generale di Rust con Python, Go e C++, guarda Rust vs C++, Go e Python.

Usa questa tabella come un frasario. L’ultima colonna rimanda alla lezione di questo sito che spiega il lato Rust.

Python Rust Per saperne di più
Tipi dinamici, controllati durante l’esecuzione Tipi statici, controllati in compilazione. Rust ne deduce quasi tutti, quindi scrivi meno tipi di quanto pensi Tipi di dati
Type hint (def f(x: int) -> str) I tipi nelle firme delle funzioni sono obbligatori, e il compilatore li fa rispettare Funzioni
x = 5 let x = 5; (non si può cambiare). let mut x = 5; se vuoi cambiarlo Variabili
int (cresce quanto serve) Interi di dimensione fissa: i32, i64, u64, usize, i128… Possono andare in overflow Tipi di dati, Overflow degli interi
float f64 (o f32) Tipi di dati
str String (posseduta, può crescere) e &str (una vista presa in prestito su un testo) Stringhe
f-string: f"Hi {name}" format!("Hi {name}") e println!("Hi {name}") Output
print(repr(x)) / __repr__ println!("{x:?}") con #[derive(Debug)] Output, Trait comuni
__str__ Implementa Display (e poi funziona anche .to_string()) Trait comuni
__eq__, __hash__, __lt__ Derive di PartialEq, Eq, Hash, PartialOrd, Ord Trait comuni
None, Optional[T] Option<T>: o Some(valore) o None Option
x or default x.unwrap_or(default) Option
raise, try/except Result<T, E> + l’operatore ? Result, Errori personalizzati
Un’eccezione per un vero bug (assert, IndexError) panic! (ferma il thread) Panic
class con __init__ e metodi struct per i dati + un blocco impl per i metodi; new è una funzione normale Struct
@dataclass struct con #[derive(Debug, Clone, PartialEq)] Struct
self &self (legge), &mut self (modifica) o self (prende l’ownership) Struct
Ereditarietà Niente ereditarietà. Usa la composizione (una struct dentro una struct) + trait con metodi predefiniti Trait, Design pattern
Duck typing, Protocol, ABC trait Trait
Una lista di oggetti di classi diverse Vec<Box<dyn Trait>> (un trait object) Trait object
TypeVar / funzioni generiche Generici con vincoli di trait (trait bound): fn largest<T: Ord>(items: &[T]) Generici
Classe Enum enum, in cui ogni variante può portare i propri dati Enum
match / case (3.10+) match (il compilatore controlla che tu abbia coperto ogni caso) match, Pattern matching
if my_list: (valori «truthy») Sii esplicito: if !my_list.is_empty(). In un if va solo un bool if/else
for i in range(10) for i in 0..10 Cicli for
enumerate(xs), zip(a, b) xs.iter().enumerate(), a.iter().zip(b.iter()) Iteratori
lambda x: x + 1 Closure: |x| x + 1 (e possono occupare molte righe) Closure
List comprehension, map, filter Adattatori di iteratori: filter, map, poi collect() Iteratori
Generatori e yield Un tuo tipo che implementa Iterator, o std::iter::from_fn Iteratori personalizzati
with open(...) as f: Non devi scrivere niente: file e lock si chiudono quando il loro proprietario esce dallo scope (Drop) Scope, I/O su file
Argomenti predefiniti, *args, **kwargs Non supportati. Usa parametri Option, slice (&[T]), una struct con Default, o un builder Funzioni, Design pattern
Decoratori Nessun equivalente diretto. Avvolgi con le closure, o usa macro attributo (come #[tokio::main]) Closure, Macro procedurali
import e moduli mod e use Moduli
Pacchetto su PyPI Crate su crates.io Cargo e crate
if __name__ == "__main__": fn main() Primi passi
threading (con il GIL) std::thread: thread davvero paralleli Concorrenza
multiprocessing, concurrent.futures Thread, o il crate rayon per iteratori paralleli Concorrenza, Stato condiviso
asyncio, async def, await async fn e .await, con un runtime come tokio Async/await
7 // 2, 2 ** 10 7 / 2 (divisione intera sugli interi), 2_i64.pow(10) Operatori
int("42"), str(42) "42".parse::<i32>() (restituisce un Result), 42.to_string() Conversione di tipo, Conversioni

Tutte le collezioni integrate di Python hanno un posto nella libreria standard di Rust. Le differenze più grandi: una collezione Rust contiene un solo tipo (un Vec<i32> contiene solo i32), e una HashMap non ricorda l’ordine di inserimento.

Python Rust Per saperne di più
list Vec<T> Vettori
tuple Tuple (i32, String), o una piccola struct con campi con nome Tuple
dict HashMap<K, V> (nessun ordine fisso!) HashMap
dict quando ti servono le chiavi ordinate BTreeMap<K, V> HashMap
dict quando ti serve l’ordine di inserimento Il crate indexmap
set, frozenset HashSet<T>, BTreeSet<T> HashSet
collections.Counter, defaultdict HashMap con l’API entry: *map.entry(k).or_insert(0) += 1 HashMap
collections.deque VecDeque<T> VecDeque e BinaryHeap
heapq (prima il più piccolo) BinaryHeap<T> (prima il più grande; avvolgi gli elementi in Reverse per avere prima il più piccolo) VecDeque e BinaryHeap
namedtuple struct Struct
bytes, bytearray &[u8], Vec<u8> Slice
Slicing: xs[1:3] Slice: &xs[1..3] (niente indici negativi; usa xs.len() - 1) Slice

Qui molti sviluppatori Python tirano un sospiro di sollievo. Rust ha un solo strumento ufficiale, Cargo, che fa il lavoro di pip, venv, setuptools, pytest e altro ancora. Non esiste il dibattito «quale strumento di packaging usiamo?».

Python Rust Note
Interprete python Compilatore rustc, di solito chiamato tramite cargo Prima compili, poi esegui un binario nativo. Vedi Primi passi
pyenv rustup (e un file rust-toolchain.toml per fissare una versione) Vedi Strumenti e configurazione
pip, poetry, uv cargo Compila, esegue, testa, aggiunge pacchetti, pubblica. Vedi Cargo e crate
pip install requests cargo add reqwest Aggiunge il crate al Cargo.toml solo per questo progetto
pyproject.toml / requirements.txt Cargo.toml
poetry.lock / uv.lock Cargo.lock Viene creato per te
venv / virtualenv Non serve Ogni progetto ha le sue dipendenze e compila nella sua cartella target/
PyPI crates.io La documentazione delle API di ogni crate pubblicato è su docs.rs (in inglese)
python main.py cargo run (cargo run --release per la massima velocità) Le build di debug sono molto più lente in esecuzione; misura la velocità in modalità release
pipx install ruff cargo install ripgrep Installa uno strumento da riga di comando
PyInstaller cargo build --release Ottieni un unico binario nativo; gli utenti non devono installare Rust
black cargo fmt Un solo stile standard per tutti
ruff, pylint, flake8 cargo clippy Centinaia di lint utili, ufficiali
mypy, pyright Il compilatore stesso Il controllo dei tipi non è facoltativo e non è un passaggio separato
pytest, unittest #[test] integrato e cargo test (cargo nextest è un runner più veloce molto usato) Vedi Test
Fixture di pytest e parametrize rstest
hypothesis proptest Test basati sulle proprietà. Vedi Test avanzati
unittest.mock mockall, o finti oggetti scritti a mano che implementano il tuo trait
timeit, pytest-benchmark criterion Vedi Prestazioni
cProfile, py-spy cargo flamegraph, perf, o qualsiasi profiler nativo
Docstring + Sphinx Commenti di documentazione /// in Markdown + cargo doc Gli esempi di codice nella documentazione vengono eseguiti come test. Vedi Documentazione
La REPL di Python, Jupyter Il Rust Playground; evcxr_repl e evcxr_jupyter Rust non ha una REPL integrata. Il modo abituale per sperimentare sono piccoli progetti con cargo run
VS Code + Pylance, PyCharm VS Code + rust-analyzer, o JetBrains RustRover rust-analyzer offre completamento, vai-alla-definizione ed errori in linea

Ecco un piccolo Cargo.toml, così puoi confrontarlo con 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"] }

Librerie e framework: «cosa usiamo al posto di…?»

Sezione intitolata “Librerie e framework: «cosa usiamo al posto di…?»”

«Uso requests, FastAPI e pandas. Qual è la versione Rust?» Ecco gli equivalenti più vicini. L’ecosistema di Python è molto più vecchio e più grande, quindi in alcune aree (soprattutto la data science) Rust ha opzioni più giovani e meno numerose.

Python Rust Note
Flask, FastAPI axum (ne costruiamo una in Progetto: API web), actix-web Gli handler sono semplici async fn; i tipi di richiesta e risposta vengono dalle tue struct, un po’ come FastAPI con pydantic
Django Nessun equivalente completo. Loco (loco-rs) è un framework «batterie incluse»; la maggior parte dei team combina axum + un ORM + un motore di template Nessun pannello di amministrazione integrato
requests, httpx reqwest (asincrono, con un client bloccante opzionale), ureq (semplice e bloccante) Vedi Reti
aiohttp reqwest + tokio
Jinja2 minijinja (dello stesso autore di Jinja), tera, askama (template controllati in compilazione)
BeautifulSoup scraper
Python Rust Note
ORM di SQLAlchemy Diesel (query controllate sui tipi in compilazione), SeaORM (asincrono, stile a entità) Database in Rust li mostra entrambi con codice funzionante
psycopg, SQL scritto a mano sqlx: SQL semplice, controllato sul tuo vero database in compilazione Asincrono, molto popolare, non è un ORM
sqlite3 rusqlite
redis-py redis
Celery apalis per le code di job; tokio::spawn per task in background nel processo
Python Rust Note
pandas Polars Scritto in Rust; la sua API Python, che forse conosci già, è costruita sulla stessa libreria Rust
NumPy ndarray Array a N dimensioni con slicing e broadcasting; molti meno extra di NumPy
scikit-learn linfa, smartcore Meno algoritmi e comunità molto più piccole
PyTorch candle (crate candle-core), burn Buoni per eseguire modelli; quasi tutto l’addestramento si fa ancora in Python
matplotlib plotters
Modulo csv Crate csv + serde
Python Rust Note
json serde + serde_json Lo standard; gestisce anche TOML, YAML e altro. Vedi JSON con Serde
pydantic serde per il parsing + validator o garde per le regole di validazione La tua struct è lo schema
argparse, click, typer clap Definisci gli argomenti come una struct. Vedi Progetto: tool CLI con clap
logging tracing, o log + env_logger Vedi Logging
re regex Niente look-around né backreference, ma tempo lineare garantito; fancy-regex li ha. Vedi Espressioni regolari
datetime chrono o jiff Vedi Date e orari
random rand Vedi Numeri casuali
asyncio tokio Vedi Async/await
multiprocessing.Pool.map rayon: cambia .iter() in .par_iter()
itertools Quasi tutto è già integrato in Iterator; il crate itertools aggiunge il resto
os, pathlib, subprocess std::env, std::path, std::fs, std::process Vedi I/O su file, Ambiente e processi

Ecco una piccola 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} has {self.balance}"
account = Account("Ada", 100)
account.deposit(50)
print(account)

E la stessa cosa in Rust:

use std::fmt;
struct Account {
owner: String,
balance: i64,
}
impl Account {
// `new` è solo una convenzione, non una parola chiave. Niente argomenti
// predefiniti, quindi chi chiama passa sempre entrambi i valori.
fn new(owner: &str, balance: i64) -> Self {
Account { owner: owner.to_string(), balance }
}
// `&mut self`: questo metodo modifica il conto.
fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
}
// Questo è il __str__ di Rust.
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ha {}", self.owner, self.balance)
}
}
fn main() {
let mut account = Account::new("Ada", 100);
account.deposit(50);
println!("{account}");
}

Output:

Ada ha 150
▶ Provalo tu

Alcune cose sono diverse. I campi vengono dichiarati subito con i loro tipi, quindi non puoi aggiungere un nuovo attributo più tardi, come Python ti lascia fare con account.nickname = "A". Dati (la struct) e comportamento (l’impl) vivono in due blocchi separati. Ogni metodo dichiara se si limita a leggere (&self) o se modifica (&mut self) il valore, e siccome deposit modifica il conto, la variabile deve essere dichiarata let mut. Rust non ha argomenti predefiniti: se vuoi un conto che parte da zero, aggiungi una seconda funzione, come fn empty(owner: &str) -> Self.

A Python non interessa di che classe sia un oggetto, solo che abbia il metodo giusto:

class Duck:
def speak(self):
return "Quack"
class Robot:
def speak(self):
return "Beep boop"
def introduce(thing):
print(f"It says: {thing.speak()}")
for thing in [Duck(), Robot()]:
introduce(thing)
introduce(42) # AttributeError, ma solo quando questa riga viene eseguita

In Rust, «ha un metodo speak» si scrive come un trait, e ogni tipo aderisce con un blocco impl:

trait Speak {
fn speak(&self) -> String;
}
struct Duck;
struct Robot;
impl Speak for Duck {
fn speak(&self) -> String {
"Quack".to_string()
}
}
impl Speak for Robot {
fn speak(&self) -> String {
"Beep boop".to_string()
}
}
// Accetta qualsiasi tipo che implementa Speak.
fn introduce(thing: &dyn Speak) {
println!("Dice: {}", 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); // errore di compilazione: `{integer}` non implementa Speak
}

Output:

Dice: Quack
Dice: Beep boop
▶ Provalo tu

Nello spirito è ancora duck typing, ma controllato dal compilatore: introduce(&42) è un errore prima ancora che il programma parta, non un AttributeError in produzione. Se hai usato typing.Protocol, i trait ti sembreranno naturali. Una lista di tipi diversi ha bisogno di Box<dyn Speak>, perché Rust deve sapere come chiamare il metodo su ogni elemento (vedi Trait object). E puoi implementare un tuo trait per tipi che non hai scritto tu, come String o i32.

In Python qualsiasi valore può essere, di nascosto, None, e te ne accorgi quando lo usi:

def find_email(user_id):
emails = {1: "[email protected]", 2: "[email protected]"}
return emails.get(user_id) # restituisce None se manca
print(find_email(1).upper())
print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'

In Rust un valore che potrebbe mancare ha il tipo Option<T>. Non puoi usarlo come un valore normale finché non hai gestito il caso None:

fn find_email(user_id: u32) -> Option<String> {
match user_id {
1 => Some("[email protected]".to_string()),
2 => Some("[email protected]".to_string()),
_ => None,
}
}
fn main() {
for id in [1, 3] {
match find_email(id) {
Some(email) => println!("{id}: {}", email.to_uppercase()),
None => println!("{id}: nessuna email registrata"),
}
}
// Come `find_email(3) or "unknown"` in Python.
let email = find_email(3).unwrap_or("sconosciuta".to_string());
println!("{email}");
// Trasforma il valore contenuto, se c'è.
let upper = find_email(2).map(|e| e.to_uppercase());
println!("{upper:?}");
}

Output:

3: nessuna email registrata
sconosciuta
▶ Provalo tu

Il compilatore Rust non ti lascia chiamare .to_uppercase() su un Option<String>, quindi l’errore «'NoneType' object has no attribute» semplicemente non può capitare. Una String di Rust è sempre una stringa vera. Impara i metodi di supporto nella pagina Option, e la forma breve if let Some(email) = ... in if let.

Fianco a fianco: le eccezioni diventano Result e ?

Sezione intitolata “Fianco a fianco: le eccezioni diventano Result e ?”

In Python int() lancia un’eccezione, e tu la catturi da qualche parte più in alto nello stack delle chiamate:

def parse_port(text):
port = int(text) # può lanciare ValueError
if not 1 <= port <= 65535:
raise ValueError(f"port {port} is out of range")
return port
for text in ["8080", "http", "70000"]:
try:
print(f"port {parse_port(text)}")
except ValueError as e:
print(f"error: {e}")

In Rust una funzione che può fallire lo dichiara nel tipo di ritorno. L’operatore ? significa «se questa cosa è fallita, restituisci subito l’errore a chi mi ha chiamato». Fa lo stesso lavoro di un’eccezione che risale, ma lo vedi nel codice:

fn parse_port(text: &str) -> Result<u32, String> {
let port: u32 = text
.parse()
.map_err(|e| format!("{text:?} non è un numero ({e})"))?;
if !(1..=65535).contains(&port) {
return Err(format!("la porta {port} è fuori intervallo"));
}
Ok(port)
}
fn main() {
for text in ["8080", "http", "70000"] {
match parse_port(text) {
Ok(port) => println!("porta {port}"),
Err(e) => println!("errore: {e}"),
}
}
}

Output:

porta 8080
errore: "http" non è un numero (invalid digit found in string)
errore: la porta 70000 è fuori intervallo
▶ Provalo tu

Il testo tra parentesi, «invalid digit found in string» (cifra non valida nella stringa), arriva dalla libreria standard ed è sempre in inglese.

La grande differenza: in Python qualsiasi funzione potrebbe lanciare un’eccezione, e niente nella sua firma te lo dice. In Rust Result fa parte della firma, quindi chi chiama non può dimenticarsene. Mentre impari, String va benissimo come tipo di errore; i progetti veri usano i propri enum di errore (Errori personalizzati) o il crate anyhow, che assomiglia molto a «lancia un’eccezione qualsiasi» (anyhow e thiserror).

Fianco a fianco: comprehension e dict diventano iteratori e HashMap

Sezione intitolata “Fianco a fianco: comprehension e dict diventano iteratori e HashMap”

Le list comprehension e il conteggio con un dict sono il pane quotidiano di Python:

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}")

In Rust una comprehension diventa una catena di iteratori che termina con collect(), e il dict diventa una 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;
}
// Prima le più frequenti; a parità, in ordine alfabetico.
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}");
}
}

Output:

["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]
the: 3
cat: 1
end: 1
▶ Provalo tu

Il testo resta in inglese perché il risultato dipende dalla lunghezza delle parole, dalle maiuscole e dall’ordine alfabetico. La catena si legge da sinistra a destra invece che dall’interno verso l’esterno, ma l’idea è la stessa. Come un’espressione generatore, è pigra (lazy): non succede niente finché collect(), sum(), count() o un ciclo for non tirano fuori i valori. collect() deve sapere cosa costruire, ed è per questo che loud ha il tipo Vec<String>. E nota il passaggio di ordinamento: un dict di Python ricorda l’ordine di inserimento, una HashMap di Rust no, quindi ordiniamo sempre prima di stampare. L’ordinamento è spiegato nella pagina Ordinamento e ricerca.

Nel CPython standard, il Global Interpreter Lock (GIL) lascia eseguire bytecode Python a un solo thread alla volta. I thread aiutano quando aspetti la rete, ma un lavoro pesante per la CPU come questo non diventa più veloce con più thread, così gli sviluppatori Python ricorrono a 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 non ha il GIL. Ogni thread::spawn è un vero thread del sistema operativo, e i thread girano su core diversi della CPU nello stesso momento:

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)];
// Ogni intervallo ha il suo thread del sistema operativo. Girano davvero in parallelo.
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} numeri primi");
}
println!("totale: {}", counts.iter().sum::<usize>());
}

Output:

0..25000: 2762 numeri primi
25000..50000: 2371 numeri primi
50000..75000: 2260 numeri primi
75000..100000: 2199 numeri primi
totale: 9592
▶ Provalo tu

thread::scope aspetta tutti i thread prima di restituire il controllo, quindi i thread possono prendere in prestito i dati locali senza rischi. Senza GIL potresti temere che due thread modifichino lo stesso valore nello stesso momento. Rust se ne occupa in compilazione: i dati condivisi che possono cambiare vanno avvolti in qualcosa come Arc<Mutex<T>>, altrimenti il programma non compila (vedi Stato condiviso). Nei progetti veri il crate rayon rende tutto ancora più breve, un po’ come multiprocessing.Pool.map ma con i thread:

use rayon::prelude::*;
let total = (0..100_000u32).into_par_iter().filter(|&n| is_prime(n)).count();

Anche Python sta cambiando su questo punto: Python 3.13 ha aggiunto una build «free-threaded» opzionale, senza GIL. Ma il CPython standard che la maggior parte delle persone installa e mette in produzione ha ancora il GIL, e molte estensioni in C non sono ancora pronte per il free threading.

Scrivere estensioni Python in Rust (PyO3 + maturin)

Sezione intitolata “Scrivere estensioni Python in Rust (PyO3 + maturin)”

Non devi lasciare Python per usare Rust. PyO3 (in inglese) ti permette di scrivere un modulo Python in Rust, e maturin (in inglese) lo trasforma in una normale wheel che pip può installare. È così che sono fatte Polars, pydantic-core e molte altre librerie Python veloci.

Prepara un progetto (dentro un ambiente virtuale attivato):

Terminal window
pip install maturin
maturin new --bindings pyo3 fastmath
cd fastmath

maturin scrive per te il Cargo.toml e il pyproject.toml. Poi metti le tue funzioni Rust in src/lib.rs:

use pyo3::prelude::*;
/// Un modulo Python scritto in Rust.
#[pymodule]
mod fastmath {
use pyo3::prelude::*;
/// Somma i quadrati di una lista di numeri.
#[pyfunction]
fn sum_of_squares(numbers: Vec<i64>) -> i64 {
numbers.iter().map(|n| n * n).sum()
}
/// Conta le parole in un testo.
#[pyfunction]
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
}

Compilalo nel tuo ambiente virtuale con maturin develop (aggiungi --release quando ti interessa la velocità), e usalo da Python come qualsiasi altro modulo:

import fastmath
print(fastmath.sum_of_squares([1, 2, 3, 4])) # 30
print(fastmath.word_count("the quick brown fox")) # 4
fastmath.sum_of_squares(["a"]) # TypeError: 'str' object cannot be interpreted as an integer

PyO3 converte i tipi per te: una list Python di interi diventa un Vec<i64>, una str diventa un &str, e i commenti di documentazione diventano docstring Python. I tipi sbagliati diventano un normale TypeError di Python. Quando vuoi condividerlo, maturin build --release crea una wheel e maturin publish la carica su PyPI. Una buona regola: prima misura, poi sposta in Rust solo il ciclo critico. Passare da Python a Rust ha un piccolo costo, quindi una sola chiamata che fa tanto lavoro batte un milione di chiamate minuscole.

Le cose che ti faranno inciampare se vieni da Python

Sezione intitolata “Le cose che ti faranno inciampare se vieni da Python”

Chiunque arrivi da Python prende le stesse buche. Conoscerle in anticipo aiuta.

In Python, b = a ti dà due nomi per la stessa lista. In Rust, let b = a; sposta l’ownership di un Vec o di una String in b, e a non si può più usare:

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` trait
3 | let mut b = a;
| - value moved here
4 | 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();
| ++++++++

Il compilatore dice che a è stato spostato in b e poi usato di nuovo, e suggerisce di clonarlo. La soluzione è prendere in prestito (let b = &a;, se ti serve solo leggere) o fare di proposito una copia vera (a.clone()). Questa regola, l’ownership, è l’idea nuova più importante per te. Leggi con calma quella pagina e Prestito.

  • Gli interi hanno una dimensione. L’int di Python non va mai in overflow. Un i32 di Rust si ferma a 2.147.483.647; andare oltre provoca un panic in una build di debug. Scegli i64 o u64 per i numeri grandi, e vedi Overflow degli interi.
  • Divisione e resto funzionano in modo diverso. In Rust 7 / 2 vale 3, perché entrambi i lati sono interi (il // di Python). Scrivi 7.0 / 2.0 per ottenere 3.5. Inoltre -7 % 3 vale -1 in Rust ma 2 in Python; usa (-7_i32).rem_euclid(3) per avere la risposta di Python.
  • Niente mescolanza di tipi numerici. let x: f64 = 2.5 * 3; non compila, perché 3 è un intero. Scrivi 3.0, o converti con as f64 (vedi Conversione di tipo).
  • Non puoi indicizzare una stringa. word[0] è un errore di compilazione, perché le stringhe di Rust sono UTF-8 e un «carattere» può occupare più byte. Usa word.chars().next() o word.chars().nth(i). Vedi Stringhe.
  • String contro &str. Python ha un solo tipo str. Rust ne ha due: String possiede il suo testo, &str lo prende in prestito. Di solito le funzioni accettano &str e le struct contengono String.
  • Le variabili sono immutabili per default. Scrivi let mut quando un valore deve cambiare. Se te lo dimentichi, il compilatore te lo dice.
  • Graffe e punti e virgola. A Rust l’indentazione non importa; le { } sì. E l’ultima riga di una funzione senza punto e virgola è il suo valore di ritorno: fn double(x: i32) -> i32 { x * 2 }.
  • Ogni if e ogni match devono essere completi e con i tipi giusti. Niente valori «truthy»: if count non compila, scrivi if count > 0. Un match deve coprire ogni caso.
  • Una HashMap non ha ordine. Il dict di Python ricorda l’ordine di inserimento; la HashMap di Rust no. Ordina prima di stampare, usa una BTreeMap, o aggiungi il crate indexmap.
  • C’è un passaggio di compilazione. Niente REPL, e una build pulita di un progetto con molti crate richiede un po’ di tempo. Usa cargo check mentre scrivi (salta la generazione del codice ed è molto più veloce), e cargo run --release quando misuri la velocità.
  • Il borrow checker farà resistenza. Schemi normali in Python, come tanti oggetti che contengono e modificano un unico oggetto condiviso, o oggetti che puntano al loro genitore, richiedono smart pointer o un design diverso. Mettiti in conto qualche settimana frustrante. Poi diventa molto più facile.

Non devi leggere questo sito in ordine. Da sviluppatore Python, questo percorso si sofferma dove Rust è più diverso da quello che conosci:

  1. Primi passi e Strumenti e configurazione: installa Rust ed esegui cargo new.
  2. Variabili, Tipi di dati e Overflow degli interi: i tipi sono la prima abitudine nuova.
  3. Funzioni, If…else e Cicli: letture veloci; fai caso alle espressioni e ai punti e virgola mancanti.
  4. Ownership, poi Prestito, poi Slice: le pagine più importanti per te. Prenditi il tuo tempo.
  5. Stringhe: String contro &str, e perché non puoi indicizzare una stringa.
  6. Struct ed Enum, poi match e Pattern matching.
  7. Option e Result, poi Errori personalizzati.
  8. Vettori, HashMap e HashSet.
  9. Closure e Iteratori: le tue comprehension e i tuoi generatori.
  10. Trait, Generici, Trait comuni e Trait object.
  11. Moduli, Cargo e crate e Test.
  12. Lifetime e Smart pointer.
  13. Concorrenza, Stato condiviso e Async/await.
  14. JSON con Serde, poi costruisci un tool CLI con clap o un’API web.

Entrambi gli esercizi ti danno un frammento di Python. Portalo in Rust.

Porta questo codice Python. Suggerimento: per stampare i nomi in ordine, raccoglili in un Vec<&str>, chiama .sort() e poi .join(", "). Per la media, converti con as f64 prima di dividere.

scores = {"Ada": 92, "Bob": 47, "Cy": 75, "Dee": 38}
passed = sorted(name for name, score in scores.items() if score >= 50)
print("passed:", ", ".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"average: {average:.1f}")
Mostra la soluzione
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!("promossi: {}", 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!("media: {average:.1}");
}

Output:

promossi: Ada, Cy
[1, 9, 25, 49, 81]
media: 63.0
▶ Provalo tu

Iterare su una HashMap ti dà coppie di riferimenti, ed è per questo che vedi **score e *name: seguono i riferimenti fino ai valori. Qui il sort() non è facoltativo: senza, i nomi potrebbero uscire in un ordine qualsiasi. E total as f64 serve perché Rust non trasforma mai un intero in un float alle tue spalle.

Porta questo codice Python. Ogni riga dovrebbe essere nome: età. Suggerimento: line.split_once(':') restituisce un Option<(&str, &str)>; trasformalo in un Result con .ok_or(...), poi usa ?. Fai in modo che la tua funzione restituisca Result<(String, u32), String>.

def parse_line(line):
if ":" not in line:
raise ValueError("missing ':'")
name, age = line.split(":", 1)
try:
return name.strip(), int(age.strip())
except ValueError as e:
raise ValueError(f"bad age: {e}")
ages = {}
for line in ["Ada: 36", "Grace 45", "Linus: forty"]:
try:
name, age = parse_line(line)
ages[name] = age
print(f"ok: {name} is {age}")
except ValueError as e:
print(f"skipped {line!r}: {e}")
print(len(ages), "saved")
Mostra la soluzione
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> {
let (name, age) = line.split_once(':').ok_or("manca ':'")?;
let age: u32 = age.trim().parse().map_err(|e| format!("età non valida: {e}"))?;
Ok((name.trim().to_string(), age))
}
fn main() {
let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: forty"] {
match parse_line(line) {
Ok((name, age)) => {
println!("ok: {name} ha {age} anni");
ages.insert(name, age);
}
Err(e) => println!("saltata {line:?}: {e}"),
}
}
println!("salvati: {}", ages.len());
}

Output:

ok: Ada ha 36 anni
saltata "Grace 45": manca ':'
saltata "Linus: forty": età non valida: invalid digit found in string
salvati: 1
▶ Provalo tu

Entrambi i raise sono diventati valori Err, e il try/except è diventato un match. Il ? dopo ok_or("manca ':'") funziona anche se l’errore è un &str e la funzione restituisce errori String, perché ? converte l’errore con From. Nota l’ordine dentro Ok: prima stampiamo, poi insert sposta name nella mappa. Scambia quelle due righe e il compilatore ti dirà che name è stato spostato.

  • Molto di Python si porta dietro: cicli sugli iteratori, tuple, closure, match ed elaborazione dei dati in stile comprehension (come catene di iteratori).
  • I grandi cambiamenti sono i tipi statici controllati prima dell’esecuzione, l’ownership al posto del garbage collector, Option al posto di None, e Result + ? al posto delle eccezioni.
  • Le classi diventano struct + impl, il duck typing diventa trait, e i blocchi with diventano il Drop automatico. Una HashMap non ha ordine, e gli interi hanno una dimensione fissa.
  • Rust non ha il GIL: i thread girano in parallelo, e il compilatore impedisce le data race.
  • Cargo sostituisce pip, venv e compagnia; cargo fmt, cargo clippy e cargo test sostituiscono black, ruff e pytest.
  • Puoi usare entrambi i linguaggi insieme: PyO3 e maturin trasformano codice Rust in un modulo Python.

Verifica veloce

1. Cosa usa Rust al posto del None di Python?

2. In Python, b = a dà due nomi alla stessa lista. Cosa fa let b = a; con un Vec in Rust?

3. Quanto vale 7 / 2 in Rust, se entrambi i numeri sono interi?

4. Vuoi velocizzare con Rust una funzione lenta di un progetto Python. Quali strumenti usi?

Punteggio: 0 / 4

  • The Rust Programming Language (in inglese): il libro ufficiale. I capitoli 4 (ownership), 6 (enum e Option), 9 (errori) e 13 (closure e iteratori) sono i più importanti per chi viene da Python.
  • PyO3 user guide (in inglese): tutto su come chiamare Rust da Python (e Python da Rust).
  • Maturin user guide (in inglese): compilare, testare e pubblicare pacchetti Python scritti in Rust.
  • Rust by Example (in inglese): brevi esempi eseguibili, ideali se impari meglio leggendo codice.
  • Are We Learning Yet? (in inglese): una panoramica dei crate di Rust per il machine learning e i dati.