PyO3/pyo3 · critical

Cannot clone pointer into Python heap without the thread…

Error message

Cannot clone pointer into Python heap without the thread being attached.

What it means

Panic from Clone for a pointer type wrapping a Python heap object. Cloning bumps the CPython reference count via Py_INCREF, which is only legal while the calling thread is attached to the Python interpreter (holding the GIL or, on free-threaded builds, being attached). If the thread is not attached, incrementing the refcount would be unsynchronized/invalid, so the code aborts instead of corrupting the refcount.

Solutions

  1. Wrap the clone in Python::with_gil(|py| ...) (or Python::attach) so the thread is attached first
  2. Don't share Py<T> across unattached threads; send via channels and clone inside with_gil
  3. Avoid touching Python objects during interpreter shutdown (use before-thread/atexit hooks to drop them earlier)

Example fix

// before (foreign thread)
let cloned = py_obj.clone(); // panics: not attached
// after
let cloned = Python::with_gil(|_py| py_obj.clone());
Defensive patterns

Strategy: type-guard

Validate before calling

fn can_touch_python() -> bool { Python::with_gil(|_| true) } // only call on threads you attach yourself

Type guard

fn is_attached() -> bool { Python::with_gil(|_py| /* reached means attached */ true) }

Try / catch

// guard foreign-thread access
std::panic::catch_unwind(|| Python::with_gil(|_py| py_obj.clone())).is_err()

Prevention

When it happens

Trigger: Calling .clone() on a Py<T>/Borrowed pointer from a thread that is not attached to Python (e.g. outside Python::with_gil, on a foreign thread without Python::attach/with_gil), or during interpreter finalization when attachment is refused.

Common situations: Rust threads spawned manually touching Py<T> clones without Python::with_gil; callbacks from non-Python threads; cloning during interpreter shutdown.

Related errors


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

Appendix: source

Thrown at src/instance.rs:2266

    #[inline]
    fn clone(&self) -> Self {
        #[track_caller]
        #[inline]
        fn try_incref(obj: NonNull<ffi::PyObject>) {
            use crate::internal::state::thread_is_attached;

            if thread_is_attached() {
                // SAFETY: Py_INCREF is safe to call on a valid Python object if the thread is attached.
                unsafe { ffi::Py_INCREF(obj.as_ptr()) }
            } else {
                incref_failed()
            }
        }

        #[cold]
        #[track_caller]
        fn incref_failed() -> ! {
            panic!("Cannot clone pointer into Python heap without the thread being attached.");
        }

        try_incref(self.0);

        Self(self.0, PhantomData)
    }
}

/// Dropping a `Py` instance decrements the reference count
/// on the object by one if the thread is attached to the Python interpreter.
///
/// Otherwise and by default, this registers the underlying pointer to have its reference count
/// decremented the next time PyO3 attaches to the Python interpreter.
///
/// However, if the `pyo3_disable_reference_pool` conditional compilation flag
/// is enabled, it will abort the process.
impl<T> Drop for Py<T> {
    #[inline]

View on GitHub (pinned to ac9b6899d3)