Ir al contenido

Rust para desarrolladores de Python: guía práctica

Si sabes Python, ya conoces muchas de las ideas de Rust. Rust tiene bucles for sobre iteradores, tuplas, closures (como las lambdas de Python, pero más útiles), una sentencia match que funciona como el match de Python y cadenas de iteradores que hacen lo mismo que las listas por comprensión. Rust incluso comparte algunos gustos con Python: una única forma obvia de formatear el código, una herramienta de paquetes amable y mensajes de error que se esfuerzan de verdad por ayudarte.

Lo nuevo tiene que ver sobre todo con el compilador. Python comprueba los tipos mientras tu programa se ejecuta; Rust los comprueba todos antes de ejecutarlo. Python libera la memoria con un recolector de basura (garbage collector); Rust usa ownership (propiedad) y borrowing (préstamo), que se comprueban al compilar. Python tiene None y excepciones; Rust tiene Option y Result. En esas tres cosas es donde vas a pasar la mayor parte de tu tiempo de aprendizaje.

Entonces, ¿por qué aprenden Rust los desarrolladores de Python? Estas son las razones sinceras:

  • Velocidad. Rust se compila a código máquina nativo. El trabajo que exige mucha CPU y que es lento en Python puro (analizar texto, cálculos numéricos, bucles grandes) suele ser mucho más rápido en Rust, sin trucos de optimización.
  • Hilos de verdad. En el CPython estándar, el GIL deja que un solo hilo ejecute código Python a la vez. Los hilos de Rust se ejecutan en todos los núcleos de tu CPU a la vez, y el compilador detiene las condiciones de carrera (data races) antes de que tu programa se ejecute.
  • Los errores aparecen antes. Una errata en el nombre de un atributo, un None que olvidaste o una función llamada con el tipo equivocado: en Python los descubres cuando se ejecuta esa línea. En Rust, el programa no compila hasta que los corriges.
  • No tienes que elegir. Con PyO3 y maturin puedes escribir en Rust la parte lenta de un proyecto de Python e importarla con import desde Python, como cualquier otro módulo. Herramientas de Python muy populares, como Polars, Ruff y uv, están escritas en Rust.

Si prefieres una comparación más general de Rust con Python, Go y C++, consulta Rust vs C++, Go y Python.

Usa esta tabla como un diccionario de bolsillo. La última columna enlaza con la lección de este sitio que explica la parte de Rust.

Python Rust Para saber más
Tipos dinámicos, comprobados al ejecutar Tipos estáticos, comprobados al compilar. Rust deduce la mayoría, así que escribes menos tipos de lo que crees Tipos de datos
Anotaciones de tipo (def f(x: int) -> str) Los tipos en la firma de una función son obligatorios, y el compilador los hace cumplir Funciones
x = 5 let x = 5; (no puede cambiar). let mut x = 5; si quieres cambiarlo Variables
int (crece todo lo que haga falta) Enteros de tamaño fijo: i32, i64, u64, usize, i128… Pueden desbordarse Tipos de datos, Desbordamiento de enteros
float f64 (o f32) Tipos de datos
str String (con dueño, puede crecer) y &str (una vista prestada de un texto) Strings
f-strings: f"Hi {name}" format!("Hi {name}") y println!("Hi {name}") Salida en Rust
print(repr(x)) / __repr__ println!("{x:?}") con #[derive(Debug)] Salida en Rust, Traits comunes
__str__ Implementar Display (y entonces .to_string() también funciona) Traits comunes
__eq__, __hash__, __lt__ Derivar PartialEq, Eq, Hash, PartialOrd, Ord Traits comunes
None, Optional[T] Option<T>: o bien Some(value) o bien None Option
x or default x.unwrap_or(default) Option
raise, try/except Result<T, E> + el operador ? Result, Errores personalizados
Una excepción para un bug de verdad (assert, IndexError) panic! (detiene el hilo) Panic
class con __init__ y métodos struct para los datos + un bloque impl para los métodos; new es una función normal Structs
@dataclass struct con #[derive(Debug, Clone, PartialEq)] Structs
self &self (leer), &mut self (modificar) o self (tomar el ownership) Structs
Herencia No hay herencia. Usa composición (un struct dentro de otro) + traits con métodos por defecto Traits, Patrones de diseño
Duck typing, Protocol, ABC trait Traits
Una lista de objetos de clases distintas Vec<Box<dyn Trait>> (un trait object) Trait objects
TypeVar / funciones genéricas Genéricos con trait bounds (restricciones): fn largest<T: Ord>(items: &[T]) Genéricos
Clase Enum enum, donde cada variante puede llevar sus propios datos Enums
match / case (3.10+) match (el compilador comprueba que cubriste todos los casos) Match, Coincidencia de patrones
if my_list: (valor de verdad) Sé explícito: if !my_list.is_empty(). En un if solo puede ir un bool If…Else
for i in range(10) for i in 0..10 Bucles for
enumerate(xs), zip(a, b) xs.iter().enumerate(), a.iter().zip(b.iter()) Iteradores
lambda x: x + 1 Closures: |x| x + 1 (y pueden ocupar muchas líneas) Closures
Listas por comprensión, map, filter Adaptadores de iteradores: filter, map y luego collect() Iteradores
Generadores y yield Tu propio tipo que implemente Iterator, o std::iter::from_fn Iteradores personalizados
with open(...) as f: No hay que escribir nada: los archivos y los locks se cierran cuando su dueño sale del ámbito (Drop) Ámbito, Archivos
Argumentos por defecto, *args, **kwargs No existen. Usa parámetros Option, slices (&[T]), un struct con Default o un builder Funciones, Patrones de diseño
Decoradores No hay un equivalente directo. Envuelve con closures, o usa macros de atributo (como #[tokio::main]) Closures, Macros procedurales
import y módulos mod y use Módulos
Paquete en PyPI Crate en crates.io Cargo y crates
if __name__ == "__main__": fn main() Empezar con Rust
threading (con el GIL) std::thread: hilos que de verdad corren en paralelo Concurrencia
multiprocessing, concurrent.futures Hilos, o el crate rayon para iteradores en paralelo Concurrencia, Estado compartido
asyncio, async def, await async fn y .await, con un runtime como tokio Async/Await
7 // 2, 2 ** 10 7 / 2 (división entera si son enteros), 2_i64.pow(10) Operadores
int("42"), str(42) "42".parse::<i32>() (devuelve un Result), 42.to_string() Conversión de tipos, Conversiones

Todas las colecciones incorporadas de Python tienen su lugar en la biblioteca estándar de Rust. Las mayores diferencias: una colección de Rust guarda un solo tipo (un Vec<i32> solo guarda i32), y un HashMap no recuerda el orden de inserción.

Python Rust Para saber más
list Vec<T> Vectores
tuple Tuplas (i32, String), o un struct pequeño con campos con nombre Tuplas
dict HashMap<K, V> (¡sin un orden fijo!) HashMap
dict cuando necesitas las claves ordenadas BTreeMap<K, V> HashMap
dict cuando necesitas el orden de inserción El crate indexmap
set, frozenset HashSet<T>, BTreeSet<T> HashSet
collections.Counter, defaultdict HashMap con la API entry: *map.entry(k).or_insert(0) += 1 HashMap
collections.deque VecDeque<T> VecDeque y BinaryHeap
heapq (primero el menor) BinaryHeap<T> (primero el mayor; envuelve los elementos en Reverse para que salga primero el menor) VecDeque y BinaryHeap
namedtuple struct Structs
bytes, bytearray &[u8], Vec<u8> Slices
Slicing: xs[1:3] Slices: &xs[1..3] (sin índices negativos; usa xs.len() - 1) Slices

Aquí es donde muchos desarrolladores de Python respiran tranquilos. Rust tiene una herramienta oficial, Cargo, que hace el trabajo de pip, venv, setuptools, pytest y más. No existe el debate de “¿qué herramienta de empaquetado usamos?”.

Python Rust Notas
Intérprete python Compilador rustc, que normalmente se usa a través de cargo Primero compilas y luego ejecutas un binario nativo. Consulta Empezar con Rust
pyenv rustup (y un archivo rust-toolchain.toml para fijar una versión) Consulta Herramientas de Rust
pip, poetry, uv cargo Compila, ejecuta, prueba, añade paquetes y publica. Consulta Cargo y crates
pip install requests cargo add reqwest Añade el crate a Cargo.toml, solo para este proyecto
pyproject.toml / requirements.txt Cargo.toml
poetry.lock / uv.lock Cargo.lock Se crea solo
venv / virtualenv No hace falta Cada proyecto tiene sus propias dependencias y compila en su propia carpeta target/
PyPI crates.io La documentación de la API de cada crate publicado está en docs.rs
python main.py cargo run (cargo run --release para la máxima velocidad) Las compilaciones debug son mucho más lentas al ejecutarse; mide la velocidad en modo release
pipx install ruff cargo install ripgrep Instala una herramienta de línea de comandos
PyInstaller cargo build --release Obtienes un único binario nativo; los usuarios no necesitan tener Rust instalado
black cargo fmt Un único estilo estándar para todos
ruff, pylint, flake8 cargo clippy Cientos de lints útiles, oficiales
mypy, pyright El propio compilador Comprobar los tipos no es opcional ni un paso aparte
pytest, unittest #[test] y cargo test, incluidos de serie (cargo nextest es un ejecutor más rápido muy popular) Consulta Pruebas en Rust
Fixtures de pytest y parametrize rstest
hypothesis proptest Pruebas basadas en propiedades. Consulta Pruebas avanzadas
unittest.mock mockall, o implementaciones falsas escritas a mano que implementan tu trait
timeit, pytest-benchmark criterion Consulta Rendimiento en Rust
cProfile, py-spy cargo flamegraph, perf o cualquier profiler nativo
Docstrings + Sphinx Comentarios de documentación /// en Markdown + cargo doc Los ejemplos de código de la documentación se ejecutan como tests. Consulta Documentación en Rust
El REPL de Python, Jupyter El Rust Playground; evcxr_repl y evcxr_jupyter Rust no trae un REPL incorporado. Lo normal para experimentar son pequeños proyectos con cargo run
VS Code + Pylance, PyCharm VS Code + rust-analyzer, o JetBrains RustRover rust-analyzer ofrece autocompletado, ir a la definición y errores en línea

Aquí tienes un Cargo.toml pequeño, para que lo compares 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"] }

Bibliotecas y frameworks: “¿Qué usamos en lugar de…?”

Sección titulada «Bibliotecas y frameworks: “¿Qué usamos en lugar de…?”»

“Uso requests, FastAPI y pandas. ¿Cuál es la versión de Rust?” Aquí tienes los equivalentes más cercanos. El ecosistema de Python es mucho más antiguo y más grande, así que en algunas áreas (sobre todo la ciencia de datos) Rust tiene menos opciones, y más jóvenes.

Python Rust Notas
Flask, FastAPI axum (construimos una API en Proyecto: API web con Axum), actix-web Los handlers son simples async fn; los tipos de petición y respuesta salen de tus structs, un poco como FastAPI con pydantic
Django No hay un equivalente completo. Loco (loco-rs) es un framework “con pilas incluidas”; la mayoría de los equipos combinan axum + un ORM + un motor de plantillas No trae un panel de administración
requests, httpx reqwest (asíncrono, con un cliente bloqueante opcional), ureq (sencillo y bloqueante) Consulta Redes en Rust
aiohttp reqwest + tokio
Jinja2 minijinja (del propio autor de Jinja), tera, askama (plantillas comprobadas al compilar)
BeautifulSoup scraper
Python Rust Notas
ORM de SQLAlchemy Diesel (consultas comprobadas por tipos al compilar), SeaORM (asíncrono, estilo entidades) Bases de datos en Rust muestra los dos con código que funciona
psycopg, SQL a mano sqlx: SQL normal, comprobado contra tu base de datos real al compilar Asíncrono, muy popular, no es un ORM
sqlite3 rusqlite
redis-py redis
Celery apalis para colas de trabajos; tokio::spawn para tareas en segundo plano dentro del proceso
Python Rust Notas
pandas Polars Escrito en Rust; su API de Python, que quizá ya conoces, se apoya en la misma biblioteca de Rust
NumPy ndarray Arrays de N dimensiones con slicing y broadcasting; muchos menos extras que NumPy
scikit-learn linfa, smartcore Menos algoritmos y comunidades mucho más pequeñas
PyTorch candle (crate candle-core), burn Buenos para ejecutar modelos; la mayor parte del entrenamiento sigue haciéndose en Python
matplotlib plotters
Módulo csv Crate csv + serde
Python Rust Notas
json serde + serde_json El estándar; también sirve para TOML, YAML y más. Consulta JSON con Serde
pydantic serde para leer los datos + validator o garde para las reglas de validación Tu struct es el esquema
argparse, click, typer clap Defines tus argumentos como un struct. Consulta Proyecto: CLI con clap
logging tracing, o log + env_logger Consulta Logging en Rust
re regex Sin look-around ni referencias hacia atrás, pero con tiempo lineal garantizado; fancy-regex sí los tiene. Consulta Expresiones regulares
datetime chrono o jiff Consulta Fechas y horas
random rand Consulta Números aleatorios
asyncio tokio Consulta Async/Await en Rust
multiprocessing.Pool.map rayon: cambia .iter() por .par_iter()
itertools Casi todo viene incluido en Iterator; el crate itertools añade el resto
os, pathlib, subprocess std::env, std::path, std::fs, std::process Consulta Archivos en Rust y Entorno y procesos

Lado a lado: una clase se convierte en struct + impl

Sección titulada «Lado a lado: una clase se convierte en struct + impl»

Aquí tienes una pequeña clase de 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)

Y lo mismo en Rust:

use std::fmt;
struct Account {
owner: String,
balance: i64,
}
impl Account {
// `new` es solo una convención, no una palabra clave. No hay argumentos
// por defecto, así que quien llama siempre pasa los dos valores.
fn new(owner: &str, balance: i64) -> Self {
Account { owner: owner.to_string(), balance }
}
// `&mut self`: este método modifica la cuenta.
fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
}
// Este es el __str__ de Rust.
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} tiene {}", self.owner, self.balance)
}
}
fn main() {
let mut account = Account::new("Ada", 100);
account.deposit(50);
println!("{account}");
}

Salida:

Ada tiene 150
▶ Pruébalo tú mismo

Hay varias diferencias. Los campos se declaran desde el principio con sus tipos, así que no puedes añadir un atributo nuevo más tarde, como te deja hacer Python con account.nickname = "A". Los datos (el struct) y el comportamiento (el impl) viven en bloques separados. Cada método dice si solo lee (&self) o si modifica (&mut self) el valor, y como deposit modifica la cuenta, la variable tiene que declararse con let mut. Rust no tiene argumentos por defecto; si quieres una cuenta que empiece en cero, añades una segunda función, como fn empty(owner: &str) -> Self.

Lado a lado: el duck typing se convierte en traits

Sección titulada «Lado a lado: el duck typing se convierte en traits»

A Python no le importa de qué clase es un objeto, solo que tenga el método adecuado:

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, pero solo cuando se ejecuta esta línea

En Rust, “tiene un método speak” se escribe como un trait, y cada tipo se apunta con un bloque 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()
}
}
// Acepta cualquier tipo que implemente 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); // error de compilación: `{integer}` no implementa Speak
}

Salida:

Dice: Quack
Dice: Beep boop
▶ Pruébalo tú mismo

En el fondo sigue siendo duck typing, pero comprobado por el compilador: introduce(&42) es un error antes de que el programa llegue a ejecutarse, y no un AttributeError en producción. Si has usado typing.Protocol, los traits te van a resultar naturales. Una lista de tipos mezclados necesita Box<dyn Speak>, porque Rust tiene que saber cómo llamar al método en cada elemento (consulta Trait objects). Y puedes implementar tu propio trait para tipos que no escribiste tú, como String o i32.

En Python, cualquier valor puede ser None en secreto, y lo descubres al usarlo:

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

En Rust, un valor que puede faltar tiene el tipo Option<T>. No puedes usarlo como un valor normal hasta que hayas tratado el 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}: no hay email registrado"),
}
}
// Como `find_email(3) or "desconocido"` en Python.
let email = find_email(3).unwrap_or("desconocido".to_string());
println!("{email}");
// Transforma el valor de dentro, si lo hay.
let upper = find_email(2).map(|e| e.to_uppercase());
println!("{upper:?}");
}

Salida:

3: no hay email registrado
desconocido
▶ Pruébalo tú mismo

El compilador de Rust no te deja llamar a .to_uppercase() sobre un Option<String>, así que el error “'NoneType' object has no attribute” simplemente no puede ocurrir. Un String de Rust siempre es un texto de verdad. Aprende los métodos auxiliares en la página de Option, y la forma corta if let Some(email) = ... en If Let / While Let en Rust.

Lado a lado: las excepciones se convierten en Result y ?

Sección titulada «Lado a lado: las excepciones se convierten en Result y ?»

En Python, int() lanza una excepción, y la capturas en algún punto más arriba de la pila de llamadas:

def parse_port(text):
port = int(text) # puede lanzar 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}")

En Rust, una función que puede fallar lo dice en su tipo de retorno. El operador ? significa “si esto falló, devuelve el error a quien me llamó, ahora mismo”. Hace el mismo trabajo que dejar que una excepción suba por la pila, pero lo ves en el código:

fn parse_port(text: &str) -> Result<u32, String> {
let port: u32 = text
.parse()
.map_err(|e| format!("{text:?} no es un número ({e})"))?;
if !(1..=65535).contains(&port) {
return Err(format!("el puerto {port} está fuera de rango"));
}
Ok(port)
}
fn main() {
for text in ["8080", "http", "70000"] {
match parse_port(text) {
Ok(port) => println!("puerto {port}"),
Err(e) => println!("error: {e}"),
}
}
}

Salida:

puerto 8080
error: "http" no es un número (invalid digit found in string)
error: el puerto 70000 está fuera de rango
▶ Pruébalo tú mismo

El texto “invalid digit found in string” (“se encontró un dígito no válido en la cadena”) sale en inglés porque lo escribe la biblioteca estándar de Rust. La gran diferencia: en Python, cualquier función puede lanzar una excepción y nada en su firma te lo dice. En Rust, Result forma parte de la firma, así que quien llama no puede olvidarse de él. Usar String como tipo de error está bien mientras aprendes; los proyectos reales usan sus propios enums de error (Errores personalizados) o el crate anyhow, que se parece mucho a “lanzar cualquier excepción” (anyhow y thiserror).

Lado a lado: las comprensiones y los dict se convierten en iteradores y HashMap

Sección titulada «Lado a lado: las comprensiones y los dict se convierten en iteradores y HashMap»

Las listas por comprensión y contar con un dict son el pan de cada día en 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}")

En Rust, una comprensión se convierte en una cadena de iteradores que termina en collect(), y el dict se convierte en un HashMap. El texto se queda en inglés a propósito: el ejemplo filtra por la longitud en bytes (len()) y ordena alfabéticamente, y las letras con tilde cambiarían los resultados.

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;
}
// Primero las más frecuentes; los empates, en orden alfabético.
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}");
}
}

Salida:

["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]
the: 3
cat: 1
end: 1
▶ Pruébalo tú mismo

La cadena se lee de izquierda a derecha en lugar de desde dentro hacia fuera, pero la idea es la misma. Igual que una expresión generadora, es perezosa (lazy): no pasa nada hasta que collect(), sum(), count() o un bucle for sacan los valores. collect() necesita saber qué tiene que construir, y por eso loud tiene el tipo Vec<String>. Y fíjate en el paso de ordenar: un dict de Python recuerda el orden de inserción, pero un HashMap de Rust no, así que siempre ordenamos antes de imprimir. Tienes más sobre ordenación en la página Ordenar y buscar.

Lado a lado: el GIL frente a hilos de verdad

Sección titulada «Lado a lado: el GIL frente a hilos de verdad»

En el CPython estándar, el Global Interpreter Lock (GIL) deja que un solo hilo ejecute bytecode de Python a la vez. Los hilos ayudan cuando esperas a la red, pero un trabajo que exige mucha CPU, como este, no va más rápido con más hilos, así que los desarrolladores de Python recurren 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 no tiene GIL. Cada thread::spawn es un hilo real del sistema operativo, y los hilos se ejecutan en núcleos distintos de la CPU al mismo tiempo:

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)];
// Cada rango tiene su propio hilo del sistema. De verdad se ejecutan a la vez.
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} primos");
}
println!("total: {}", counts.iter().sum::<usize>());
}

Salida:

0..25000: 2762 primos
25000..50000: 2371 primos
50000..75000: 2260 primos
75000..100000: 2199 primos
total: 9592
▶ Pruébalo tú mismo

thread::scope espera a que terminen todos los hilos antes de devolver el resultado, así que los hilos pueden tomar prestados datos locales sin peligro. Sin un GIL, quizá te preocupe que dos hilos modifiquen el mismo valor a la vez. Rust se encarga de eso al compilar: los datos compartidos que pueden cambiar tienen que ir envueltos en algo como Arc<Mutex<T>>, o el programa no compila (consulta Estado compartido). En proyectos reales, el crate rayon lo acorta todavía más, un poco como multiprocessing.Pool.map pero con hilos:

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

Python también está cambiando en esto: Python 3.13 añadió una versión opcional “free-threaded”, sin el GIL. Pero el CPython estándar que instala y despliega la mayoría de la gente sigue teniendo el GIL, y muchas extensiones en C todavía no están preparadas para funcionar sin él.

Escribir extensiones de Python en Rust (PyO3 + maturin)

Sección titulada «Escribir extensiones de Python en Rust (PyO3 + maturin)»

No tienes que dejar Python para usar Rust. PyO3 te permite escribir un módulo de Python en Rust, y maturin lo empaqueta en un wheel normal que pip puede instalar. Así se hacen Polars, pydantic-core y muchas otras bibliotecas rápidas de Python.

Prepara un proyecto (dentro de un entorno virtual activado):

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

maturin escribe el Cargo.toml y el pyproject.toml por ti. Después, pon tus funciones de Rust en src/lib.rs:

use pyo3::prelude::*;
/// Un módulo de Python escrito en Rust.
#[pymodule]
mod fastmath {
use pyo3::prelude::*;
/// Suma los cuadrados de una lista de números.
#[pyfunction]
fn sum_of_squares(numbers: Vec<i64>) -> i64 {
numbers.iter().map(|n| n * n).sum()
}
/// Cuenta las palabras de un texto.
#[pyfunction]
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
}

Compílalo dentro de tu entorno virtual con maturin develop (añade --release cuando te importe la velocidad), y úsalo desde Python como cualquier otro módulo:

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 convierte los tipos por ti: una list de enteros de Python se convierte en un Vec<i64>, un str en un &str, y los comentarios de documentación se convierten en docstrings de Python. Los tipos incorrectos dan un TypeError normal de Python. Cuando quieras compartirlo, maturin build --release crea un wheel y maturin publish lo sube a PyPI. Una buena regla: mide primero y luego pasa a Rust solo el bucle que más tiempo consume. Pasar de Python a Rust tiene un pequeño coste, así que una sola llamada que hace mucho trabajo es mejor que un millón de llamadas diminutas.

Lo que te va a hacer tropezar si vienes de Python

Sección titulada «Lo que te va a hacer tropezar si vienes de Python»

Todo el que llega desde Python se encuentra con los mismos baches. Conocerlos de antemano ayuda.

En Python, b = a te da dos nombres para la misma lista. En Rust, let b = a; mueve el ownership de un Vec o un String a b, y a ya no se puede usar:

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();
| ++++++++

El compilador te dice que a se movió en la línea 3 (“value moved here”) y que después intentas usarlo en la línea 5, y hasta te sugiere clonarlo. La solución es tomarlo prestado (let b = &a;, si solo necesitas leerlo) o hacer una copia real a propósito (a.clone()). Esta regla, el ownership, es la idea nueva más importante para ti. Lee esa página y la de Borrowing con calma.

  • Los enteros tienen un tamaño. El int de Python nunca se desborda. Un i32 de Rust llega hasta 2.147.483.647; si te pasas, el programa entra en pánico (panic) en una compilación debug. Elige i64 o u64 para números grandes, y consulta Desbordamiento de enteros.
  • La división y el resto funcionan de otra manera. 7 / 2 es 3 en Rust, porque los dos lados son enteros (el // de Python). Escribe 7.0 / 2.0 para obtener 3.5. Además, -7 % 3 es -1 en Rust pero 2 en Python; usa (-7_i32).rem_euclid(3) para obtener el resultado de Python.
  • No se mezclan tipos numéricos. let x: f64 = 2.5 * 3; no compila, porque 3 es un entero. Escribe 3.0, o convierte con as f64 (consulta Conversión de tipos).
  • No puedes indexar un string. word[0] es un error de compilación, porque los strings de Rust son UTF-8 y un “carácter” puede ocupar varios bytes. Usa word.chars().next() o word.chars().nth(i). Consulta Strings en Rust.
  • String frente a &str. Python tiene un solo tipo str. Rust tiene dos: String es dueño de su texto y &str lo toma prestado. Las funciones suelen recibir &str y los structs suelen guardar String.
  • Las variables son inmutables por defecto. Escribe let mut cuando un valor tenga que cambiar. El compilador te avisa si se te olvidó.
  • Llaves y puntos y coma. A Rust no le importa la indentación; le importan las { }. Y la última línea de una función sin punto y coma es su valor de retorno: fn double(x: i32) -> i32 { x * 2 }.
  • Cada if y cada match tienen que estar completos y tener el tipo correcto. No hay “valor de verdad”: if count no compila, escribes if count > 0. Un match tiene que cubrir todos los casos.
  • Un HashMap no tiene orden. El dict de Python recuerda el orden de inserción; el HashMap de Rust no. Ordena antes de imprimir, usa un BTreeMap o añade el crate indexmap.
  • Hay un paso de compilación. No hay REPL, y una compilación desde cero de un proyecto con muchos crates tarda un rato. Usa cargo check mientras editas (se salta la generación de código y es mucho más rápido), y cargo run --release cuando midas la velocidad.
  • El borrow checker te va a llevar la contraria. Patrones normales en Python, como muchos objetos que guardan y modifican un mismo objeto compartido, u objetos que apuntan a su padre, necesitan punteros inteligentes o un diseño distinto. Cuenta con unas semanas frustrantes. Luego se vuelve mucho más fácil.

Una ruta de aprendizaje para desarrolladores de Python

Sección titulada «Una ruta de aprendizaje para desarrolladores de Python»

No necesitas leer este sitio en orden. Si vienes de Python, esta ruta dedica el tiempo a lo que más diferencia a Rust de lo que ya conoces:

  1. Empezar con Rust y Herramientas de Rust: instala Rust y ejecuta cargo new.
  2. Variables, Tipos de datos y Desbordamiento de enteros: los tipos son el primer hábito nuevo.
  3. Funciones, If…Else y Bucles: lecturas rápidas; fíjate en las expresiones y en los puntos y coma que faltan.
  4. Ownership, luego Borrowing y luego Slices: las páginas más importantes para ti. Tómate tu tiempo.
  5. Strings: String frente a &str, y por qué no puedes indexar un string.
  6. Structs y Enums, luego Match y Coincidencia de patrones.
  7. Option y Result, luego Errores personalizados.
  8. Vectores, HashMap y HashSet.
  9. Closures e Iteradores: tus comprensiones y generadores.
  10. Traits, Genéricos, Traits comunes y Trait objects.
  11. Módulos, Cargo y crates y Pruebas.
  12. Lifetimes y Punteros inteligentes.
  13. Concurrencia, Estado compartido y Async/Await.
  14. JSON con Serde, y después construye una CLI con clap o una API web.

Los dos ejercicios te dan un fragmento de Python. Pásalo a Rust.

Pasa a Rust este código de Python. Pista: para imprimir los nombres en orden, recógelos en un Vec<&str>, llama a .sort() y luego a .join(", "). Para la media, convierte con as f64 antes de dividir.

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}")
Ver solución
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!("aprobados: {}", 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}");
}

Salida:

aprobados: Ada, Cy
[1, 9, 25, 49, 81]
media: 63.0
▶ Pruébalo tú mismo

Recorrer un HashMap te da pares de referencias, y por eso ves **score y *name: siguen las referencias hasta llegar a los valores. El sort() no es opcional aquí: sin él, los nombres podrían salir en cualquier orden. Y total as f64 hace falta porque Rust nunca convierte un entero en un número decimal a tus espaldas.

Pasa a Rust este código de Python. Cada línea debe tener la forma nombre: edad. Pista: line.split_once(':') devuelve un Option<(&str, &str)>; conviértelo en un Result con .ok_or(...) y luego usa ?. Haz que tu función devuelva 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")
Ver solución
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> {
let (name, age) = line.split_once(':').ok_or("falta ':'")?;
let age: u32 = age.trim().parse().map_err(|e| format!("edad no válida: {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} tiene {age} años");
ages.insert(name, age);
}
Err(e) => println!("omitida {line:?}: {e}"),
}
}
println!("guardadas: {}", ages.len());
}

Salida:

ok: Ada tiene 36 años
omitida "Grace 45": falta ':'
omitida "Linus: forty": edad no válida: invalid digit found in string
guardadas: 1
▶ Pruébalo tú mismo

Los dos raise se convierten en valores Err, y el try/except se convierte en un match. El ? después de ok_or("falta ':'") funciona aunque el error sea un &str y la función devuelva errores de tipo String, porque ? convierte el error con From. Fíjate en el orden dentro de Ok: primero imprimimos y luego insert mueve name dentro del mapa. Si cambias esas dos líneas de sitio, el compilador te dirá que name se movió.

  • Mucho de Python te sirve en Rust: los bucles sobre iteradores, las tuplas, los closures, match y el procesamiento de datos al estilo de las comprensiones (como cadenas de iteradores).
  • Los grandes cambios son los tipos estáticos comprobados antes de ejecutar, el ownership en lugar de un recolector de basura, Option en lugar de None, y Result + ? en lugar de excepciones.
  • Las clases se convierten en un struct + impl, el duck typing en traits y los bloques with en el Drop automático. Un HashMap no tiene orden, y los enteros tienen un tamaño fijo.
  • Rust no tiene GIL: los hilos se ejecutan en paralelo, y el compilador evita las condiciones de carrera.
  • Cargo sustituye a pip, a venv y compañía; cargo fmt, cargo clippy y cargo test sustituyen a black, ruff y pytest.
  • Puedes combinar los dos lenguajes: PyO3 y maturin convierten código de Rust en un módulo de Python.

Comprueba lo aprendido

1. ¿Qué usa Rust en lugar del None de Python?

2. En Python, b = a da dos nombres a la misma lista. ¿Qué hace let b = a; con un Vec en Rust?

3. ¿Cuánto vale 7 / 2 en Rust, si los dos números son enteros?

4. Quieres acelerar con Rust una función lenta de un proyecto de Python. ¿Qué herramientas usas?

Puntuación: 0 / 4

  • El lenguaje de programación Rust: el libro oficial, en su traducción de la comunidad. Los capítulos 4 (ownership), 6 (enums y Option), 9 (errores) y 13 (closures e iteradores) son los más importantes si vienes de Python.
  • PyO3 user guide (en inglés): todo sobre llamar a Rust desde Python (y a Python desde Rust).
  • Maturin user guide (en inglés): cómo compilar, probar y publicar paquetes de Python hechos con Rust.
  • Rust by Example (en inglés): ejemplos cortos que se pueden ejecutar, ideales si aprendes mejor leyendo código.
  • Are We Learning Yet? (en inglés): un resumen de los crates de Rust para machine learning y datos.