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
- Construct errors only via safe APIs: `PyErr::new`, `PyErr::from_value`, `PyErr::from_type`
- Do not restore a `PyErr` whose state was already consumed by normalization
- Check for mixed pyo3 versions among extension modules (cargo tree)
- 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
- Construct PyErr only via PyErr::new/from_value/from_type
- Never restore a PyErr twice
- Audit for mixed pyo3 versions with cargo tree
- Report panics from safe-API-only usage upstream
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
- Neither abi3 or abi3t features are enabled
- Cannot target an abi3t version below {MINIMUM_SUPPORTED_VERS
- failed to run the Python interpreter at {}: {}
- Python script failed
- cannot set a minimum Python version {} higher than the inter
AI-assisted analysis of PyO3/pyo3@ac9b6899d3 (2026-09-05).
Data as JSON: /api/errors/6681f02ba6b0effe.
Report an issue: GitHub.