Skip to content

Conversation

@danparizher
Copy link
Contributor

Summary

Fixes #21142

B006 now flags mutable defaults passed via module-level named constants. Previously, it only flagged inline mutable defaults (e.g., {}, [], set()), missing cases where the default references a module-level constant bound to a mutable object (e.g., MY_SET = {"ABC", "DEF"}).

Problem Analysis

B006’s detection was syntax-local and didn’t resolve identifiers to check mutability. When a default used a name like MY_SET, it wasn’t resolved to the bound mutable object, causing false negatives.

Example:

MY_SET = {"ABC", "DEF"}  # module-level mutable constant

def func_A(s: set[str] = MY_SET):  # Not flagged (should be flagged)
    return s

def func_B(s: set[str] = {"ABC", "DEF"}):  # Correctly flagged
    return s

This is a false negative because func_A and func_B share the same mutable default object.

Approach

Extended is_guaranteed_mutable_expr (used in preview mode) to handle Expr::Name:

  1. Resolve the identifier using semantic.only_binding() to get the binding.
  2. Require an assignment binding (not imports, parameters, etc.).
  3. Extract the assigned value via find_binding_value().
  4. Recursively check if that value is mutable using is_guaranteed_mutable_expr().

This enables detecting cases like:

  • MY_SET = {"ABC", "DEF"} → flagged when used as a default
  • MY_LIST = [1, 2, 3] → flagged when used as a default
  • MY_DICT = {"key": "value"} → flagged when used as a default

The fix is enabled only in preview mode. Non-preview mode preserves existing behavior (only flags inline mutable defaults).

@github-actions
Copy link
Contributor

github-actions bot commented Nov 1, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+4 -0 violations, +0 -0 fixes in 4 projects; 51 projects unchanged)

apache/airflow (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

+ providers/snowflake/tests/unit/snowflake/hooks/test_snowflake_sql_api.py:209:55: B006 Do not use mutable data structures for argument defaults

mlflow/mlflow (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview

+ docs/docs/genai/flavors/dspy/notebooks/dspy_quickstart.ipynb:cell 23:1:58: B006 Do not use mutable data structures for argument defaults

pandas-dev/pandas (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview

+ pandas/io/formats/css.py:345:76: B006 Do not use mutable data structures for argument defaults

zulip/zulip (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

+ zerver/lib/migration_status.py:100:47: B006 Do not use mutable data structures for argument defaults

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
B006 4 4 0 0 0

@BlindChickens
Copy link

This brings the rule more in line with the pylint rule it is supposed to represent/replace. dangerous-default-value (W0102)
Lets get it merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

B006 doesn’t flag mutable defaults when passed via a named constant

2 participants