PyO3/pyo3 · error

Already mutably borrowed

Error message

Already mutably borrowed

What it means

PyO3's borrow checker for `#[pyclass]` objects enforces Rust-style runtime borrowing: an object can be immutably (shared) borrowed many times or mutably borrowed once. `PyRef::borrow` (immutable borrow) panics with 'Already mutably borrowed' when the object is currently held as a `PyRefMut`. This replaces an `Err(PyBorrowError)` with a panic at the call site (`#[track_caller]`).

Source

Thrown at src/pycell.rs:311

    #[inline]
    pub fn as_ptr(&self) -> *mut ffi::PyObject {
        self.inner.as_ptr()
    }

    /// Returns an owned raw FFI pointer represented by self.
    ///
    /// # Safety
    ///
    /// The reference is owned; when finished the caller should either transfer ownership
    /// of the pointer or decrease the reference count (e.g. with [`pyo3::ffi::Py_DecRef`](crate::ffi::Py_DecRef)).
    #[inline]
    pub fn into_ptr(self) -> *mut ffi::PyObject {
        self.inner.clone().into_ptr()
    }

    #[track_caller]
    pub(crate) fn borrow(obj: &Bound<'py, T>) -> Self {
        Self::try_borrow(obj).expect("Already mutably borrowed")
    }

    pub(crate) fn try_borrow(obj: &Bound<'py, T>) -> Result<Self, PyBorrowError> {
        let cell = obj.get_class_object();
        cell.ensure_threadsafe();
        cell.borrow_checker()
            .try_borrow()
            .map(|_| Self { inner: obj.clone() })
    }
}

impl<'p, T> PyRef<'p, T>
where
    T: PyClass,
    T::BaseType: PyClass,
{
    /// Gets a `PyRef<T::BaseType>`.
    ///

View on GitHub (pinned to ac9b6899d3)

Solutions

  1. Shorten the `PyRefMut` scope: drop the mutable borrow before invoking Python code that may re-enter the object
  2. Use `try_borrow`/`try_borrow_mut` and handle the `Err` instead of panicking
  3. Wrap re-entrant work in `py.allow_threads(|| ...)` after extracting needed data, releasing the borrow first
  4. Restructure the class to split interior state into a `Py<RefCell<T>>`/lock so re-entrancy is handled gracefully

Example fix

// before
fn do_work(&self, py: Python<'_>) {
    let mut this = self.into_ref_mut(py);
    this.callback.call0()?; // re-enters object -> panic
}
// after
fn do_work(&self, py: Python<'_>) -> PyResult<()> {
    let data = self.into_ref(py).data.clone(); // use immutable borrow / clone out
    let cb = self.callback.clone();
    py.allow_threads(move || cb.call0())?;
    Ok(())
}
Defensive patterns

Strategy: try-catch

Validate before calling

# Rust: check borrow state before re-entering
obj.try_borrow(py).map_err(|_| PyRuntimeError::new_err('object busy'))?;

Try / catch

// Rust
let borrowed = match obj.try_borrow(py) {
    Ok(r) => r,
    Err(_) => return Err(PyRuntimeError::new_err('already mutably borrowed')),
};

Prevention

When it happens

Trigger: Calling a Python method on a `#[pyclass]` object while another method already holds `&mut self` (PyRefMut) — e.g. a method that calls back into Python which re-enters the same object; holding `PyRefMut` across a `py.allow_threads` boundary is safe, but re-entering while holding it is not; storing a `PyRef`/`PyRefMut` and calling `borrow` again on the same object nested.

Common situations: Recursive callback patterns where a Rust method invokes Python code that touches the same object; iterator methods borrowing while `__next__` also borrows mutably; accidental long-lived `PyRefMut` stored in a struct field.

Related errors


AI-assisted analysis of PyO3/pyo3@ac9b6899d3 (2026-09-05). Data as JSON: /api/errors/fe7a96137cc232dc. Report an issue: GitHub.