A First Look of PyO3

Introduction

PyO3 is a Rust library for building Python bindings. In this post, we will try to build a simple Python module with a few features using PyO3. For exhaustive documentation, please refer to the PyO3 user guide.

Setup

To learn how to setup PyO3, please follow the PyO3 user guide. I have save my code in this repository. Please follow the README there to run my code.

A Simple Python Module

Let us implement a single Python function in Rust.

1
2
3
4
5
6
use pyo3::prelude::*;

#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
    Ok((a + b).to_string())
}

We can call the function with the following Python code.

1
2
3
import pyo3_test

print(pyo3_test.sum_as_string(3, 5))

The output is 8.

A Python Class

Next, we will implement a Python function that returns a Python class in Rust.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#[pyclass]
struct MyClass {
    #[pyo3(get, set)]
    num: i32,
}

#[pyfunction]
fn return_myclass() -> Py<MyClass> {
    Python::with_gil(|py| -> Py<MyClass> {
        Py::new(py, MyClass { num: 1 }).unwrap()
    })
}

Note that we use Py::new() to allocate the class in Python GIL memory.

We can use the class in Python.

1
print(pyo3_test.return_myclass().num)

The output is 1.

A Python Iterator

Next, we will implement something very Pythonic: an iterator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#[pyclass]
struct PyClassIter {
    count: usize,
}

#[pymethods]
impl PyClassIter {
    #[new]
    pub fn new() -> Self {
        PyClassIter { count: 0 }
    }

    fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
        slf
    }

    fn __next__(&mut self) -> Option<usize> {
        if self.count < 5 {
            self.count += 1;
            // Given an instance `counter`, First five `next(counter)` calls yield 1, 2, 3, 4, 5.
            Some(self.count)
        } else {
            None
        }
    }
}

We can use the iterator in Python.

1
2
for i in pyo3_test.PyClassIter():
    print(i)

The output is 1, 2, 3, 4, 5.

A Python Async Iterator

Next, we want to have even more fun: an async iterator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#[pyclass]
struct PyAsyncIter {
    count: Arc<Mutex<usize>>,
}

#[pymethods]
impl PyAsyncIter {
    #[new]
    pub fn new() -> Self {
        PyAsyncIter { count: Arc::new(Mutex::new(0)) }
    }

    fn __aiter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
        slf
    }

    fn __anext__(slf: PyRefMut<'_, Self>) -> PyResult<Option<PyObject>> {
        let count = slf.count.clone();
        let fut = pyo3_asyncio::tokio::future_into_py(slf.py(), async move {
            let mut count = count.lock().unwrap();
            if *count < 5 {
                *count += 1;
                Ok(Python::with_gil(|py| count.into_py(py)))
            } else {
                Err(PyStopAsyncIteration::new_err("stream exhausted"))
            }    
        })?;
        Ok(Some(fut.into()))
    }
}

There is a bit of boilerplate code. Hope PyO3 0.20 will make it easier.

We can use the async iterator in Python.

1
2
async for i in pyo3_test.PyAsyncIter():
    print(i)

The output is 1, 2, 3, 4, 5.

Summary

PyO3 is a very powerful library for building Python bindings. We can build very Pythonic modules in Rust.

Finally, we will implement a Python module in Rust.


Last modified on 2023-07-29