Error Handling
Python and Rust have distinct mechanisms for handling errors.
In Python, exceptions are raised when an error occurs. These exceptions propagate up the call stack until they are caught and handled.
In Rust, errors are returned as values. The Result<T, E>
enum represents the result of an operation that may fail. The T
type is the value returned on success, and the E
type is the error returned on failure.
PyO3 bridges the gap between Python and Rust error handling by using the PyResult<T>
type, which is an alias for Result<T, PyErr>
. Here, PyErr
represents a Python exception. If a PyResult
returns from Rust to Python through PyO3 and it is an Err
variant, the associated exception will be raised.
#![allow(unused)] fn main() { use pyo3::exceptions::PyValueError; use pyo3::prelude::*; #[pyfunction] fn divide(x: i32, y: i32) -> PyResult<i32> { if y == 0 { return Err(PyValueError::new_err("division by zero")); } Ok(x / y) } #[pymodule] fn my_module(m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(divide, m)?)?; Ok(()) } }
from my_module import divide
try:
divide(1, 0)
except ValueError as e:
print(e) # division by zero
Many error types in the standard library implement Into<PyErr>
, allowing the use of the ?
operator to easily propagate errors.
#![allow(unused)] fn main() { use pyo3::prelude::*; #[pyfunction] fn parse_int(s: &str) -> PyResult<i64> { Ok(s.parse()?) } #[pymodule] fn my_module(m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(parse_int, m)?)?; Ok(()) } }
from my_module import parse_int
try:
parse_int("abc")
except ValueError as e:
print(e) # invalid digit found in string
Conveniently, anyhow::Error
implements Into<PyErr>
, so you can use anyhow
for error handling in Rust and propagate errors to Python with the ?
operator.
#![allow(unused)] fn main() { use anyhow::Context; use pyo3::prelude::*; #[pyfunction] fn divide(x: i32, y: i32) -> PyResult<i32> { Ok(x.checked_div(y).context("division by zero")?) } }