Output values

We've gone deep into the weeds of how pyo3 handles arguments to your #[pyfunction]s. Let's now move our focus to output values: how do you return something from your Rust functions to Python?

IntoPyObject

Guess what? There's a trait for that too!
IntoPyObject is the counterpart of FromPyObject. It converts Rust values into Python objects:

#![allow(unused)]
fn main() {
pub trait IntoPyObject<'py>: Sized {
    type Target;
    type Output: BoundObject<'py, Self::Target>;
    type Error: Into<PyErr>;

    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error>;
}
}

The output type of your #[pyfunction] must implement IntoPyObject.

IntoPyObject::into_pyobject

IntoPyObject::into_pyobject expects two arguments:

  • self: the Rust value you want to convert into a Python object.
  • Python<'py>: a GIL token that you can use to create new Python objects.

The conversion can fail, so the method returns a Result.
The output type itself is more complex, so let's break it down using an example.

Case study: a newtype

Let's look at a simple example: a newtype that wraps a u64. We want it to be represented as a "plain" integer in Python.

#![allow(unused)]
fn main() {
use std::convert::Infallible;
use pyo3::prelude::*;
use pyo3::types::PyInt;

struct MyType {
    value: u64,
}

impl<'py> IntoPyObject<'py> for MyType {
    /// `Target` is the **concrete** Python type we want to use
    /// to represent our Rust value.
    /// The underlying Rust type is a `u64`, so we'll convert it to a `PyInt`,
    /// a Python integer.
    type Target = PyInt;
    /// `Output`, instead, is a **wrapper** around the concrete type.
    /// It captures the ownership relationship between the Python object
    /// and the Python runtime.
    /// In this case, we're using a `Bound` smart pointer to a `PyInt`.
    /// The `'py` lifetime ensures that the Python object is owned 
    /// by the Python runtime.
    type Output = Bound<'py, PyInt>;
    /// Since the conversion can fail, we need to specify an error type.
    /// We can't fail to convert a `u64` into a Python integer,
    /// so we'll use `Infallible` as the error type.
    type Error = Infallible;

    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
        // `u64` already implements `IntoPyObject`, so we delegate 
        // to its implementation to do the actual conversion.
        self.value.into_pyobject(py)
    }
}
}

The Output associated type

Let's focus on the Output associated type for a moment.
In almost all cases, you'll be setting Output to Bound<'py, Self::Target>1. You're creating a new Python object and its lifetime is tied to the Python runtime.

In a few cases, you might be able to rely on Borrowed<'a, 'py, Self::Target> instead. It's slightly faster2, but it's limited to scenarios where you are borrowing from an existing Python object—fairly rare for an IntoPyObject implementation.

There are no other options for Output, since Output must implement the BoundObject trait, the trait is sealed and those two types are the only implementors within pyo3.
If it helps, think of Output as an enum with two variants: Bound and Borrowed.

Provided implementations

pyo3 provides out-of-the-box implementations of IntoPyObject for many Rust types, as well as for all Py* types. Check out its documentation for an exhaustive list.

1

The actual syntax is a bit more complex: type Output = Bound<'py, <Self as IntoPyObject<'py>>::Target>>;. We've simplified it for clarity.

2

In addition to its documentation, you may find this issue useful to understand the trade-offs between &Bound and Borrowed.

Exercise

The exercise for this section is located in 01_intro/06_output