Global Interpreter Lock (GIL)

If you go back to pyo3's documentation on arguments, you'll find a table column listing so called "Python-native" types. What are they, and why would you use them?

Python-native types

There is overhead in converting a Python object into a Rust-native type.
That overhead might dominate the cost of invoking your Rust function if the function itself isn't doing much computational work. In those cases, it can be desirable to work directly using Python's in-memory representation of the object. That's where the Py* types come in: they give you direct access to Python objects, with minimal overhead1.

Out of the entire family of Py* types, PyAny deserves a special mention. It's the most general Python-native type in pyo3: it stands for an arbitrary Python object. You can use it whenever you don't know the exact type of the object you're working with, or you don't care about it.

Py* types don't implement FromPyObject

Let's try to rewrite the solution of the previous exercise using PyList rather than Vec<u64>:

#![allow(unused)]
fn main() {
use pyo3::prelude::*;

fn print_number_list(list: &PyList) {
    todo!()
}
}

If you try to compile this code, you'll get an error:

error[E0277]: the trait bound `&PyList: PyFunctionArgument<'_, '_>` is not satisfied
   --> src/lib.rs:7:28
    |
7   | fn print_number_list(list: &PyList) {
    |                            ^ 
    |        the trait `PyClass` is not implemented for `&PyList`, 
    |        which is required by `&PyList: PyFunctionArgument<'_, '_>`
    |
    = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`:
              &'a pyo3::Bound<'py, T>
              Option<&'a pyo3::Bound<'py, T>>
    = note: required for `&PyList` to implement `FromPyObject<'_>`
    = note: required for `&PyList` to implement `FromPyObjectBound<'_, '_>`
    = note: required for `&PyList` to implement `PyFunctionArgument<'_, '_>`

The error message is a bit cryptic because it mentions a number of private pyo3 traits (PyFunctionArgument and FromPyObjectBound), but the gist of it is that &PyList doesn't implement FromPyObject. That's true for all Py* types.

Confusing, isn't it? How is possible that Python-native types, that require no conversion, don't implement the trait that allows you to convert Python objects into Rust types?

It's time to have that talk, the one about Python's Global Interpreter Lock (GIL).

Global Interpreter Lock (GIL)

Out of the box, Python's2 data structures are not thread-safe. To prevent data races, there is a global mutual exclusion lock that allows only one thread to execute Python bytecode at a time—i.e. the so-called Global Interpreter Lock (GIL).

It is forbidden to interact with Python objects without holding the GIL.

That's why pyo3 doesn't implement FromPyObject for Py* types: it would allow you to interact with Python objects without you necessarily holding the GIL, a recipe for disaster.

Python<'py>

pyo3 uses a combination of lifetimes and smart pointers to ensure that you're interacting with Python objects in a safe way.

Python<'py> is the cornerstone of the entire system: it's a token type that guarantees that you're holding the GIL. All APIs that require you to hold the GIL will, either directly or indirectly, require you to provide a Python<'py> token as proof.

pyo3 will automatically acquire the GIL behind the scenes whenever you invoke a Rust function from Python. In fact, you can ask for a Python<'py> token as an argument to your Rust function, and pyo3 will provide it for you—it has no (additional) cost.

#![allow(unused)]
fn main() {
use pyo3::prelude::*;
// There is no runtime difference between invoking the two functions
// below from Python.
// The first one is just more explicit about the fact that it requires
// the caller to acquire the GIL ahead of time.

#[pyfunction]
fn print_number_list(_py: Python<'_>, list: Vec<u64>) {
    todo!()
}

#[pyfunction]
fn print_number_list2(list: Vec<u64>) {
    todo!()
}
}

'py, the lifetime parameter of Python<'py>, is used to represent how long the GIL is going to be held.

Bound<'py>

You won't be interacting with Python<'py> directly most of the time.
Instead, you'll use the Bound<'py, T> type, a smart pointer that encapsulates a reference to a Python object, ensuring that you're holding the GIL when you're interacting with it.

Using Bound<'py, T> we can finally start using the Py* types as function arguments:

#![allow(unused)]
fn main() {
use pyo3::prelude::*;

#[pyfunction]
fn print_number_list(list: Bound<'_, PyList>) {
    todo!()
}
}

Bound ensures that we're holding the GIL when interacting with the list instance that has been passed to us as function argument.

FromPyObject

We can now go back to the definition of the FromPyObject trait:

#![allow(unused)]
fn main() {
pub trait FromPyObject<'py>: Sized {
    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self>;
}
}

extract_bound takes a &Bound<'py, PyAny> as argument, rather than a bare &PyAny, to ensure that we're holding the GIL when we're interacting with the Python object during the conversion.

References

Footnotes

1

pyo3 still needs to ensure that the Python object you're working with is of the expected type. It'll therefore perform an isinstance check before handing you the object—e.g. checking that an object is indeed a list before giving you a PyList argument. The only exception to this rule is PyAny, which can represent an arbitrary Python object.

2

CPython is the reference implementation of Python, written in C. It's the most widely used Python interpreter and what most people refer to when they say "Python".

Exercise

The exercise for this section is located in 01_intro/05_gil