astral-sh/ruff · error

ParamSpec sub-call should only contain a single CallableBind

Error message

ParamSpec sub-call should only contain a single CallableBinding

What it means

For ParamSpec forwarding (e.g., `def f(cb: Callable[P, R]) -> R: return cb(...)`), inference performs the sub-call of the forwarded callable and, per the SAFETY comment, assumes the resulting Bindings contain exactly one CallableBinding because the sub-call was constructed from a single callable. The expect on single_element() fires when that assumption breaks and the sub-call yields multiple binding elements.

Source

Thrown at crates/ty_python_semantic/src/types/call/bind.rs:6794

            }
            self.parameter_ty_builders[parameter_index] = Some(builder);
        } else {
            self.parameter_tys[parameter_index] = Some(argument_type);
        }
    }

    fn is_gradual_variadic_parameter(parameters: &Parameters<'db>, parameter_index: usize) -> bool {
        let parameter = &parameters[parameter_index];

        matches!(parameters.kind(), ParametersKind::Gradual)
            && matches!(parameter.annotated_type(), Type::Dynamic(_))
            && (parameter.is_variadic() || parameter.is_keyword_variadic())
    }

    // TODO: Remove this workaround once call binding can infer a `TypeVarTuple` from `*args` and
    // callable argument checking preserves correlations across overloads.
    fn should_defer_typevartuple_callable_check(
        &self,
        declared_type: Type<'db>,
        expected_type: Type<'db>,
        argument_type: Type<'db>,
    ) -> bool {
        let db = self.db;
        let Some(declared_callables) = declared_type.try_upcast_to_callable(db, self.env) else {
            return false;
        };
        let parameters_contain_typevartuple = declared_callables.iter().any(|callable| {
            callable.signatures(db).iter().any(|signature| {
                signature.parameters().iter().any(|parameter| {
                    any_over_type(db, self.env, parameter.annotated_type(), false, |ty| {
                        matches!(
                            ty,
                            Type::TypeVar(typevar) if typevar.is_typevartuple(db)
                        )
                    })
                })

View on GitHub (pinned to 15f3fe6b15)

Solutions

  1. Reduce to the higher-order function (ParamSpec plus a union/overloaded callback) and file a ty issue
  2. Retry on the latest ty build, since ParamSpec binding logic is under active development
  3. As a contributor: either iterate all elements (extend_errors per element) or construct the sub-call from a single callable so it cannot union
Defensive patterns

Strategy: fallback

Try / catch

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| check_file(&db, file)));
match result {
    Ok(diags) => diags,
    Err(payload) => { log::warn!("ty panicked on ParamSpec sub-call in {}: {:?}", file, payload); vec![] }
}

Prevention

When it happens

Trigger: A ParamSpec sub-call where the rebound callable expands to a union during binding - e.g., the callback is or becomes a union of callables/overloads, so infer_call returns merged multi-element Bindings instead of one.

Common situations: Higher-order functions whose callback argument is a union of overloaded callables; churn in union-call binding merging recently touched this area, so nightly regressions are plausible.

Related errors


AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-08-20). Data as JSON: /api/errors/0d8347b59606cf22. Report an issue: GitHub.