astral-sh/ruff · error

Expected `NamedTuple` definition r.h.s. to be a call express

Error message

Expected `NamedTuple` definition r.h.s. to be a call expression

What it means

The second assertion in `deferred_spec`: after confirming the definition is an assignment, the value must be the `NamedTuple(...)` call expression, because the code then reads `node.arguments.args[1]` (the fields list). The `as_call_expr().expect(...)` fires when the RHS is not a call.

Source

Thrown at crates/ty_python_semantic/src/types/class/named_tuple.rs:455

            })
        }
    }

    fn spec(self, db: &'db dyn Db) -> NamedTupleSpec<'db> {
        #[salsa::tracked(
            returns(copy),
            cycle_initial=|db, _, _| NamedTupleSpec::unknown(db),
            heap_size=ruff_memory_usage::heap_size
        )]
        fn deferred_spec<'db>(db: &'db dyn Db, definition: Definition<'db>) -> NamedTupleSpec<'db> {
            let python_file = definition.python_file(db);
            let module = parsed_module(db, python_file).load(db);
            let node = definition
                .kind(db)
                .value(&module)
                .expect("Expected `NamedTuple` definition to be an assignment")
                .as_call_expr()
                .expect("Expected `NamedTuple` definition r.h.s. to be a call expression");
            match definition_expression_type(db, definition, &node.arguments.args[1]) {
                Type::KnownInstance(KnownInstanceType::NamedTupleSpec(spec)) => spec,
                _ => NamedTupleSpec::unknown(db),
            }
        }

        match self.anchor(db) {
            DynamicNamedTupleAnchor::CollectionsDefinition { spec, .. }
            | DynamicNamedTupleAnchor::ScopeOffset { spec, .. } => *spec,
            DynamicNamedTupleAnchor::TypingDefinition(definition) => deferred_spec(db, *definition),
        }
    }

    fn fields(self, db: &'db dyn Db) -> &'db [NamedTupleField<'db>] {
        self.spec(db).fields(db)
    }

    /// Returns the field declared directly on this dynamic named tuple, if any.

View on GitHub (pinned to d1087a4b9e)

Solutions

  1. Anchor only after the RHS matches a `NamedTuple`/`collections.namedtuple` call with the expected arity.
  2. Use `as_call_expr()` with a let-else returning `NamedTupleSpec::unknown(db)` so malformed anchors degrade instead of panicking.
  3. Add mdtests covering `P = NamedTuple`, calls missing the fields argument, and keyword forms.
  4. Report upstream if reachable without source changes.

Example fix

// before
.as_call_expr()
.expect("Expected `NamedTuple` definition r.h.s. to be a call expression");

// after
let Some(call) = value.as_call_expr() else {
    return NamedTupleSpec::unknown(db);
};
Defensive patterns

Strategy: try-catch

Try / catch

let spec = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    named_tuple.spec(db)
}))
.unwrap_or_else(|_| NamedTupleSpec::unknown(db));

Prevention

When it happens

Trigger: Resolving a typing-module NamedTuple spec where the anchored definition's value is not an `ast::ExprCall` - e.g. `P = NamedTuple` (bare alias) or a rebinding got anchored by mistake. Note the same code also indexes `args[1]`, so zero/one-argument calls are a neighboring crash risk.

Common situations: Broadening NamedTuple detection to bare aliases or star-call forms; refactors that anchor definitions before validating the call shape; stale anchors after edits.

Related errors


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