astral-sh/ruff · critical

Expected dictionary argument to be kwarg

Error message

Expected dictionary argument to be kwarg

What it means

The fix for flake8-comprehensions C408 (unnecessary `dict()`/`tuple()` call) rewrites `dict(a=1)` into `{'a': 1}`. It assumes every argument in the call was already verified to be a keyword argument during diagnostic emission; if a positional or starred argument slips into the fix stage, `arg.keyword` is None and this expect panics.

Source

Thrown at crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs:255

    // below.
    let mut arena: Vec<String> = vec![];

    let quote = checker
        .interpolated_string_quote_style()
        .unwrap_or(stylist.quote());

    // Quote each argument.
    //
    // Python normalizes identifiers to NFKC, but string literals are not normalized. Emitting the
    // raw source text of a keyword argument would change the dictionary key at runtime, so the
    // name has to be normalized. See https://github.com/astral-sh/ruff/issues/16234.
    for arg in &call.args {
        let quoted = format!(
            "{}{}{}",
            quote,
            arg.keyword
                .as_ref()
                .expect("Expected dictionary argument to be kwarg")
                .value
                .nfkc(),
            quote,
        );
        arena.push(quoted);
    }

    let elements = call
        .args
        .iter()
        .enumerate()
        .map(|(i, arg)| DictElement::Simple {
            key: Expression::SimpleString(Box::new(SimpleString {
                value: &arena[i],
                lpar: vec![],
                rpar: vec![],
            })),
            value: arg.value.clone(),

View on GitHub (pinned to 26f38c119c)

Solutions

  1. Report the file to Ruff — the fix must not be offered for calls with positional/starred args
  2. Upgrade Ruff to a version where the C408 fix/diagnostic mismatch is fixed
  3. Skip the fix for that file (`ruff check --fix --no-cache <file>` avoided; use `# noqa: C408` or exclude the rule)
  4. Manually rewrite `dict(...)` to a dict literal as a workaround

Example fix

// before
let key = arg.keyword.as_ref().expect("Expected dictionary argument to be kwarg");
// after
let Some(key) = arg.keyword.as_ref() else { return Err(anyhow!("positional arg in dict() fix")) };
Defensive patterns

Strategy: try-catch

Validate before calling

# avoid triggering the C408 fix on calls with positional/starred args
import re
if re.search(r'\bdict\((\*|[^)=,]+\s*,\s*)', src):
    print('dict() call has positional/starred args; fix may crash — rewrite manually')

Type guard

def is_safe_c408_target(call_src: str) -> bool:
    args = call_src[call_src.index('(')+1:call_src.rindex(')')].strip()
    return bool(args) and '*' not in args and all('=' in a for a in args.split(','))

Prevention

When it happens

Trigger: Applying an unsafe fix for C408 (`ruff check --fix`) on a `dict(...)` call whose args include something without a keyword — e.g. `dict(**kwargs)`-style or a diagnostic emitted without re-checking positional/starred args (an internal fix-diagnostic mismatch).

Common situations: Users running `ruff --fix` on code like `dict(x, a=1)` or `dict(*args)` where detection and fix disagree, producing a crash during fixing.

Related errors


AI-assisted analysis of astral-sh/ruff@26f38c119c (2026-09-05). Data as JSON: /api/errors/7049c5baaeeffab4. Report an issue: GitHub.