PyO3/pyo3 · critical

PyErr state should never be invalid outside of normalization

Error message

PyErr state should never be invalid outside of normalization

What it means

`PyErrState::restore` unwraps the internal state, asserting that a `PyErr`'s state is always valid (normalized or lazily normalizable) outside of normalization itself. Panicking here means a `PyErr` with an invalid/empty state was restored into the Python interpreter. In practice this indicates a pyo3-internal invariant violation rather than a bug in user code.

Source

Thrown at src/err/err_state.rs:65

                pvalue: args.arguments(py),
            }
        })))
    }

    pub(crate) fn normalized(normalized: PyErrStateNormalized) -> Self {
        let state = Self::from_inner(PyErrStateInner::Normalized(normalized));
        // This state is already normalized, by completing the Once immediately we avoid
        // reaching the `py.detach` in `make_normalized` which is less efficient
        // and introduces a GIL switch which could deadlock.
        // See https://github.com/PyO3/pyo3/issues/4764
        state.normalized.call_once(|| {});
        state
    }

    pub(crate) fn restore(self, py: Python<'_>) {
        self.inner
            .into_inner()
            .expect("PyErr state should never be invalid outside of normalization")
            .restore(py)
    }

    fn from_inner(inner: PyErrStateInner) -> Self {
        Self {
            normalized: Once::new(),
            normalizing_thread: Cell::new(None),
            inner: UnsafeCell::new(Some(inner)),
        }
    }

    #[inline]
    pub(crate) fn as_normalized(&self, py: Python<'_>) -> &PyErrStateNormalized {
        if self.normalized.is_completed() {
            match unsafe {
                // Safety: self.inner will never be written again once normalized.
                &*self.inner.get()
            } {

View on GitHub (pinned to ac9b6899d3)

Solutions

  1. Construct errors only via safe APIs: `PyErr::new`, `PyErr::from_value`, `PyErr::from_type`
  2. Do not restore a `PyErr` whose state was already consumed by normalization
  3. Check for mixed pyo3 versions among extension modules (cargo tree)
  4. Upgrade pyo3 and report the issue if it reproduces with only safe API usage

Example fix

// before
let err: PyErr = unsafe { /* built from invalid state */ }; err.restore(py)
// after
let err = PyErr::new::<exceptions::PyValueError, _>("message"); err.restore(py)
Defensive patterns

Strategy: validation

Validate before calling

# Rust side: only build PyErr through safe constructors
let err = PyErr::new::<PyValueError, _>("msg");

Type guard

fn has_valid_state(err: &PyErr) -> bool {
    // safe API: PyErr is always validly constructed
    !err.value(py).is_none()
}

Try / catch

// Rust
match result {
    Ok(v) => v,
    Err(e) => { e.restore(py); return Err(PySystemError::new_err("state error")); }
}

Prevention

When it happens

Trigger: Manually constructing a `PyErr` with an invalid state and restoring it; calling `PyErr::restore` (or `PyErr::take`-style flows) with a state already consumed during normalization; unsafe FFI misuse of the PyErr API.

Common situations: Custom pyo3 extensions that build `PyErr` values from raw C-API results; pyo3 version mismatches where `PyErr` internals differ; use of deprecated/unsafe PyErr construction helpers.

Related errors


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