Learn how to write your first Python module in Rust.
I'm not going to open with a long-winded introduction explaining why Rust is so awesome and great. I don't know if it is or not. I just hear a lot about Rust and I wanted to try and write a Python module in Rust. So, I did!
To follow this tutorial, you need to start by installing Rust. You can head over to rust-lang.org and follow the installation instructions there.
Then, we'll use PyO3 to bind Python and Rust together. To make our lives easier, we set everything up through maturin, which I understand simplifies the process a bit.
Start by creating a virtual environment in a folder for this project:
$ mkdir rust-factorial
$ cd rust-factorial
$ python -m venv .env
$ source .env/bin/activate # Activate the virtual env.
$ pip install maturin
After installing maturin, we use the command maturin init
to create a project.
When asked for it, select the pyo3
bindings!
Maturin will go ahead and create a couple of files for you. They'll contain a simple, working example of a Python module written in Rust.
You can actually see it already works by running the command maturin develop
and then using the module from Python:
$ maturin develop
# lots of progress output as maturin runs the compilation...
$ python
>>> import rust_factorial
>>> rust_factorial.sum_as_string(5, 20)
'25'
Now, let us write our own Rust code.
As my first attempt at writing some Rust code, I decided to write the factorial function. Between some educated guesses and compiler errors, I managed to put this together:
fn factorial(n: u128) -> u128 {
if n <= 1 {
return n
} else {
return n * factorial(n - 1)
}
}
This is the base Rust code I used.
Then, I need to fit this into the file lib.rs
that maturin created for me:
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 rust_factorial(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
Ok(())
}
The function factorial
is going to replace the function sum_as_string
, so it looks like it takes some arguments and then it needs to return a data type that is compatible with Python.
That's what I gather from the PyResult<String>
result.
The PyO3 documentation tells me I can use Rust integer types inside the PyResult
, so I tried this:
use pyo3::prelude::*;
fn factorial(n: u128) -> PyResult<u128> {
if n <= 1 {
Ok(n)
} else {
Ok(n * factorial(n - 1))
}
}
#[pymodule]
fn rust_factorial(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(factorial, m)?)?;
Ok(())
}
Now, this gives me errors, because the recursive function is trying to multiply a regular Rust integer with the PyResult
thing, so I actually added my original, 100% Rust function as _factorial
, and then I wrote a wrapper around that:
use pyo3::prelude::*;
#[pyfunction]
fn factorial(n: u128) -> PyResult<u128> {
Ok(_factorial(n))
}
fn _factorial(n: u128) -> u128 {
if n <= 1 {
return n
} else {
return n * _factorial(n - 1)
}
}
#[pymodule]
fn rust_factorial(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(factorial, m)?)?;
Ok(())
}
Now, running maturin develop
should enable you to use this factorial function:
$ maturin develop
$ python
>>> import rust_factorial
>>> rust_factorial.factorial(15)
1307674368000
>>> import math
>>> rust_factorial.factorial(20) == math.factorial(20)
True
And that's it! That's pretty much the whole account of my adventure! It was surprisingly easy to get everything up and running although I did write code with errors! I didn't even need to get ChatGPT's help, though!
Do you have any experience with writing Python modules in Rust? Get in touch if you do!
+35 chapters. +400 pages. Hundreds of examples. Over 30,000 readers!
My book “Pydon'ts” teaches you how to write elegant, expressive, and Pythonic code, to help you become a better developer. >>> Download it here 🐍🚀.