PyO3/pyo3 · error
should always have positional defaults <= positional paramet
Error message
should always have positional defaults <= positional parameters
What it means
In pyo3's function signature builder, required_positional_parameters computes positional_parameters.len() - default_positional_parameters.len() and expects it not to underflow. The invariant is that every default must correspond to a declared positional parameter, so defaults can never outnumber parameters. Violating it means the SignatureBuilder was mutated inconsistently (e.g. a default registered without a parameter).
Source
Thrown at pyo3-macros-backend/src/pyfunction/signature.rs:297
pub varargs: Option<String>,
// Tuples of keyword name and optional default value
pub keyword_only_parameters: Vec<(String, Option<Expr>)>,
pub kwargs: Option<String>,
}
impl PythonSignature {
pub fn has_no_args(&self) -> bool {
self.positional_parameters.is_empty()
&& self.keyword_only_parameters.is_empty()
&& self.varargs.is_none()
&& self.kwargs.is_none()
}
pub fn required_positional_parameters(&self) -> usize {
self.positional_parameters
.len()
.checked_sub(self.default_positional_parameters.len())
.expect("should always have positional defaults <= positional parameters")
}
/// Makes every positional parameter positional-only, exactly as a trailing `/` in a
/// signature does. Deliberately leaves keyword-only parameters alone.
pub fn make_all_parameters_positional_only(&mut self) {
self.positional_only_parameters = self.positional_parameters.len();
}
}
#[derive(Clone)]
pub struct FunctionSignature<'a> {
pub arguments: Vec<FnArg<'a>>,
pub python_signature: PythonSignature,
pub attribute: Option<SignatureAttribute>,
}
pub enum ParseState {
/// Accepting positional parameters, which might be positional onlyView on GitHub (pinned to ac9b6899d3)
Solutions
- Only push a default after (or together with) pushing the corresponding positional parameter.
- Audit any code that modifies positional_parameters / default_positional_parameters so lengths satisfy defaults <= parameters.
- Report to pyo3 if a plain #[pyfunction] with defaults triggers it.
Example fix
// before defaults.push(default_ty); // without params.push(...) // after params.push(param); defaults.push(default_ty);
Defensive patterns
Strategy: validation
Validate before calling
// Maintain the invariant when mutating SignatureBuilder debug_assert!(defaults.len() <= params.len(), "defaults exceed positional parameters");
Type guard
fn signature_consistent(positional: usize, defaults: usize) -> bool {
defaults <= positional
} Prevention
- Push a positional default only alongside its parameter.
- Never mutate positional_parameters/default_positional_parameters from outside the builder's methods.
- Add round-trip tests for #[pyfunction] signatures with defaults, *, /, and keyword-only args.
When it happens
Trigger: Internal state corruption while parsing a #[pyfunction] signature: e.g. a `= default` annotation or signature attribute registering a positional default with no matching positional parameter, then querying required_positional_parameters.
Common situations: Custom forks or third-party macro plugins that mutate SignatureBuilder directly; upstream parsing keeps defaults and parameters in lockstep.
Related errors
- Named fields should have identifiers
- no class given for Fn with a "self" receiver
- no class given for a class method
- complex enum has a non-unit variant
- named field has an identifier
AI-assisted analysis of PyO3/pyo3@ac9b6899d3 (2026-09-05).
Data as JSON: /api/errors/6a8c5f8021a2fe2d.
Report an issue: GitHub.