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.