PyO3/pyo3 · error

this object is already borrowed

Error message

this object is already borrowed

What it means

`PyClassGuard::into_super` converts a guard of a subclass `T` into a guard of its pyo3 base class `U`. To keep borrow counts consistent it first adds a shared borrow on the base (`try_borrow`, panicking with 'this object is already borrowed' if the base is mutably borrowed) and then releases the borrow of `T`. The panic means the base-class object's borrow state conflicts with the conversion.

Source

Thrown at src/pycell.rs:393

        let t_not_frozen = !<T::Frozen as crate::pyclass::boolean_struct::private::Boolean>::VALUE;
        let u_frozen = <<T::BaseType as PyClass>::Frozen as crate::pyclass::boolean_struct::private::Boolean>::VALUE;
        if t_not_frozen && u_frozen {
            // If `T` is mutable subclass of `U` differ, then it is possible that we need to
            // release the borrow count now. (e.g. `U` may have a noop borrow checker so
            // dropping the `PyRef<U>` later would noop and leak the borrow we currently hold.)
            //
            // However it's nontrivial, if `U` itself has a mutable base class `V`,
            // then the borrow checker of both `T` and `U` is the shared borrow checker of `V`.
            //
            // But it's really hard to prove that in the type system, the soundest thing we
            // can do is just add a borrow to `U` now and then release the borrow of `T`.

            self.inner
                .as_super()
                .get_class_object()
                .borrow_checker()
                .try_borrow()
                .expect("this object is already borrowed");

            self.inner
                .get_class_object()
                .borrow_checker()
                .release_borrow()
        };
        PyRef {
            inner: unsafe {
                ManuallyDrop::new(self)
                    .as_ptr()
                    .assume_owned_unchecked(py)
                    .cast_into_unchecked()
            },
        }
    }

    /// Borrows a shared reference to `PyRef<T::BaseType>`.
    ///

View on GitHub (pinned to ac9b6899d3)

Solutions

  1. Ensure no `PyRefMut` of the base class is alive when calling `into_super`
  2. Convert guards to the base only from an immutable (`PyRef`) state
  3. Use `try_borrow`-based explicit checks before converting
  4. Restructure to avoid calling `into_super` while a base method is executing

Example fix

// before
fn method(mut this: PyRefMut<'_, Self>) -> PyResult<()> {
    let base = this.into_super(); // base already mutably borrowed
    base.base_method()
}
// after
fn method(this: PyRef<'_, Self>) -> PyResult<()> {
    let base = this.into_super();
    base.base_method()
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Rust: ensure base is not mutably borrowed before into_super
// (call into_super only from PyRef, not PyRefMut)

Type guard

fn can_take_super<'py, T: PyClass>(obj: &Bound<'py, T>) -> bool {
    obj.try_borrow().is_ok()
}

Prevention

When it happens

Trigger: Calling `into_super()` while the base class portion is mutably borrowed (`PyRefMut` of the base alive); overlapping guards for base and derived instances of the same underlying object; nested `into_super` calls inside methods that already hold mutable borrows.

Common situations: Multi-level `#[pyclass(extends=...)]` hierarchies with methods on both base and subclass calling each other; Python subclass method calling `super()`-like Rust conversions while the base is mutably accessed.

Related errors


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