Convert Python dictionary to Rust struct using `pyo3`

I would like to convert a Python dictionary to a Rust struct in the framework of pyo3. Specifically, I have a Rust struct that looks like

struct MyStruct {
  key_1: Option<String>,
  key_2: Option<i32>,
  key_3: Option<bool>,
}

I would like to take a Python dictionary and assign matching keys, if they exist, ignoring the rest.
e.g.

my_dict = { 'key_1': 'value_1', 'key_3': True, 'invalid_key': 42 }

should result in the MyStruct

MyStruct {
  key_1: Some("value_1"),
  key_2: None,
  key_3: Some(true),
}

I attempted to do this by marking MyStruct with the pyo3::pyclass attribute, but receive the Python error TypeError: argument 'my_dict': 'dict' object cannot be converted to 'MyStruct'. I then tried to implment the pyo3::FromPyObject trait for MyStruct, but receive the Rust error conflicting implementation in crate 'pyo3': - impl<'a, T> pyo3::FromPyObject<'a> for Twhere T: PyClass, T: Clone;, which I guess is being implemented from pyclass.

Is there a way to implement this Python dictionary to Rust struct conversion?

Caveat

I would also like to map fields from the Python dictionary to fields with different names in the Rsut struct.
e.g.

my_dict = { 'str_val': 'a string', 'bool_val': false, 'num_val': -1}

should be mapped to

MyStruct {
  key_1: Some("a string"),
  key_2: Some(false),
  key_3: Some(-1),
}

Just write a function. Or if you want traits, define your own trait and implement that.
This should be easy given that you already have done it and got a "conflicting implementation" error.
The conversion you're trying to do is some sort of serialization/deserialization and that is not supported out of the box in PyO3 as far as I know.

Thanks @RedDocMD , could you elaborate a bit more on how to use a function to do the conversion.

I am passing the Python dictionary to a function I expose through my pyo3 module

use pyo3::prelude::*;

#[pyfunction]
fn my_fn(my_struct: MyStruct) {
    // do stuff with my_struct
}

#[pymodule]
fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(my_fn, m)?)?;
    Ok(())
}

so I don't understand at which point I should interject for the conversion.

Do I need to change the signature of my_fn to accept a pyo3::PyDict instead of a MyStruct, then do the conversion in my_fn?

It looks like Py<T>: Deserialize where T: Deserialize + PyClass. Which would suggest an easy solution:

  • add #[pyclass], as you already tried, to satisfy the PyClass bound
  • add #[derive(Deserialize)], to satisfy the Deserialize bound
  • convert PyDict to dynamically-typed, deserializeable value tree (e.g. serde_json::Value) or another serialization format (e.g. json.dumps() it)
  • Py::<MyStruct>::deserialize() from the dynamically-typed value tree/raw serialized data (this can alternatively be MyStruct::deserialize() if you don't need it to be a PyClass)
2 Likes

@H2CO3 , thank you for the suggestion. So in this case my_fn should accept a PyDict and I should do the conversion to a MyStruct within the function?

It doesn't really matter in terms of functionality where exactly the conversion happens. I would, however, strongly prefer if functions took actual static types as arguments instead of arbitrary/dynamic key-value maps, for reasons of readability/correctness/maintainability.

Thanks to @RedDocMD and @H2CO3 I came up with this solution. If there are suggestions for how to improve I am always happy to receive feedback.

  1. The function being called (my_fn in above discussion) has the signature my_fn(py: Python<'_>, dict: HashMap<String, PyObject>
  2. In my_fn I call a function convert_hashmap_to_my_struct that converts dict to a MyStruct manually.
  3. The conversion function has signature convert_hashmap_to_my_struct(py: Python<'_>, map: HashMap<String, PyObject>) -> MyStruct.
  4. Values for the MyStruct are extracted from the PyObject values of map using the extract function. The extracted value is then used to manually build a MyStruct, which is returned.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.