Cómo llamar a Rust desde Python

, Python es lo suficientemente rápido, especialmente cuando te apoyas en NumPy, Polars u otras bibliotecas bien ajustadas escritas en lenguajes compilados como C. Pero de vez en cuando, terminas con un bucle activo que no se vectoriza: tal vez estés recorriendo una lista de cadenas para limpiarlas, o estés analizando texto desordenado donde cada carácter importa. Lo perfilas, confirmas al culpable y miras un bucle for que consume la mitad de tu tiempo de ejecución. Este es el momento en que Rust brilla.

Rust le brinda un rendimiento predecible, un control estricto sobre la memoria y una simultaneidad valiente, sin la molestia de la administración manual de la memoria. Si estás pensando: ¡no es otro idioma que aprender!, la buena noticia es que no necesitas abandonar Python para usar Rust. Puede conservar su orquestación, sus cuadernos, sus pruebas y mover solo los pequeños y aburridos bucles internos a Rust. Esto mantiene la curva de aprendizaje de Rust al mínimo absoluto.

En este artículo, demostraré cómo llamar a Rust desde Python y compararé las diferencias de rendimiento entre ejecutar Python puro y una combinación de Python/Rust. Este no será un tutorial sobre la programación de Rust, ya que supongo que al menos conoces los conceptos básicos.

¿Por qué molestarse?

Ahora, podrías pensar: si conozco Rust, ¿por qué me molestaría en integrarlo con Python? Simplemente programar en Rust, ¿verdad?

Bueno, primero, diría que conocer Rust no lo convierte automáticamente en el mejor lenguaje para toda su aplicación. Para muchos sistemas, por ejemplo, ML, AI, scripting y backends web, etc., Python ya es el lenguaje elegido.

En segundo lugar, la mayor parte del código no es crítico para el rendimiento. Para aquellas partes que lo son, a menudo solo se necesita un subconjunto muy pequeño de Rust para marcar una diferencia real, por lo que un poquito de conocimiento de Rust puede ser de gran ayuda.

Por último, el ecosistema de Python es difícil de reemplazar. Incluso si conoces bien Rust, Python te brinda acceso inmediato a herramientas como:

pandas NumPy scikit-learn Jupyter Airflow FastAPI herramientas una gran cantidad de bibliotecas de secuencias de comandos y automatización

Rust puede ser más rápido, pero Python a menudo gana en alcance del ecosistema y conveniencia de desarrollo.

Con suerte, he hecho lo suficiente para convencerte de que le des una oportunidad a la integración de Rust con Python. Dicho esto, comencemos.

Óxido y Maturín

Para nuestros casos de uso, necesitamos dos cosas: Rust y una herramienta llamada maturin.

La mayoría de ustedes conocerán Rust. Un lenguaje compilado rápido que ha pasado a primer plano en los últimos años. Aunque es posible que no hayas oído hablar de maturin.

Maturin es básicamente una herramienta de compilación y empaquetado para extensiones de Python escritas en Rust (usando PyO3 o Rust-cpython). Nos ayuda a hacer lo siguiente:

Construye su código Rust en un módulo de Python

Toma su caja Rust y la compila en una biblioteca compartida (.pyd en Windows, .so en Linux, .dylib en macOS) que Python puede importar. Establece automáticamente los indicadores correctos del compilador para lanzamiento/depuración y para la versión de Python a la que se dirige. Funciona con la función del módulo de extensión de PyO3, por lo que Python puede importar la biblioteca compilada como un módulo normal.

Ruedas de paquetes para distribución.

Las ruedas son los archivos .whl que carga en PyPI (binarios precompilados). Maturin admite la creación de ruedas para muchos Linux, macOS y Windows que funcionan en versiones y plataformas de Python. Realiza una compilación cruzada cuando es necesario o se ejecuta dentro de una imagen de Docker para satisfacer las reglas "manylinux" de PyPI.

Publica en PyPI

Con un comando, Maturin puede crear su extensión Rust y cargarla. Maneja credenciales, metadatos y etiquetas de plataforma automáticamente.

Integra Rust en el empaquetado de Python

Maturin genera un pyproject.toml que define su proyecto para que las herramientas de Python como pip sepan cómo construirlo.
Admite PEP 517, por lo que la instalación de pip funciona incluso si el usuario no tiene instalado maturin. Funciona a la perfección con las herramientas de configuración cuando combinas código Python y Rust en un solo paquete.

Bien, ya basta de teoría, comencemos a escribir, ejecutar y cronometrar algunos ejemplos de código.

Configurar un entorno de desarrollo

Como de costumbre, configuraremos un entorno de desarrollo independiente para realizar nuestro trabajo. De esa manera, nuestro trabajo no interferirá con ningún otro proyecto que podamos tener en marcha. Utilizo la herramienta UV para esto y estoy usando WSL2 Ubuntu para Windows como mi sistema operativo.

$ uv init pyrust $ cd pyrust $ uv venv pyrust $ fuente pyrust/bin/activate (pyrust) $

Instalación de óxido

Ahora podemos instalar Rust con este sencillo comando.

(pirusto) $ curl –proto '=https' –tlsv1.2 -sSf https://sh.rustup.rs | sh

Finalmente, se mostrarán 3 opciones en su pantalla como esta.

¡Bienvenido a Rust! Esto descargará e instalará el compilador oficial del lenguaje de programación Rust y su administrador de paquetes, Cargo. … … … 1) Continúe con la instalación estándar (predeterminada: simplemente presione Intro) 2) Personalice la instalación 3) Cancele la instalación

Presione 1, luego presione Entrar cuando se le soliciten opciones de instalación si desea utilizar las opciones predeterminadas. Para asegurarse de que Rust esté instalado correctamente, ejecute el siguiente comando.

(pirusto) $ Rustc –versión Rustc 1.89.0 (29483883e 2025-08-04)

Ejemplo 1: un equivalente de Hola mundo

Comencemos con un ejemplo simple de cómo llamar a Rust desde Python. Cree una nueva subcarpeta y agregue estos tres archivos.

carga.toml

[paquete] nombre = "hello_rust" versión = "0.1.0" edición = "2021" [lib] tipo-caja = ["cdylib"] [dependencias] pyo3 = {versión = "0.25", características = ["módulo de extensión"] }

pyproject.toml

[build-system] requiere = ["maturin>=1.5,<2"] build-backend = "maturin" [proyecto] nombre = "hello_rust" versión = "0.1.0" requiere-python = ">=3.9"

Por último, nuestro archivo fuente de Rust va a la subcarpeta src/lib.rs.

utilizar pyo3::preludio::*; /// Una función simple que expondremos a Python #[pyfunction] fn greet(name: &str) -> PyResult { Ok(format!("Hello, {} from Rust!", name)) } /// La definición del módulo #[pymodule] fn hello_rust(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(saludo, m)?)?; De acuerdo(()) }

Ahora ejecútelo con…

(pyrust) $ python -c "import hello_rust as hr; print(hr.greet('world'))" # Salida ¡Hola, mundo de Rust!

Colocamos nuestro código de Rust en src/lib.rs para seguir la convención de que el código de la biblioteca de Rust va allí, en lugar de en src/main.rs, que está reservado para el código ejecutable de Rust independiente.

Maturin + PyO3 busca dentro de src/lib.rs la función #[pymodule], que registra sus funciones de Rust para que Python las llame.

Ejemplo 2: bucles de Python frente a bucles de Rust

Considere algo deliberadamente mundano pero representativo: tiene una lista de oraciones y necesita normalizarlas. Por normalizar me refiero a convertirlos en una forma estándar y consistente antes de su posterior procesamiento.

Supongamos que queremos poner todo en minúsculas, eliminar la puntuación y dividirlo en tokens. Esto es difícil de vectorizar de manera eficiente porque la lógica se bifurca en cada carácter.

En Python puro, podrías escribir esto:-

# ———————— # Línea base de Python # ———————— def process_one_py(text: str) -> list[str]: palabra =[]fuera =[]para c en texto: si c.isalnum(): word.append(c.lower()) más: si palabra: out.append("".join(word)) palabra =[]if word: out.append("".join(word)) return out # Ejecute lo anterior para muchas entradas def batch_process_py(texts: list[str]) -> list[list[str]]: return [process_one_py

Así, por ejemplo,

(pyrust) $ batch_process_py["¡Hola mundo! 123", "Esto es una prueba"]

volvería,

[['hola', 'mundo', '123'], ['esto', 'es', 'una', 'prueba']]

Así es como se vería el equivalente de Rust:

/// src/lib.rs usa pyo3::preludio::*; utilizar pyo3::wrap_pyfunction; /// Procesar una cadena: minúsculas + quitar puntuación + dividir en espacios en blanco fn Process_one(text: &str) -> Vec { let mut out = Vec::new(); let mut palabra = String::new(); para c en text.chars() { if c.is_alphanumeric() { word.push(c.to_ascii_lowercase()); } else if c.is_whitespace() { if !word.is_empty() { out.push(std::mem::take(&mut palabra)); } } // ignorar la puntuación por completo } if !word.is_empty() { out.push(word); } out } #[pyfunction] fn batch_process(texts: Vec) -> PyResult>> { Ok(texts.iter().map(|t| Process_one #[pymodule] fn rust_text(_py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(batch_process, m)?)?; Ok(()) }

Bien, ejecutemos estos dos programas con una entrada grande (500.000 textos) y veamos cuáles son las diferencias en tiempo de ejecución. Para eso, escribí un script Python de referencia de la siguiente manera.

desde el momento importar perf_counter de estadísticas importar mediana importar cadena de importación aleatoria importar Rust_text # la extensión compilada # ———————— # Línea base de Python # ———————— def proceso_one_py(text: str) -> lista[str]: palabra =[]fuera =[]para c en texto: if c.isalnum(): word.append(c.lower()) elif c.isspace(): if word: out.append("".join(word)) word =[]# ignorar la puntuación si palabra: out.append("".join(word)) return out def batch_process_py(texts: list[str]) -> list[list[str]]: return [process_one_py # ———————— # Datos sintéticos # ———————— def make_texts(n=500_000, vocab=10_000, mean_len=40): palabras = ["".join(random.choices(string.ascii_lowercase, k=5)) for _ in range(vocab)] textos =[]for _ in range(n): L = max(3, int(random.expovariate(1/mean_len))) texts.append(" ".join(random.choice(words) for _ in range(L))) return texts texts = make_texts() # ———————— # Ayudante de sincronización # ———————— def timeit(fn, *args, repetir=5): ejecuta =[]para _ en rango (repetición): t0 = perf_counter() fn(*args) t1 = perf_counter() run.append(t1 – t0) return median(runs) # ———————— # Ejecutar puntos de referencia # ———————— py_time = timeit(batch_process_py, textos) rust_time = timeit(rust_text.batch_process, textos) n = len(textos) print("n— Punto de referencia —") print(f"Mediana de Python: {py_time:.3f} s | rendimiento: {n/py_time:,.0f} textos/s") print(f"Mediana de Rust 1 subproceso: {rust_time:.3f} s | rendimiento: {n/rust_time:,.0f} textos/s")

Como antes, necesitamos compilar nuestro código Rust para que Python pueda importarlo. En el ejemplo anterior, maturin se usó indirectamente como backend de compilación a través de pyproject.toml. Aquí lo llamamos directamente desde la línea de comando:

(pirusto) $ maturina desarrollar –liberar

Y ahora podemos simplemente ejecutar nuestro código de referencia de esta manera.

(pyrust) $ python benchmark.py — Punto de referencia — Mediana de Python: 5,159 s | rendimiento: 96,919 textos/s Rust 1-hilo mediana: 3,024 s | rendimiento: 165.343 textos/s

Esa fue una aceleración razonable sin demasiado esfuerzo. Hay algo más que podemos usar para lograr reducciones aún mayores en el tiempo de ejecución.

Rust tiene acceso a una biblioteca de paralelización llamada Rayon, lo que facilita la distribución de código en múltiples núcleos de CPU. En pocas palabras, Rayón…

Reemplacemos los iteradores secuenciales (iter()) con iteradores paralelos (par_iter()). Divide automáticamente sus datos en fragmentos, distribuye el trabajo entre subprocesos de la CPU y luego fusiona los resultados. Abstrae la complejidad de la gestión y sincronización de subprocesos.

Ejemplo 3: Agregar paralelismo a nuestro código Rust existente

Esto es sencillo. Si observamos el código Rust del ejemplo anterior, solo necesitamos realizar los siguientes tres cambios menores (marcados con comentarios a continuación).

/// src/lib.rs usa pyo3::preludio::*; utilizar pyo3::wrap_pyfunction; /// Agregue esta línea – Cambio 1 use rayon::prelude::*; /// Procesar una cadena: minúsculas + quitar puntuación + dividir en espacios en blanco fn Process_one(text: &str) -> Vec { let mut out = Vec::new(); let mut palabra = String::new(); para c en text.chars() { if c.is_alphanumeric() { word.push(c.to_ascii_lowercase()); } else if c.is_whitespace() { if !word.is_empty() { out.push(std::mem::take(&mut palabra)); } } // ignorar la puntuación por completo } if !word.is_empty() { out.push(word); } out } #[pyfunction] fn Batch_process(texts: Vec) -> PyResult>> { Ok(texts.iter().map(|t| Process_one } /// Agregue esta función – cambie 2 #[pyfunction] fn Batch_process_parallel(texts: Vec) -> PyResult>> { Ok(texts.par_iter().map(|t| Process_one } #[pymodule] fn Rust_text(_py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(batch_process, m)?)? // Agregue esta línea – cambie 3 m.add_function(wrap_pyfunction!(batch_process_parallel, m)?)?;

En nuestro código Python de referencia, solo necesitamos agregar una llamada al código Rust paralelo e imprimir los nuevos resultados.

… … # ———————— # Ejecutar puntos de referencia modificados # ———————— py_time = timeit(batch_process_py, textos) Rust_time = timeit(rust_text.batch_process, textos) Rust_par_time = timeit(rust_text.batch_process_parallel, textos) n = len(texts) print("n— Punto de referencia —") print(f"Mediana de Python: {py_time:.3f} s | rendimiento: {n/py_time:,.0f} textos/s") print(f"Mediana de Rust 1 hilo: {rust_time:.3f} s | rendimiento: {n/rust_time:,.0f} textos/s") print(f"Mediana de Rust Rayon: {rust_par_time:.3f} s | rendimiento: {n/rust_par_time:,.0f} textos/s")

Estos son los resultados de la ejecución del punto de referencia modificado.

— Punto de referencia — Mediana de Python: 5,171 s | rendimiento: 96.694 textos/s Rust 1-hilo mediana: 3.091 s | rendimiento: 161.755 textos/s Rust Rayon mediana: 2,223 s | rendimiento: 224.914 textos/s

El código Rust paralelizado redujo aproximadamente un 27% el tiempo de Rust no paralelizado y fue más del doble de rápido que el código Python simple. No está nada mal.

Resumen

Python suele ser lo suficientemente rápido para la mayoría de las tareas. Pero si la creación de perfiles muestra un punto lento que no se puede vectorizar y realmente afecta su tiempo de ejecución, no tiene que renunciar a Python o reescribir todo su proyecto. En su lugar, puedes mover solo las partes críticas para el rendimiento a Rust y dejar el resto del código como está.

Con PyO3 y maturin, puedes compilar código Rust en un módulo Python que funcione sin problemas con tus bibliotecas existentes. Esto le permite conservar la mayor parte de su código, pruebas, empaquetado y flujos de trabajo de Python, mientras obtiene la velocidad, la seguridad de la memoria y los beneficios de simultaneidad de Rust donde más los necesita.

Los ejemplos simples y los puntos de referencia aquí muestran que reescribir solo una pequeña parte de su código en Rust puede hacer que Python sea mucho más rápido. Agregar Rayon para el paralelismo mejora aún más el rendimiento, con solo unos pocos cambios de código y sin herramientas complicadas. Esta es una forma práctica y sencilla de acelerar las cargas de trabajo de Python sin cambiar todo el proyecto a Rust.