@@ -137,6 +137,67 @@ and you want to create a sharable FFI counterpart, you could write:
137137 let my_provider = MyTableProvider::default();
138138 let ffi_provider = FFI_TableProvider::new(Arc::new(my_provider), false, None);
139139
140+ .. _ffi_pyclass_mutability :
141+
142+ PyO3 class mutability guidelines
143+ --------------------------------
144+
145+ PyO3 bindings should present immutable wrappers whenever a struct stores shared or
146+ interior-mutable state. In practice this means that any ``#[pyclass] `` containing an
147+ ``Arc<RwLock<_>> `` or similar synchronized primitive must opt into ``#[pyclass(frozen)] ``
148+ unless there is a compelling reason not to.
149+
150+ The :mod: `datafusion ` configuration helpers illustrate the preferred pattern. The
151+ ``PyConfig `` class in :file: `src/config.rs ` stores an ``Arc<RwLock<ConfigOptions>> `` and is
152+ explicitly frozen so callers interact with configuration state through provided methods
153+ instead of mutating the container directly:
154+
155+ .. code-block :: rust
156+
157+ #[pyclass(name = "Config", module = "datafusion", subclass, frozen)]
158+ #[derive(Clone)]
159+ pub(crate) struct PyConfig {
160+ config: Arc<RwLock<ConfigOptions>>,
161+ }
162+
163+ The same approach applies to execution contexts. ``PySessionContext `` in
164+ :file: `src/context.rs ` stays frozen even though it shares mutable state internally via
165+ ``SessionContext ``. This ensures PyO3 tracks borrows correctly while Python-facing APIs
166+ clone the inner ``SessionContext `` or return new wrappers instead of mutating the
167+ existing instance in place:
168+
169+ .. code-block :: rust
170+
171+ #[pyclass(frozen, name = "SessionContext", module = "datafusion", subclass)]
172+ #[derive(Clone)]
173+ pub struct PySessionContext {
174+ pub ctx: SessionContext,
175+ }
176+
177+ Occasionally a type must remain mutable—for example when PyO3 attribute setters need to
178+ update fields directly. In these rare cases add an inline justification so reviewers and
179+ future contributors understand why ``frozen `` is unsafe to enable. ``DataTypeMap `` in
180+ :file: `src/common/data_type.rs ` includes such a comment because PyO3 still needs to track
181+ field updates:
182+
183+ .. code-block :: rust
184+
185+ // TODO: This looks like this needs pyo3 tracking so leaving unfrozen for now
186+ #[derive(Debug, Clone)]
187+ #[pyclass(name = "DataTypeMap", module = "datafusion.common", subclass)]
188+ pub struct DataTypeMap {
189+ #[pyo3(get, set)]
190+ pub arrow_type: PyDataType,
191+ #[pyo3(get, set)]
192+ pub python_type: PythonType,
193+ #[pyo3(get, set)]
194+ pub sql_type: SqlType,
195+ }
196+
197+ When reviewers encounter a mutable ``#[pyclass] `` without a comment, they should request
198+ an explanation or ask that ``frozen `` be added. Keeping these wrappers frozen by default
199+ helps avoid subtle bugs stemming from PyO3's interior mutability tracking.
200+
140201If you were interfacing with a library that provided the above ``FFI_TableProvider `` and
141202you needed to turn it back into an ``TableProvider ``, you can turn it into a
142203``ForeignTableProvider `` with implements the ``TableProvider `` trait.
0 commit comments