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
Noneque 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
importdesde 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.
Conceptos del lenguaje: de Python a Rust
Sección titulada «Conceptos del lenguaje: de Python a Rust»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 |
Colecciones
Sección titulada «Colecciones»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 |
Herramientas: de pip a cargo
Sección titulada «Herramientas: de pip a cargo»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.
Web y redes
Sección titulada «Web y redes»| 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 |
Datos y bases de datos
Sección titulada «Datos y bases de datos»| 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 |
Ciencia de datos y machine learning
Sección titulada «Ciencia de datos y machine learning»| 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 |
Piezas del día a día
Sección titulada «Piezas del día a día»| 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 150Hay 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íneaEn 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: QuackDice: Beep boopEn 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.
Lado a lado: None se convierte en Option
Sección titulada «Lado a lado: None se convierte en Option»En Python, cualquier valor puede ser None en secreto, y lo descubres al usarlo:
def find_email(user_id): 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 { _ => 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 registradodesconocidoSome("[email protected]")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 8080error: "http" no es un número (invalid digit found in string)error: el puerto 70000 está fuera de rangoEl 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: 3cat: 1end: 1La 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 primos25000..50000: 2371 primos50000..75000: 2260 primos75000..100000: 2199 primostotal: 9592thread::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):
pip install maturinmaturin new --bindings pyo3 fastmathcd fastmathmaturin 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])) # 30print(fastmath.word_count("the quick brown fox")) # 4fastmath.sum_of_squares(["a"]) # TypeError: 'str' object cannot be interpreted as an integerPyO3 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.
Asignar mueve el valor
Sección titulada «Asignar mueve el valor»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` 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(); | ++++++++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.
El resto de la lista
Sección titulada «El resto de la lista»- Los enteros tienen un tamaño. El
intde Python nunca se desborda. Uni32de Rust llega hasta 2.147.483.647; si te pasas, el programa entra en pánico (panic) en una compilación debug. Eligei64ou64para números grandes, y consulta Desbordamiento de enteros. - La división y el resto funcionan de otra manera.
7 / 2es3en Rust, porque los dos lados son enteros (el//de Python). Escribe7.0 / 2.0para obtener3.5. Además,-7 % 3es-1en Rust pero2en 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, porque3es un entero. Escribe3.0, o convierte conas 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. Usaword.chars().next()oword.chars().nth(i). Consulta Strings en Rust. Stringfrente a&str. Python tiene un solo tipostr. Rust tiene dos:Stringes dueño de su texto y&strlo toma prestado. Las funciones suelen recibir&stry los structs suelen guardarString.- Las variables son inmutables por defecto. Escribe
let mutcuando 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
ify cadamatchtienen que estar completos y tener el tipo correcto. No hay “valor de verdad”:if countno compila, escribesif count > 0. Unmatchtiene que cubrir todos los casos. - Un HashMap no tiene orden. El
dictde Python recuerda el orden de inserción; elHashMapde Rust no. Ordena antes de imprimir, usa unBTreeMapo añade el crateindexmap. - 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 checkmientras editas (se salta la generación de código y es mucho más rápido), ycargo run --releasecuando 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:
- Empezar con Rust y Herramientas de Rust: instala Rust y ejecuta
cargo new. - Variables, Tipos de datos y Desbordamiento de enteros: los tipos son el primer hábito nuevo.
- Funciones, If…Else y Bucles: lecturas rápidas; fíjate en las expresiones y en los puntos y coma que faltan.
- Ownership, luego Borrowing y luego Slices: las páginas más importantes para ti. Tómate tu tiempo.
- Strings:
Stringfrente a&str, y por qué no puedes indexar un string. - Structs y Enums, luego Match y Coincidencia de patrones.
- Option y Result, luego Errores personalizados.
- Vectores, HashMap y HashSet.
- Closures e Iteradores: tus comprensiones y generadores.
- Traits, Genéricos, Traits comunes y Trait objects.
- Módulos, Cargo y crates y Pruebas.
- Lifetimes y Punteros inteligentes.
- Concurrencia, Estado compartido y Async/Await.
- JSON con Serde, y después construye una CLI con clap o una API web.
Práctica
Sección titulada «Práctica»Los dos ejercicios te dan un fragmento de Python. Pásalo a Rust.
1. Pasa a Rust una comprensión y un dict
Sección titulada «1. Pasa a Rust una comprensión y un dict»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.0Recorrer 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.
2. Pasa try/except a Result
Sección titulada «2. Pasa try/except a Result»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ñosomitida "Grace 45": falta ':'omitida "Linus: forty": edad no válida: invalid digit found in stringguardadas: 1Los 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ó.
Resumen
Sección titulada «Resumen»- Mucho de Python te sirve en Rust: los bucles sobre iteradores, las tuplas, los closures,
matchy 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,
Optionen lugar deNone, yResult+?en lugar de excepciones. - Las clases se convierten en un
struct+impl, el duck typing en traits y los bloqueswithen elDropautomático. UnHashMapno 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 clippyycargo testsustituyen 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 que aprendiste
Sección titulada «Comprueba lo que aprendiste»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
Para leer más
Sección titulada «Para leer más»- 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.