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 only

View on GitHub (pinned to ac9b6899d3)

Solutions

  1. Only push a default after (or together with) pushing the corresponding positional parameter.
  2. Audit any code that modifies positional_parameters / default_positional_parameters so lengths satisfy defaults <= parameters.
  3. 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

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


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