I got plenty of feedback on my post about Calling Rust from Python: Hacker News /r/python /r/rust Many comments mentioned , and that I should use it instead of cooking my own. Thanks to the authors, I checked: in this post, I explain what it is and how I migrated my code. pyo3 What is pyo3? Rust bindings for Python, including tools for creating native Python extension modules. Running and interacting with Python code from a Rust binary is also supported. — PyO3 user guide [1] Indeed, fits my use case, calling from . Even better, it handles converting Python types to Rust types and back again. Finally, it offers the utility to make the interaction between the Python project and the Rust project seamless. pyo3 Rust Python maturin Maturin Build and publish crates with pyo3, rust-cpython, cffi and uniffi bindings as well as rust binaries as python packages. — Maturin on GitHub [2] is available via . It offers several commands: maturin pip install : create a new Cargo project with maturin configured new : build the wheels and store them locally build : build the crate into a Python package and publish it to pypi publish : build the crate as a Python module directly into the current virtual environment, making it available to Python develop Note that Maturin started as a companion project to but now offers rust-cpython, cffi and uniffi bindings. pyo3 Migrating the project The term migrating is a bit misleading here since we will start from scratch to fit Maturin’s usage. However, we will achieve the same end state. I won’t paraphrase since it works seamlessly. Ultimately, we have a fully functional Rust project with a single function, which we can call in a Python shell. Note the dependency to : the tutorial sum_as_string() pyo3 pyo3 = "0.20.0" The second step is to re-use the material from the previous project. First, we add our function at the end of the file: compute() lib.rs #[pyfunction] //1 fn compute(command: &str, a: Complex<f64>, b: Complex<f64>) -> PyResult<Complex<f64>> { //2-3 match command { "add" => Ok(a + b), "sub" => Ok(a - b), "mul" => Ok(a * b), _ => Err(PyValueError::new_err("Unknown command")), //4 } } The macro allows the use of the function in Python pyfunction Use regular Rust types for parameters; can convert them pyo3 We need to return a type, which is an alias over PyResult Result<T, PyErr> Return a specific Python error if the command doesn’t match automatically handles conversion for most types. However, complex numbers require an additional feature. We also need to migrate from the crate to the : pyo3 num num-complex pyo3 = { version = "0.20.0" , features = ["num-complex"]} num-complex = "0.4.4" To convert custom types, you must implement traits for parameters and for return values. FromPyObject ToPyObject Finally, we only need to add the function to the module: #[pymodule] fn rust_over_pyo3(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; m.add_function(wrap_pyfunction!(compute, m)?)?; //1 Ok(()) } Add the function to the module At this point, we can use Maturin to test the project: maturin develop After the compilation finishes, we can start a Python shell in the virtual environment: python >>> from rust_over_pyo3 import compute >>> compute('add',1+3j,-5j) (1-2j) >>> compute('sub',1+3j,-5j) (1+8j) Finishing touch The above setup allows us to use Rust from a Python shell but not in a Python file. To leverage the default, we must create a Python project inside the Rust project, whose name matches the Rust module name. Since I named my lib , here’s the overall structure: rust_over_pyo3 my-project ├── Cargo.toml ├── rust_over_pyo3 │ └── main.py ├── pyproject.toml └── src └── lib.rs To use the Rust library in Python, we need first to build the library. maturin build --release We manually move the artifact from to under the Python package. We can also run instead; in this case, the source file is directly under . /target/release/maturin/librust_over_pyo3.dylib rust_over_pyo3.so cargo build --release /target/release At this point, we can use the library as any other Python module: from typing import Optional from click import command, option from rust_over_pyo3 import compute (1) @command() @option('--add', 'command', flag_value='add') @option('--sub', 'command', flag_value='sub') @option('--mul', 'command', flag_value='mul') @option('--arg1', help='First complex number in the form x+yj') @option('--arg2', help='Second complex number in the form x\'+y\'j') def cli(command: Optional[str], arg1: Optional[str], arg2: Optional[str]) -> None: n1: complex = complex(arg1) n2: complex = complex(arg2) result: complex = compute(command, n1, n2) (2) print(result) if __name__ == '__main__': cli() Regular Python import Look, ma, it works! Conclusion In this post, I improved the low-level integration with to the generic ready-to-use library. I barely scratched the surface, though; is a powerful, well-maintained library with plenty of features. ctypes pyo3 pyo3 I want to thank everyone who pointed me in this direction. The complete source code for this post can be found on . GitHub To go further: [1] PyO3 user guide [2] maturin Originally published at on October 29th, 2023 A Java Geek