PyO3 binding guidelines#

Conventions for the #[pyclass] bindings in crates/. These are review policy for this repository, not part of the extension protocol — an extension library is free to shape its own classes differently.

Class mutability#

PyO3 bindings should present immutable wrappers whenever a struct stores shared or interior-mutable state. In practice this means that any #[pyclass] containing an Arc<RwLock<_>> or similar synchronized primitive must opt into #[pyclass(frozen)] unless there is a compelling reason not to.

The execution context illustrates the preferred pattern. PySessionContext in src/context.rs stays frozen even though it shares mutable state internally via SessionContext. This ensures PyO3 tracks borrows correctly while Python-facing APIs clone the inner SessionContext or return new wrappers instead of mutating the existing instance in place:

#[pyclass(from_py_object, frozen, name = "SessionContext", module = "datafusion", subclass)]
#[derive(Clone)]
pub struct PySessionContext {
    pub ctx: SessionContext,
}

Occasionally a type must remain mutable—for example when PyO3 attribute setters need to update fields directly. In these rare cases add an inline justification so reviewers and future contributors understand why frozen is unsafe to enable. DataTypeMap in src/common/data_type.rs includes such a comment because PyO3 still needs to track field updates:

// TODO: This looks like this needs pyo3 tracking so leaving unfrozen for now
#[derive(Debug, Clone)]
#[pyclass(from_py_object, name = "DataTypeMap", module = "datafusion.common", subclass)]
pub struct DataTypeMap {
    #[pyo3(get, set)]
    pub arrow_type: PyDataType,
    #[pyo3(get, set)]
    pub python_type: PythonType,
    #[pyo3(get, set)]
    pub sql_type: SqlType,
}

When reviewers encounter a mutable #[pyclass] without a comment, they should request an explanation or ask that frozen be added. Keeping these wrappers frozen by default helps avoid subtle bugs stemming from PyO3’s interior mutability tracking.