PyO3/pyo3 · error

Cannot attach to the Python interpreter while it is finalizi

Error message

Cannot attach to the Python interpreter while it is finalizing.

What it means

This panic fires from Python::attach()/AttachGuard::attach when the interpreter has begun finalizing (Python 3.13+, via AttachError::Finalizing). Once CPython is shutting down, attaching a thread to obtain the GIL is unsafe and undefined, so PyO3 refuses with a panic instead of blocking forever or crashing in C code.

Source

Thrown at src/internal/state.rs:81

    ///
    /// If the thread was already attached via PyO3, this returns
    /// `AttachGuard::Assumed`. Otherwise, the thread will attach now and
    /// `AttachGuard::Ensured` will be returned.
    pub(crate) fn attach() -> Self {
        match Self::try_attach() {
            Ok(guard) => guard,
            Err(AttachError::ForbiddenDuringTraverse) => {
                panic!("{}", ForbidAttaching::FORBIDDEN_DURING_TRAVERSE)
            }
            Err(AttachError::NotInitialized) => {
                // try to initialize the interpreter and try again
                crate::interpreter_lifecycle::ensure_initialized();
                // SAFETY: just initialized the interpreter
                unsafe { Self::do_attach_unchecked() }
            }
            #[cfg(Py_3_13)]
            Err(AttachError::Finalizing) => {
                panic!("Cannot attach to the Python interpreter while it is finalizing.");
            }
        }
    }

    /// Variant of the above which will will return gracefully if the interpreter cannot be attached to.
    pub(crate) fn try_attach() -> Result<Self, AttachError> {
        match ATTACH_COUNT.try_with(|c| c.get()) {
            Ok(i) if i > 0 => {
                // SAFETY: We just checked that the thread is already attached.
                return Ok(unsafe { Self::assume() });
            }
            // Cannot attach during GC traversal.
            Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => {
                return Err(AttachError::ForbiddenDuringTraverse)
            }
            // other cases handled below
            _ => {}
        }

View on GitHub (pinned to ac9b6899d3)

Solutions

  1. Ensure worker threads are joined/stopped before Py_Finalize or process exit
  2. Use Python::with_embedded_python_interpreter/teardown utilities that shut threads down first
  3. Check the interpreter isn't finalizing with Python::version/pool guards — use try_attach-based APIs (Python::try_attach) and skip work on error
  4. In 3.13+ embeddings, call Py_Finalize only after all PyO3-using threads have terminated

Example fix

// before
std::thread::spawn(|| Python::with_gil(|py| run(py))); // may run during finalize
// after
let handle = std::thread::spawn(|| Python::with_gil(|py| run(py)));
handle.join().expect("worker");
// then finalize the interpreter
Defensive patterns

Strategy: validation

Validate before calling

// PyO3 0.23+: check via pool/attach result before work
if pyo3::Python::try_attach().is_none() { return; } // skip if not attachable

Type guard

fn interpreter_usable() -> bool { pyo3::Python::try_attach().is_some() }

Try / catch

std::panic::catch_unwind(|| pyo3::Python::with_gil(|py| work(py))).unwrap_or_default();

Prevention

When it happens

Trigger: Calling Python::with_gil / Python::attach during interpreter shutdown: destructors/drop handlers running after Py_Finalize, daemon threads still executing Python calls, atexit or __del__ code that re-enters PyO3 late in teardown.

Common situations: Background worker threads (rayon/tokio/std threads) still alive when the embedding application or interpreter calls Py_Finalize; objects dropped in process exit paths; long-lived caches flushed after finalization started.

Related errors


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