Skip to content

Import your Rust code in Python

Posted on:November 28, 2023 at 03:22 PM

Import your Rust code in Python

Why? I started this as a joke. I had just started learning Rust, and as everyone knows Rust is fast! Python is slow. It does not have a type system, which always makes me feel like I am always walking on thin ice. So if was working on a Python project with a team, and I wrote Rust code that could be imported it in Python, I would have the type system, the speed, and no one would have to “re-write everything in Rust”. Awesome! I get the added benefit of telling everyone that you wrote it in Rust and be the 10x engineer that I am striving to be.

With the intention to spin up a little project that can be imported in Python, I got to the internet search and found PyO3! I understood that I was not the first person to have this bright idea.

Check out PyO3 btw: https://pyo3.rs

A small test project

It’s admittedly very simple to get started with PyO3, but I thought that if I was going to screw it up in my first attempt, it should be a side-project or a dummy project. I also wanted to approach this a little differently than what I had previously joked about. I wanted to build a Python library using Rust. It would have Rust code, but install with PyPi and would be usable by simply importing it in a Python file. I’ll briefly explain the process with an extremely simple example, and leave the hard work to you.

This library will be called utkarshpy will have a yell_name function that can be called, and it will print on the screen, in my case, my name. You can take the creative liberty to put whatever you like.

Start a cargo project: maturin

This is the quick boilerplate stuff. We’re making a Rust project, running a init and getting our Cargo.toml and our src directory ready. Now this is where we are introduced to the first new thing, maturin. It’s a zero-config tool for building our library.

Starting the project is simple, we create our project folder, and from in it, run a maturin init and get going. It gets us the basic boilerplate: Cargo.toml, a src directory, and even a .github that has some basic CI workflow to build the project. To change the name of the lib, we edit the Cargo.toml. If we run a maturin build on this project as it is, we’ll see that it can make a Python dynamic library file already, and we can install the wheel for the library, usually placed in the target/wheels folder.

If you are following this around the same time that it was written, this would be in your src/lib.rs file. This is the entrypoint of our library, and the #[pymodule] denotes that it is the Python module implementation.

use pyo3::prelude::*;

/// Formats the sum of two numbers as string.
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
    Ok((a + b).to_string())
}

/// A Python module implemented in Rust.
#[pymodule]
fn sample(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
    Ok(())
}

Writing my own function

We now get to adding our function yell_name.

#[pyfunction]
fn yell_name() -> PyResult<()> {
    println!("HELLO UTKARSH!");
    Ok(())
}

Quite simple! Just add this to the module using the add_function method.

m.add_wrapped(wrap_pyfunction!(yell_name))?;

Install the library and use it

Mostly done! Build it again using maturin build and install the wheel using pip install <wheel file>.

In a main.py, we just need to import it.

import utkarshpy

utkarshpy.yell_name()
 python main.py
HELLO UTKARSH!

It worked! PyO3 makes this really simple! This is what I ended up with: https://github.com/tumblingpointers/004-rust-python-library

A big project

I work on ML projects. I write inference applications as a software engineer, and Python is my only option. And to be honest, it is perfectly fine! There are plenty of capable Python API frameworks: Flask, FastAPI and others. But after having done this small stunt, I thought, can we do this in Rust’s Actix web framework. I present to you: Meteorite ☄️ GitHub. It implements a simple job queue, uses webhooks to return a response of the job, and uses the Actix web framework to process requests.

Does it really give a huge boost in performance? I have honestly not benchmarked it. It works well, it’s easy to maintain, has some in-built stuff that works really well with ML API development, and does not make me anxious as I try to expand its features. So I develop this further! And btw, I’m the 10x engineer now who is writing this in Rust!