Skip to content

Conversation

@eureka928
Copy link

Summary

Resolves #16495.

RUF008 previously only caught bare mutable defaults like mutable_default: list[int] = [] in dataclass attributes, but missed mutable defaults wrapped in field(default=...) calls. For example, the following was not flagged:

@define
class A:
    mutable_default: list[int] = attrs.field(default=[])

This PR modifies mutable_dataclass_default() to look inside recognized dataclass field calls (dataclasses.field(), attrs.field(), attr.ib(), attr.attrib()) and check the default keyword argument for mutability.

The approach mirrors how RUF009 already uses is_dataclass_field() to identify field calls — RUF008 now reuses the same helper to extract and inspect the default keyword argument. No duplicate diagnostics are introduced since RUF009 already skips field calls entirely.

Test Plan

cargo nextest run -p ruff_linter and cargo clippy.

Added test cases covering:

  • Positive: field(default=[]), attrs.field(default={}), attr.ib(default=[]), attr.attrib(default=set()), field(default=dict())
  • Negative: factory=list, default=(), default="hello", default=1, default=KNOWINGLY_MUTABLE_DEFAULT, ClassVar annotation with field(default=[])

RUF008 previously only caught bare mutable defaults like `[]` or `{}`,
but missed mutable defaults wrapped in `field(default=...)`,
`attrs.field(default=...)`, `attr.ib(default=...)`, and
`attr.attrib(default=...)` calls.

When the assigned value is a recognized dataclass field call, extract the
`default` keyword argument and check that for mutability instead.
Add positive cases for `field(default=[])`, `attrs.field(default={})`,
`attr.ib(default=[])`, `attr.attrib(default=set())`, and `dict()`.
Add negative cases for `factory=list`, immutable defaults, ClassVar
annotations combined with field defaults, and non-mutable variables.
@eureka928
Copy link
Author

Hi @ntBre would you review my first PR?
Thank you

@ntBre ntBre self-requested a review February 2, 2026 22:35
@ntBre ntBre added the rule Implementing or modifying a lint rule label Feb 2, 2026
@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 3, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! This is looking good.

The main thing we need to do is make this a preview change. See my inline comment for a link to other example preview functions. Other than that, I just a couple of nits!

/// If the default value is intended to be mutable, it must be annotated with
/// `typing.ClassVar`; otherwise, a `ValueError` will be raised.
///
/// This rule also detects mutable defaults passed via the `default` keyword
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to make this a preview change and also mention that here:

Suggested change
/// This rule also detects mutable defaults passed via the `default` keyword
/// In [preview](https://docs.astral.sh/ruff/preview/) this rule also detects mutable defaults passed via the `default` keyword

Comment on lines 96 to 101
} else {
Some(value.as_ref())
}
} else {
Some(value.as_ref())
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be slightly nicer as a match to avoid these dangling elses:

        let value_to_check = match &**value {
            Expr::Call(ast::ExprCall {
                func, arguments, ..
            }) if is_dataclass_field(func, checker.semantic(), dataclass_kind) => {
                arguments.find_argument_value("default", 0)
            }
            value => Some(value),
        };

I would probably just shadow value instead of using value_to_check too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this and pushed!
Would you review once more? this would be good to merge 😉

@ntBre
Copy link
Contributor

ntBre commented Feb 3, 2026

Oh, and thank you for handling dataclasses as well! I thought from the issue that this was only about attrs, but the stdlib dataclasses were affected too.

eureka928 and others added 5 commits February 3, 2026 15:50
…ult.rs

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
…ult.rs

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still has not been gated behind a preview check, and my suggestion to add comments to the test cases was for every new test case, not that single line.

Please make sure you've addressed all of the feedback before pinging us for an additional review.

# Mutable defaults wrapped in field() calls
@dataclass
class C:
mutable_default: list[int] = field(default=[]) # okay
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is not correct. This line emits a diagnostic.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you review my last commit if that looks good to you?

@ntBre ntBre added the preview Related to preview mode features label Feb 3, 2026
@ntBre ntBre changed the title [ruff] Detect mutable defaults in field(default=...) calls (RUF008) [ruff] Detect mutable defaults in field calls (RUF008) Feb 4, 2026
@ntBre ntBre enabled auto-merge (squash) February 4, 2026 15:24
@ntBre ntBre merged commit b43a204 into astral-sh:main Feb 4, 2026
39 of 40 checks passed
@eureka928
Copy link
Author

Thank you for merging this!

eureka928 added a commit to eureka928/ruff that referenced this pull request Feb 4, 2026
The field(default=...) detection from astral-sh#23046 is behind a preview gate,
so class D diagnostics should only appear in the preview snapshot.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
eureka928 added a commit to eureka928/ruff that referenced this pull request Feb 6, 2026
The field(default=...) detection from astral-sh#23046 is behind a preview gate,
so class D diagnostics should only appear in the preview snapshot.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ruff-mutable-default (RUF008) not not catching mutable default passed to attrs.field.

3 participants