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 = ¶meters[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
- Reduce to the higher-order function (ParamSpec plus a union/overloaded callback) and file a ty issue
- Retry on the latest ty build, since ParamSpec binding logic is under active development
- 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
- Report ParamSpec-forwarding repros with union/overloaded callbacks - they pinpoint the multi-element path
- Retry on the latest ty build before reporting; this area changes frequently
- Contributors: construct sub-calls from single callables or iterate all binding elements rather than assuming single_element()
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
- bindings must not be empty
- argument index should be valid
- checked bindings are stable across fixpoint iterations
- argument index should be valid
- extra use-def data should have been retained
AI-assisted analysis of astral-sh/ruff@15f3fe6b15 (2026-08-20).
Data as JSON: /api/errors/0d8347b59606cf22.
Report an issue: GitHub.