Skip to content

Bridging Rust and Python: Building Python Extensions with PyO3

Rust and Python are a match made in heaven:
- Python is flexible and has an amazing ecosystem.
- Rust is fast, safe, and low-level.

What if you could use Rust’s performance directly inside your Python programs? That’s exactly what PyO3 lets you do.

In this guide, we’ll build a Python extension in Rust that processes input events, translates text, and even converts TOML to JSON—all callable from Python.


Step 1: Project Setup

First, create a Rust library project:

cargo new --lib afrim_py
cd afrim_py
````

Update `Cargo.toml`:

```toml
[lib]
name = "afrim_py"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.21", features = ["extension-module"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
pythonize = "0.21"

Install maturin for packaging:

pip install maturin

Step 2: Writing Rust Functions for Python

Let’s start with a simple Rust function exposed to Python.

use pyo3::prelude::*;
use toml;

/// Convert TOML to JSON and return it as a Python string.
#[pyfunction]
fn convert_toml_to_json(content: &str) -> PyResult<String> {
    let data: toml::Value = toml::from_str(content)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(serde_json::to_string(&data)?)
}

In lib.rs, register it as part of a Python module:

#[pymodule]
fn afrim_py(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(convert_toml_to_json, m)?)?;
    Ok(())
}

Step 3: Exposing Rust Structs as Python Classes

You can also expose Rust structs as Python classes. For example, here’s a Preprocessor with methods:

#[pyclass]
pub struct Preprocessor {
    buffer: String,
}

#[pymethods]
impl Preprocessor {
    #[new]
    fn new() -> Self {
        Self { buffer: String::new() }
    }

    fn process(&mut self, input: &str) {
        self.buffer.push_str(input);
    }

    fn get_buffer(&self) -> String {
        self.buffer.clone()
    }
}

Now Python can do:

from afrim_py import Preprocessor

p = Preprocessor()
p.process("Hello")
print(p.get_buffer())  # "Hello"

Step 4: Building and Installing

Run:

maturin develop

This builds your Rust code into a Python extension and installs it locally.


Step 5: Using It in Python

Here’s a taste of what you can now do:

from afrim_py import convert_toml_to_json, Preprocessor

print(convert_toml_to_json("[info]\nname = 'sample'"))
# {"info": {"name": "sample"}}

p = Preprocessor()
p.process("Rust ❤️ Python")
print(p.get_buffer())
# "Rust ❤️ Python"

Why This Matters

  • Performance: Rust handles heavy tasks much faster than pure Python.
  • Safety: No segfaults or data races.
  • Integration: You can extend existing Python libraries instead of rewriting them.

This approach is perfect for:

  • Accelerating parsing, preprocessing, or machine learning pipelines.
  • Reusing Rust libraries in Python apps.
  • Building new Python modules powered by Rust.

Final Thoughts

With PyO3 and maturin, bridging Rust and Python is easier than ever. You write safe, fast Rust code—and expose it directly as Python modules.

If you're building performance-critical Python tools, give this combo a try. It's like giving Python a turbo engine without losing its simplicity.


Happy hacking with Rust & Python!

```