Skip to content

Commit 6de60bc

Browse files
committed
Add PyO3 class mutability guidelines reference to contributor guide
1 parent 799e8fb commit 6de60bc

File tree

2 files changed

+65
-0
lines changed

2 files changed

+65
-0
lines changed

docs/source/contributor-guide/ffi.rst

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
140201
If you were interfacing with a library that provided the above ``FFI_TableProvider`` and
141202
you needed to turn it back into an ``TableProvider``, you can turn it into a
142203
``ForeignTableProvider`` with implements the ``TableProvider`` trait.

docs/source/contributor-guide/introduction.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ We welcome and encourage contributions of all kinds, such as:
2626
In addition to submitting new PRs, we have a healthy tradition of community members reviewing each other’s PRs.
2727
Doing so is a great way to help the community as well as get more familiar with Rust and the relevant codebases.
2828

29+
Before opening a pull request that touches PyO3 bindings, please review the
30+
:ref:`PyO3 class mutability guidelines <ffi_pyclass_mutability>` so you can flag missing
31+
``#[pyclass(frozen)]`` annotations during development and review.
32+
2933
How to develop
3034
--------------
3135

0 commit comments

Comments
 (0)