Exceptions

Python and Rust have different error handling mechanisms.
In Python, you raise exceptions to signal that something went wrong.
In Rust, errors are normal values that you return from functions, usually via the Result type.

pyo3 provides PyResult<T> to help you bridge the gap between these two worlds.

PyResult<T>

PyResult<T> is the type you'll return whenever your #[pyfunction] can fail.
It is a type alias for Result<T, PyErr>, where PyErr is pyo3's representation of a Python exception. pyo3 will automatically raise a Python exception whenever a #[pyfunction] returns Err(PyErr) value:

#![allow(unused)]
fn main() {
use pyo3::prelude::*;
use pyo3::types::PyAny;

#[pyfunction]
fn print_if_number(item: Bound<'_, PyAny>) -> PyResult<()> {
    let number = item.extract::<u64>()?;
    println!("{}", number);
    Ok(())
}
}

In the example above, extract::<u64>()? returns a PyResult<u64>.
If the object is not an unsigned integer, extract will return an error, which will be propagated up to the caller via the ? operator. On the Python side, this error will be raised as a Python exception by pyo3.

Built-in exception types

You should be intentional about the types of exceptions you raise. What kind of error are you signaling? What is the caller expected to catch?

All built-in Python exceptions are available in pyo3::exceptions—e.g. pyo3::exceptions::PyValueError for a ValueError. You can use their new_err method to create an instance.

Panics

Rust provides another mechanism for handling "unrecoverable" errors: panics. What happens if you panic in a #[pyfunction]?
pyo3 will catch the panic and raise a pyo3_runtime.PanicException to the Python caller. You've probably seen this behaviour at play when solving the exercises associated to the previous sections.

Exercise

The exercise for this section is located in 01_intro/07_exceptions