PyO3/pyo3 · critical

Cannot drop pointer into Python heap without the thread bein

Error message

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

What it means

register_decref panics with this message and then aborts the process (PanicTrap) when reference-count dropping is attempted without a thread attached, under the pyo3_disable_reference_pool configuration (with pyo3_leak_on_drop_without_reference_pool off). PyO3 cannot safely decref a Python object from an unattached thread, so it deliberately aborts rather than corrupt refcounts.

Source

Thrown at src/internal/state.rs:364

        ATTACH_COUNT.with(|c| c.set(self.count));
    }
}

/// Registers a Python object pointer inside the release pool, to have its reference count decreased
/// the next time the thread is attached in pyo3.
#[inline]
pub fn register_decref(obj: Py<PyAny>) {
    #[cfg(not(pyo3_disable_reference_pool))]
    {
        POOL.register_decref(obj);
    }
    #[cfg(all(
        pyo3_disable_reference_pool,
        not(pyo3_leak_on_drop_without_reference_pool)
    ))]
    {
        let _trap = PanicTrap::new("Aborting the process to avoid panic-from-drop.");
        panic!("Cannot drop pointer into Python heap without the thread being attached.");
    }
}

/// Private helper function to check if we are currently in a GC traversal (as detected by PyO3).
#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
pub(crate) fn is_in_gc_traversal() -> bool {
    ATTACH_COUNT
        .try_with(|c| c.get() == ATTACH_FORBIDDEN_DURING_TRAVERSE)
        .unwrap_or(false)
}

/// Increments pyo3's internal attach count - to be called whenever an AttachGuard is created.
#[inline(always)]
fn increment_attach_count() {
    // Ignores the error in case this function called from `atexit`.
    let _ = ATTACH_COUNT.try_with(|c| {
        let current = c.get();
        if current < 0 {

View on GitHub (pinned to ac9b6899d3)

Solutions

  1. Re-enable PyO3's reference pool (remove pyo3_disable_reference_pool from cargo features)
  2. Enable pyo3_leak_on_drop_without_reference_pool if leaking instead of aborting is acceptable
  3. Wrap drops in Python::with_gil(|py| { drop(obj) }) or Python::detach boundaries so decref happens on an attached thread
  4. Avoid moving Py<T> values to threads that drop them unattached; send owned handles back to a GIL-holding thread

Example fix

// before
let obj: Py<MyType> = ...;
std::thread::spawn(move || drop(obj)); // unattached drop: aborts
// after
let obj: Py<MyType> = ...;
std::thread::spawn(move || Python::with_gil(|_py| drop(obj)));
Defensive patterns

Strategy: validation

Validate before calling

// before dropping a Py<T> on this thread, ensure attachment
fn drop_attached<T: pyo3::IntoPyObjectExt + 'static>(obj: pyo3::Py<T>) {
    pyo3::Python::with_gil(|_py| drop(obj));
}

Type guard

fn can_drop_py_value() -> bool { pyo3::Python::try_attach().is_some() || cfg!(not(feature = "pyo3_disable_reference_pool")) }

Prevention

When it happens

Trigger: Dropping a PyObject/GIL-independent wrapper (e.g. Py<T>) on a thread that never called Python::with_gil, when compiled with pyo3_disable_reference_pool and not pyo3_leak_on_drop_without_reference_pool.

Common situations: Sending Py<T> handles across threads and dropping them in workers; dropping cached objects at thread teardown; custom builds that disabled the reference pool for performance.

Related errors


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