Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 21 additions & 17 deletions guide/src/free-threading.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,36 @@ This document provides advice for porting Rust code using PyO3 to run under free

## Supporting free-threaded Python with PyO3

Many simple uses of PyO3, like exposing bindings for a "pure" Rust function with no side-effects or defining an immutable Python class, will likely work "out of the box" on the free-threaded build.
All that will be necessary is to annotate Python modules declared by rust code in your project to declare that they support free-threaded Python, for example by declaring the module with `#[pymodule(gil_used = false)]`.
Since PyO3 0.28, PyO3 defaults to assuming Python modules created with it are thread-safe.
This will be the case except for Rust code which has used `unsafe` to assume thread-safety incorrectly.
An example of this is `unsafe` code which was written with the historical assumption that Python was single-threaded due to the GIL, and so the `Python<'py>` token used by PyO3 could be used to guarantee thread-safety.
A module can opt-out of supporting free-threaded Python until it has audited its `unsafe` code for correctness by declaring the module with `#[pymodule(gil_used = true)]` (see below).

More complicated `#[pyclass]` types may need to deal with thread-safety directly; there is [a dedicated section of the guide](./class/thread-safety.md) to discuss this.
Complicated `#[pyclass]` types may need to deal with thread-safety directly; there is [a dedicated section of the guide](./class/thread-safety.md) to discuss this.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Want me to go over these docs once more to see if they still make sense with the new default?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping docs fresh is always much appreciated! I think I got everything but will never say no to another pair of eyes 😄


At a low-level, annotating a module sets the `Py_MOD_GIL` slot on modules defined by an extension to `Py_MOD_GIL_NOT_USED`, which allows the interpreter to see at runtime that the author of the extension thinks the extension is thread-safe.
You should only do this if you know that your extension is thread-safe.
Because of Rust's guarantees, this is already true for many extensions, however see below for more discussion about how to evaluate the thread safety of existing Rust extensions and how to think about the PyO3 API using a Python runtime with no GIL.

If you do not explicitly mark that modules are thread-safe, the Python interpreter will re-enable the GIL at runtime while importing your module and print a `RuntimeWarning` with a message containing the name of the module causing it to re-enable the GIL.
By opting-out of supporting free-threaded Python, the Python interpreter will re-enable the GIL at runtime while importing your module and print a `RuntimeWarning` with a message containing the name of the module causing it to re-enable the GIL.
You can force the GIL to remain disabled by setting the `PYTHON_GIL=0` as an environment variable or passing `-Xgil=0` when starting Python (`0` means the GIL is turned off).

If you are sure that all data structures exposed in a `PyModule` are thread-safe, then pass `gil_used = false` as a parameter to the `pymodule` procedural macro declaring the module or call `PyModule::gil_used` on a `PyModule` instance.
For example:

```rust,no_run
use pyo3::prelude::*;
### Example opting-in

(Note: for PyO3 versions 0.23 through 0.27, the default was `gil_used = true` and so the opposite was needed; modules needed to opt-in to free-threaded Python support with `gil_used = false`.)

/// This module supports free-threaded Python
#[pymodule(gil_used = false)]
fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> {
// add members to the module that you know are thread-safe
Ok(())
```rust,no_run
/// This module relies on the GIL for thread safety
#[pyo3::pymodule(gil_used = true)]
mod my_extension {
use pyo3::prelude::*;

// this type is not thread-safe
#[pyclass]
struct MyNotThreadSafeType {
// insert not thread-safe code
}
}
```

Expand All @@ -53,15 +60,12 @@ use pyo3::prelude::*;
# #[allow(dead_code)]
fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> {
let child_module = PyModule::new(parent_module.py(), "child_module")?;
child_module.gil_used(false)?;
child_module.gil_used(true)?;
parent_module.add_submodule(&child_module)
}

```

For now you must explicitly opt in to free-threading support by annotating modules defined in your extension.
In a future version of `PyO3`, we plan to make `gil_used = false` the default.

See the [`string-sum`](https://github.com/PyO3/pyo3/tree/main/pyo3-ffi/examples/string-sum) example for how to declare free-threaded support using raw FFI calls for modules using single-phase initialization and the [`sequential`](https://github.com/PyO3/pyo3/tree/main/pyo3-ffi/examples/sequential) example for modules using multi-phase initialization.

If you would like to use conditional compilation to trigger different code paths under the free-threaded build, you can use the `Py_GIL_DISABLED` attribute once you have configured your crate to generate the necessary build configuration data.
Expand Down
7 changes: 7 additions & 0 deletions guide/src/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ For a detailed list of all changes, see the [CHANGELOG](changelog.md).

## from 0.27.* to 0.28

### Default to supporting free-threaded Python

When PyO3 0.23 added support for free-threaded Python, this was as an opt-in feature for modules by annotating with `#[pymodule(gil_used = false)]`.

As the support has matured and PyO3's own API has evolved to remove reliance on the GIL, the time is right to switch the default.
Modules now automatically allow use on free-threaded Python, unless they directly state they require the GIL with `#[pymodule(gil_used = true)]`.

### Deprecation of automatic `FromPyObject` for `#[pyclass]` types which implement `Clone`

`#[pyclass]` types which implement `Clone` used to also implement `FromPyObject` automatically.
Expand Down
1 change: 1 addition & 0 deletions newsfragments/5564.packaging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support for free-threaded Python is now opt-out rather than opt-in.
8 changes: 1 addition & 7 deletions pyo3-macros-backend/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,6 @@ fn module_initialization(

impl_::ModuleDef::new(__PYO3_NAME, #doc, &SLOTS)
};
#[doc(hidden)]
// so wrapped submodules can see what gil_used is
pub static __PYO3_GIL_USED: bool = #gil_used;
};
if !is_submodule {
result.extend(quote! {
Expand All @@ -547,10 +544,7 @@ fn module_initialization(
#[doc(hidden)]
#[export_name = #pyinit_symbol]
pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject {
_PYO3_DEF.init_multi_phase(
unsafe { #pyo3_path::Python::assume_attached() },
#gil_used
)
_PYO3_DEF.init_multi_phase()
}
});
}
Expand Down
1 change: 1 addition & 0 deletions pyo3-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use syn::{parse_macro_input, Item};
/// | `#[pyo3(submodule)]` | Skips adding a `PyInit_` FFI symbol to the compiled binary. |
/// | `#[pyo3(module = "...")]` | Defines the Python `dotted.path` to the parent module for use in introspection. |
/// | `#[pyo3(crate = "pyo3")]` | Defines the path to PyO3 to use code generated by the macro. |
/// | `#[pyo3(gil_used = true)]` | Declares the GIL is needed to run this module safely under free-threaded Python. |
///
/// For more on creating Python modules see the [module section of the guide][1].
///
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/awaitable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl FutureAwaitable {
}
}

#[pymodule(gil_used = false)]
#[pymodule]
pub fn awaitable(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<IterAwaitable>()?;
m.add_class::<FutureAwaitable>()?;
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/buf_and_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ fn return_memoryview(py: Python<'_>) -> PyResult<Bound<'_, PyMemoryView>> {
PyMemoryView::from(&bytes)
}

#[pymodule(gil_used = false)]
#[pymodule]
pub fn buf_and_str(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<BytesExtractor>()?;
m.add_function(wrap_pyfunction!(return_memoryview, m)?)?;
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/comparisons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ impl OrderedDefaultNe {
}
}

#[pymodule(gil_used = false)]
#[pymodule]
pub mod comparisons {
#[pymodule_export]
use super::{
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ impl TzClass {
}
}

#[pymodule(gil_used = false)]
#[pymodule]
pub fn datetime(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(make_date, m)?)?;
m.add_function(wrap_pyfunction!(get_date_tuple, m)?)?;
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/enums.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use pyo3::{pyclass, pyfunction, pymodule};

#[pymodule(gil_used = false)]
#[pymodule]
pub mod enums {
#[pymodule_export]
use super::{
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub mod pyfunctions;
pub mod sequence;
pub mod subclassing;

#[pymodule(gil_used = false)]
#[pymodule]
mod pyo3_pytests {
use super::*;

Expand Down
2 changes: 1 addition & 1 deletion pytests/src/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny>
Ok(())
}

#[pymodule(gil_used = false)]
#[pymodule]
pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(issue_219, m)?)?;
m.add_function(wrap_pyfunction!(hammer_attaching_in_thread, m)?)?;
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/objstore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl ObjStore {
}
}

#[pymodule(gil_used = false)]
#[pymodule]
pub fn objstore(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<ObjStore>()
}
2 changes: 1 addition & 1 deletion pytests/src/othermod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ fn double(x: i32) -> i32 {
x * 2
}

#[pymodule(gil_used = false)]
#[pymodule]
pub fn othermod(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(double, m)?)?;

Expand Down
2 changes: 1 addition & 1 deletion pytests/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fn take_pathbuf(path: PathBuf) -> PathBuf {
path
}

#[pymodule(gil_used = false)]
#[pymodule]
pub fn path(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(make_path, m)?)?;
m.add_function(wrap_pyfunction!(take_pathbuf, m)?)?;
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/pyclasses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ fn map_a_class(cls: AClass) -> AClass {
cls
}

#[pymodule(gil_used = false)]
#[pymodule]
pub mod pyclasses {
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
#[pymodule_export]
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fn vec_to_vec_pystring(vec: Vec<Bound<'_, PyString>>) -> Vec<Bound<'_, PyString>
vec
}

#[pymodule(gil_used = false)]
#[pymodule]
pub fn sequence(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(vec_to_vec_i32, m)?)?;
m.add_function(wrap_pyfunction!(array_to_array_i32, m)?)?;
Expand Down
2 changes: 1 addition & 1 deletion pytests/src/subclassing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ impl Subclassable {
}
}

#[pymodule(gil_used = false)]
#[pymodule]
pub fn subclassing(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Subclassable>()?;
Ok(())
Expand Down
16 changes: 5 additions & 11 deletions src/impl_/pymodule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ pub struct ModuleDef {
interpreter: AtomicI64,
/// Initialized module object, cached to avoid reinitialization.
module: PyOnceLock<Py<PyModule>>,
/// Whether or not the module supports running without the GIL
gil_used: bool,
}

unsafe impl Sync for ModuleDef {}
Expand Down Expand Up @@ -95,17 +93,16 @@ impl ModuleDef {
))]
interpreter: AtomicI64::new(-1),
module: PyOnceLock::new(),
gil_used: false,
}
}

pub fn init_multi_phase(&'static self, _py: Python<'_>, _gil_used: bool) -> *mut ffi::PyObject {
pub fn init_multi_phase(&'static self) -> *mut ffi::PyObject {
// SAFETY: `ffi_def` is correctly initialized in `new()`
unsafe { ffi::PyModuleDef_Init(self.ffi_def.get()) }
}

/// Builds a module object directly. Used for [`#[pymodule]`][crate::pymodule] submodules.
#[cfg_attr(any(Py_LIMITED_API, not(Py_GIL_DISABLED)), allow(unused_variables))]
pub fn make_module(&'static self, py: Python<'_>, _gil_used: bool) -> PyResult<Py<PyModule>> {
pub fn make_module(&'static self, py: Python<'_>) -> PyResult<Py<PyModule>> {
// Check the interpreter ID has not changed, since we currently have no way to guarantee
// that static data is not reused across interpreters.
//
Expand Down Expand Up @@ -307,10 +304,7 @@ impl PyAddToModule for PyFunctionDef {
/// For adding a module to a module.
impl PyAddToModule for ModuleDef {
fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> {
module.add_submodule(
self.make_module(module.py(), self.gil_used)?
.bind(module.py()),
)
module.add_submodule(self.make_module(module.py())?.bind(module.py()))
}
}

Expand Down Expand Up @@ -347,7 +341,7 @@ mod tests {
static MODULE_DEF: ModuleDef = ModuleDef::new(c"test_module", c"some doc", &SLOTS);

Python::attach(|py| {
let module = MODULE_DEF.make_module(py, false).unwrap().into_bound(py);
let module = MODULE_DEF.make_module(py).unwrap().into_bound(py);
assert_eq!(
module
.getattr("__name__")
Expand Down
2 changes: 1 addition & 1 deletion src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ macro_rules! wrap_pymodule {
&|py| {
use $module as wrapped_pymodule;
wrapped_pymodule::_PYO3_DEF
.make_module(py, wrapped_pymodule::__PYO3_GIL_USED)
.make_module(py)
.expect("failed to wrap pymodule")
}
};
Expand Down
31 changes: 16 additions & 15 deletions src/types/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,16 @@ impl PyModule {
/// ```
pub fn new<'py>(py: Python<'py>, name: &str) -> PyResult<Bound<'py, PyModule>> {
let name = PyString::new(py, name);
unsafe {
let module = unsafe {
ffi::PyModule_NewObject(name.as_ptr())
.assume_owned_or_err(py)
.assume_owned_or_err(py)?
.cast_into_unchecked()
}
};

// By default, PyO3 assumes modules use the GIL for thread safety.
module.gil_used(false)?;

Ok(module)
}

/// Imports the Python module with the specified name.
Expand Down Expand Up @@ -367,34 +372,30 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed {

/// Declare whether or not this module supports running with the GIL disabled
///
/// If the module does not rely on the GIL for thread safety, you can pass
/// `false` to this function to indicate the module does not rely on the GIL
/// for thread-safety.
/// Since PyO3 0.28, PyO3 defaults to assuming that modules do not require the
/// GIL for thread safety. Call this function with `true` to opt-out of supporting
/// free-threaded Python.
///
/// This function sets the [`Py_MOD_GIL`
/// slot](https://docs.python.org/3/c-api/module.html#c.Py_mod_gil) on the
/// module object. The default is `Py_MOD_GIL_USED`, so passing `true` to
/// this function is a no-op unless you have already set `Py_MOD_GIL` to
/// `Py_MOD_GIL_NOT_USED` elsewhere.
/// module object.
///
/// # Examples
///
/// ```rust,no_run
/// use pyo3::prelude::*;
///
/// #[pymodule(gil_used = false)]
/// #[pymodule]
/// fn my_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
/// let submodule = PyModule::new(py, "submodule")?;
/// submodule.gil_used(false)?;
/// submodule.gil_used(true)?;
/// module.add_submodule(&submodule)?;
/// Ok(())
/// }
/// ```
///
/// The resulting module will not print a `RuntimeWarning` and re-enable the
/// GIL when Python imports it on the free-threaded build, since all module
/// objects defined in the extension have `Py_MOD_GIL` set to
/// `Py_MOD_GIL_NOT_USED`.
/// The resulting module will print a `RuntimeWarning` and re-enable the
/// GIL when Python imports it on the free-threaded build.
///
/// This is a no-op on the GIL-enabled build.
fn gil_used(&self, gil_used: bool) -> PyResult<()>;
Expand Down
4 changes: 2 additions & 2 deletions tests/test_append_to_inittab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ fn foo() -> usize {
123
}

#[pymodule(gil_used = false)]
#[pymodule]
fn module_fn_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(foo, m)?)?;
Ok(())
}

#[pymodule(gil_used = false)]
#[pymodule]
mod module_mod_with_functions {
#[pymodule_export]
use super::foo;
Expand Down
6 changes: 2 additions & 4 deletions tests/test_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fn test_module_with_functions() {
use pyo3::wrap_pymodule;

/// This module is implemented in Rust.
#[pymodule(gil_used = false)]
#[pymodule]
mod module_with_functions {
use super::*;

Expand Down Expand Up @@ -125,7 +125,7 @@ fn test_module_with_pyfn() {
use pyo3::wrap_pymodule;

/// This module is implemented in Rust.
#[pymodule(gil_used = false)]
#[pymodule]
fn module_with_pyfn(m: &Bound<'_, PyModule>) -> PyResult<()> {
#[pyfn(m)]
#[pyo3(name = "no_parameters")]
Expand Down Expand Up @@ -263,8 +263,6 @@ fn test_module_from_code_bound() {
.extract()
.expect("The value should be able to be converted to an i32");

adder_mod.gil_used(false).expect("Disabling the GIL failed");

assert_eq!(ret_value, 3);
});
}
Expand Down
Loading