PyO3: cómo meter Rust dentro de Python (caso Pydantic)

Qué hay realmente debajo de Pydantic v2

Cada vez que validas un payload con Pydantic v2, la librería de validación más usada en Python, hay un binario en Rust ejecutando el trabajo. Su núcleo, pydantic-core, está construido con PyO3, el mismo toolchain que cualquier desarrollador puede usar para acelerar Python sin reescribir todo el stack.

Según la documentación oficial de Pydantic, la librería registra más de 550 millones de descargas mensuales y unos 8.000 paquetes en PyPI dependen de ella. Entre los proyectos que la usan aparecen FastAPI, Hugging Face, Django Ninja, SQLModel y LangChain, además de las FAANG y 20 de las 25 mayores empresas del NASDAQ, según el mismo sitio. Toda esa tracción pasa, en cada request, por una capa Rust que el desarrollador Python no ve.

Eso es lo que el artículo de Bob Belderbos desarma paso a paso: cómo se construye ese puente, qué hace internamente y por qué a veces la frontera entre ambos lenguajes cuesta más que el algoritmo en sí.

👥 ¿Quieres ir más allá de la noticia?

En nuestra comunidad discutimos las tendencias, compartimos oportunidades y nos ayudamos entre emprendedores. Sin humo, solo acción.

👥 Unirme a la comunidad

Cómo PyO3 convierte Rust en un módulo importable

Pasar de un crate en Rust a un import en Python toma cuatro pasos:

  • Escribir un módulo Rust normal.
  • Anotarlo con las macros #[pyfunction] y #[pymodule].
  • Compilarlo con maturin, que lo empaqueta como librería compartida (.so, .dylib o .pyd).
  • Instalarlo en el virtualenv.

Las dos macros son el pegamento. #[pyfunction] reescribe la función Rust para que Python pueda llamarla y se encarga de las conversiones de tipo y del conteo de referencias en la frontera. #[pymodule] registra esas funciones en un módulo con el mismo nombre del crate, de modo que import rustexample funcione igual que con cualquier paquete de PyPI.

Una pieza que suele olvidarse: durante la compilación, maturin detecta el binding de PyO3 y construye un wheel específico para la versión de CPython del entorno activo. El propio log de maturin dev -r lo muestra — desde "Found pyo3 bindings" hasta "Installed rustexample" — y el paquete termina listándose en pip list como uno más del entorno, según describe la guía de InfoWorld sobre el toolchain.

El truco sucio: la frontera Rust→Python cuesta más que el parseo

Belderbos usa como ejemplo un parser JSON escrito a mano en Rust —tokenizador y recursive-descent, sin serde— que se expone a Python. En su cohorte de seis semanas, la versión de un alumno superó al módulo json escrito en C de CPython sobre fixtures reales, y otro logró hasta 3,5 veces más velocidad que la versión Python pura.

El parser produce internamente un enum Rust:

pub enum JsonValue {
    Null,
    Boolean(bool),
    Number(f64),
    String(String),
    Array(Vec<JsonValue>),
    Object(HashMap<String, JsonValue>),
}

Ese árbol vive en Rust; Python nunca lo ve. La capa PyO3 es un adaptador fino encima. La función expuesta tiene esta forma:

#[pyfunction]
fn parse_json<'py>(py: Python<'py>, input: &str) -> PyResult<Bound<'py, PyAny>> {
    parse(input)?.into_pyobject(py)
}

El truco está en la última llamada: .into_pyobject(py). Recorre todo el árbol JsonValue y lo reconstruye como objetos nativos de Python —un dict por cada objeto, una list por cada array, un float o str por cada hoja—. Para un documento con 100.000 valores, eso son 100.000 objetos Python creados en la frontera, después de que el parseo ya terminó. En documentos grandes, ese bucle de materialización —no el parseo— puede dominar el tiempo total.

La conversión de errores sigue el mismo patrón: un impl From<JsonError> for PyErr mapea cada variante a una excepción de Python con su posición, así que un input malformado levanta un ValueError con el offset exacto donde se rompió el parseo, sin que la capa Rust se filtre.

¿Cuándo vale la pena portar código a Rust con PyO3?

La regla práctica que deja Belderbos:

  • Si la función devuelve un escalar (un número, una bandera, un string corto), el costo de la frontera es despreciable y se puede portar sin más.
  • Si devuelve una estructura grande, esa materialización es el verdadero cuello de botella y es lo próximo que hay que optimizar una vez que el algoritmo Rust ya es rápido.

Prealocar el PyDict ayuda en los márgenes. La victoria grande es arquitectónica: no materializar el árbol entero si el caller no va a tocarlo todo, sino devolver una vista respaldada por Rust y construir los objetos Python on demand.

Para código paralelo, PyO3 permite liberar el GIL envolviendo funciones puramente Rust en Python::allow_threads, según la guía de InfoWorld. Eso deja correr la parte pesada sin bloquear el intérprete, siempre que la función no toque objetos Python durante la ejecución.

Qué significa esto para tu startup

Si tu backend Python empieza a mostrar el CPU en llamas, no hace falta reescribir todo el stack. Muchas de las librerías que ya usas —Pydantic, partes de Hugging Face, motores de validación y parsing de alto rendimiento— ya están en Rust por debajo. Antes de migrar de lenguaje, vale la pena perfilar para ver dónde se va el tiempo y comprobar si ya existe un binding PyO3 maduro para esa tarea.

Dos acciones concretas para tu equipo:

  • Audita qué dependencias críticas ya usan Rust internamente. Pydantic, Polars, orjson y varios motores de embeddings exponen bindings PyO3. Saberlo te permite justificar upgrades sin escribir una sola línea de Rust.
  • Cuando una función pura en Python se vuelva hot path, plantéate un port parcial con PyO3 y maturin, pero perfilá la frontera, no solo el algoritmo. Si la salida es un árbol grande, diseña una vista lazy o paginada antes de invertir semanas en Rust.

En sistemas con payloads grandes o pipelines de validación intensiva, la diferencia entre 100 ms y 10 ms por request se decide exactamente en esta frontera invisible.

Conclusión

PyO3 ya es infraestructura mainstream: Pydantic v2 y un ecosistema creciente de paquetes críticos lo usan para correr Rust dentro de Python sin que el desarrollador lo note. Entender dónde está la frontera —y cuánto cuesta cruzarla— es lo que separa un port que vale la pena de una optimización que solo cambia el problema de lugar.

Fuentes

👥 ¿Quieres ir más allá de la noticia?

En nuestra comunidad discutimos las tendencias, compartimos oportunidades y nos ayudamos entre emprendedores. Sin humo, solo acción.

👥 Unirme a la comunidad

Daily Shot: Tu ventaja táctica

Lo que pasó en las últimas 24 horas, resumido para que tú no tengas que filtrarlo.

Suscríbete para recibir cada mañana la curaduría definitiva del ecosistema startup e inversionista. Sin ruido ni rodeos, solo la información estratégica que necesitas para avanzar:

  • Venture Capital & Inversiones: Rondas, fondos y movimientos de capital.
  • IA & Tecnología: Tendencias, Web3 y herramientas de automatización.
  • Modelos de Negocio: Actualidad en SaaS, Fintech y Cripto.
  • Propósito: Erradicar el estancamiento informativo dándote claridad desde tu primer café.

📡 El Daily Shot Startupero

Noticias del ecosistema startup en 2 minutos. Gratis, todos los días.

Share to...