rustpyo3

PyO3 turn TryFrom errors into PyErr


I'm trying to write a Python wrapper to some Rust code, which does some TryFrom conversions internally:

pub enum MicrophoneType {
    Bidirectional,
    Omnidirectional,
}

impl TryFrom<char> for MicrophoneType {
    type Error = &'static str;

    fn try_from(x: char) -> Result<Self, Self::Error> {
        match x {
            'b' => Ok(Self::Bidirectional),
            'o' => Ok(Self::Omnidirectional),
            _ => Err("MicrophoneType: Invalid character given"),
        }
    }
}

#[pyfunction]
fn compute(
    _py: Python,
    microphone: char,
) -> PyResult<()> {
    let _microphone = MicrophoneType::try_from(microphone)?;

    Ok(())
}

However I'm seeing the error message

error[E0277]: `?` couldn't convert the error to `PyErr`
  --> rir_generator_py/src/lib.rs:59:59
   |
58 | ) -> PyResult<()> {
   |      ------------ expected `PyErr` because of this
59 |     let _microphone = MicrophoneType::try_from(microphone)?;
   |                                                           ^ the trait `From<&str>` is not implemented for `PyErr`

and I am confused as to why.

It seems that PyO3 does not know what to do with the type type Error = &'static str; in my impl, right? But isn't that the default way of giving error messages? Shouldn't PyO3 expect these, if it's a common pattern? What am I missing here?


Solution


After finding these docs I've realized a few things things:

Firstly, it's not a good pattern to use type Error = &'static str;, instead you should use a custom error that can be matched and From/Intoed, if needed

#[derive(Debug)]
pub struct InvalidMicrophoneCharError {}

impl Error for InvalidMicrophoneCharError {}

impl fmt::Display for InvalidMicrophoneCharError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Incorrect microphone character")
    }
}

impl TryFrom<char> for MicrophoneType {
    type Error = InvalidMicrophoneCharError;

    fn try_from(x: char) -> Result<Self, Self::Error> {
        match x {
            'b' => Ok(Self::Bidirectional),
            'o' => Ok(Self::Omnidirectional),
            _ => Err(InvalidMicrophoneCharError {}),
        }
    }
}

Secondly, you have to use the newtype pattern to create a "From-chain" that starts with your type and ends with a PyErr, which can then be combined with PyO3's support for Rust-native Result<T, E>s:

struct MyInvalidMicrophoneCharError(InvalidMicrophoneCharError);

impl From<MyInvalidMicrophoneCharError> for PyErr {
    fn from(err: MyInvalidMicrophoneCharError) -> Self {
        PyValueError::new_err(err.0.to_string())
    }
}

impl From<InvalidMicrophoneCharError> for MyInvalidMicrophoneCharError {
    fn from(other: InvalidMicrophoneCharError) -> Self {
        Self(other)
    }
}

#[pyfunction]
fn compute(
    _py: Python,
    microphone: char,
) -> Result<(), MyInvalidMicrophoneCharError> {
    let _microphone = MicrophoneType::try_from(microphone)?;

    Ok(())
}

or skip the chain and simply use map_err()

#[pyfunction]
fn compute(
    _py: Python,
    microphone: char,
) -> Result<(), MyInvalidMicrophoneCharError> {
    let _microphone = MicrophoneType::try_from(microphone)
        .map_err(|error| PyValueError::new_err(error.to_string()))?;

    Ok(())
}