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(())
    }