pyo3 icon indicating copy to clipboard operation
pyo3 copied to clipboard

Unpacking methods on sequences

Open kaathewisegit opened this issue 1 month ago • 2 comments

Python supports unpacking all objects which support the Sequence protocol. I believe it does so by simply calling __getitem__ for consecutive integer indices.

Currently in PyO3 only tuple objects can be extracted as Rust tuples. I think it'd be nice to have an unpack method which would emulate Python's unpacking behavior and support all objects which implement the Sequence interface.

I'd implement it as a utility trait:

trait Unpackable<'py>: Sized {
	fn unpack(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<Self>;
}

Which can be implemented for tuples and arrays up to a certain size.

impl<'py, T0, T1, T2> Unpackable<'py> for (T0, T1, T2)
where
	T0: for<'a> FromPyObject<'a, 'py>,
	T1: for<'a> FromPyObject<'a, 'py>,
	T2: for<'a> FromPyObject<'a, 'py>,
{
	fn unpack(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
		let t0 = match obj.get_item(0)?.extract::<T0>() {
			Ok(v) => v,
			Err(e) => return Err(e.into()),
		};
		let t1 = match obj.get_item(1)?.extract::<T1>() {
			Ok(v) => v,
			Err(e) => return Err(e.into()),
		};
		let t2 = match obj.get_item(2)?.extract::<T2>() {
			Ok(v) => v,
			Err(e) => return Err(e.into()),
		};

		Ok((t0, t1, t2))
	}
}

I used match here because I couldn't get the Into<PyErr> try operator inference to work on extract in this example.

The trait does not support depending on the 'a lifetime of the object because get_item creates new objects which are tied to the scope of the unpack function.

Additionally, this function can use __len__ for better error messages. But that would technically deviate from the official implementation: Python allows returning nonsensical lengths in __len__, unpacking will work correctly regardless

kaathewisegit avatar Nov 12 '25 16:11 kaathewisegit

Thanks for the suggestion, it's an interesting idea. You present the trait, I guess users could call as

// subject to type inference
let (x, y, z) = Unpackable::unpack(obj)?;

I wonder if we can make it more obvious that this is mirroring Python sequence unpacking...

davidhewitt avatar Nov 18 '25 11:11 davidhewitt

Type inference might be tricky here, to the same degree that extract has to deal with. It'll probably require annotations somewhere. I personally prefer the let form:

use pyo3::Unpackable;

let (a, c, g, t): (f64, f64, f64, f64) = obj.unpack()?;

Another alternative would be to add a standalone function with a generic:

let (a, c, g, t) = unpack::<(f64, f64, f64, f64)>(obj)?;

Personally, I find Rust destructuring similar to Python's unpacking. I a bunch of snippets like those ones my mixed Python/Rust codebase:

let (left, right) = tree.children_of(&internal);
left, right = tree.children_of(node)

kaathewisegit avatar Nov 18 '25 17:11 kaathewisegit