Skip to content

[perflint] Optimize extend suggestions and fix identity false negatives (PERF401)#23008

Open
danparizher wants to merge 1 commit intoastral-sh:mainfrom
danparizher:fix-21891
Open

[perflint] Optimize extend suggestions and fix identity false negatives (PERF401)#23008
danparizher wants to merge 1 commit intoastral-sh:mainfrom
danparizher:fix-21891

Conversation

@danparizher
Copy link
Copy Markdown
Contributor

Summary

Modifies PERF401 (manual-list-comprehension) to:

  1. Allow extend suggestions when the loop is an identity transformation (e.g., for x in y: ret.append(x) -> ret.extend(y)). This was previously a false negative.
  2. Disable extend suggestions when the loop involves a transformation or filter (e.g., for x in y: ret.append(str(x))), as the resulting extend(generator) is often slower than the loop. This was a performance regression.

Fixes #21891.

Problem

PERF401 previously ignored append calls where the argument was identical to the loop target (leaving them to PERF402, which only suggests list() creation, not extend on existing lists).

Conversely, PERF401 suggested replacing append loops with extend(generator) even when the generator overhead made it slower than the original loop.

Approach

  • Modified manual_list_comprehension to allow ComprehensionType::Extend only for identity transformations without filters.
  • Updated convert_to_list_extend to generate extend(iterable) instead of extend(x for x in iterable) for identity cases.
  • Updated logic to ignore ComprehensionType::ListComprehension for identity cases (deferring to PERF402).

Test Plan

  • Added regression test cases to crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py.
  • Ran cargo test -p ruff_linter perflint.
  • Verified snapshots updated correctly (removing slow suggestions, adding fast suggestion).

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Feb 1, 2026

ruff-ecosystem results

Linter (stable)

ℹ️ ecosystem check detected linter changes. (+22 -65 violations, +0 -0 fixes in 8 projects; 47 projects unchanged)

apache/airflow (+2 -37 violations, +0 -0 fixes)

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

- airflow-core/docs/conf.py:168:13: PERF401 Use `list.extend` to create a transformed list
- airflow-core/docs/conf.py:172:13: PERF401 Use `list.extend` to create a transformed list
- airflow-core/src/airflow/cli/commands/dag_command.py:506:13: PERF401 Use `list.extend` to create a transformed list
- airflow-core/src/airflow/dag_processing/manager.py:1020:21: PERF401 Use `list.extend` to create a transformed list
- airflow-core/tests/integration/otel/test_otel.py:218:9: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/commands/ci_commands.py:362:25: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/commands/sbom_commands.py:1086:13: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/utils/airflow_release_validator.py:180:29: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/utils/airflow_release_validator.py:211:29: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/utils/publish_docs_to_s3.py:260:17: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/utils/reproducible.py:115:17: PERF401 Use `list.extend` to create a transformed list
+ dev/breeze/src/airflow_breeze/utils/run_tests.py:554:13: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/utils/selective_checks.py:1330:17: PERF401 Use `list.extend` to create a transformed list
- dev/stats/get_important_pr_candidates.py:908:17: PERF401 Use `list.extend` to create a transformed list
- devel-common/src/sphinx_exts/docs_build/fetch_inventories.py:128:9: PERF401 Use `list.extend` to create a transformed list
- devel-common/src/sphinx_exts/docs_build/fetch_inventories.py:136:9: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py:500:21: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py:706:21: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/log/s3_task_handler.py:163:17: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/operators/sagemaker.py:1262:17: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/operators/sagemaker.py:390:17: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/operators/sagemaker.py:396:17: PERF401 Use `list.extend` to create a transformed list
... 17 additional changes omitted for project

apache/superset (+1 -21 violations, +0 -0 fixes)

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

- scripts/benchmark_migration.py:128:21: PERF401 Use `list.extend` to create a transformed list
+ superset/commands/dashboard/update.py:140:21: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:224:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:614:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:620:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:630:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:648:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:659:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lint_metadata.py:576:9: PERF401 Use `list.extend` to create a transformed list
- superset/mcp_service/chart/validation/dataset_validator.py:180:17: PERF401 Use `list.extend` to create a transformed list
- superset/mcp_service/chart/validation/dataset_validator.py:213:13: PERF401 Use `list.extend` to create a transformed list
- superset/mcp_service/chart/validation/runtime/format_validator.py:119:21: PERF401 Use `list.extend` to create a transformed list
... 10 additional changes omitted for project

bokeh/bokeh (+1 -4 violations, +0 -0 fixes)

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

- src/bokeh/layouts.py:475:25: PERF401 Use `list.extend` to create a transformed list
+ src/bokeh/server/contexts.py:88:17: PERF401 Use `list.extend` to create a transformed list
- src/bokeh/settings.py:780:21: PERF401 Use `list.extend` to create a transformed list
- src/bokeh/settings.py:791:21: PERF401 Use `list.extend` to create a transformed list
- tests/codebase/test_windows_reserved_filenames.py:43:17: PERF401 Use `list.extend` to create a transformed list

langchain-ai/langchain (+1 -0 violations, +0 -0 fixes)

+ libs/langchain/tests/unit_tests/agents/test_agent.py:524:47: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)

latchbio/latch (+1 -3 violations, +0 -0 fixes)

+ src/latch_cli/menus.py:15:13: PERF401 Use `list.extend` to create a transformed list
- src/latch_cli/snakemake/config/utils.py:288:9: PERF401 Use `list.extend` to create a transformed list
- src/latch_cli/snakemake/workflow.py:155:21: PERF401 Use `list.extend` to create a transformed list
- src/latch_cli/snakemake/workflow.py:158:21: PERF401 Use `list.extend` to create a transformed list

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

+ tests/dspy/test_save.py:511:13: PERF401 Use `list.extend` to create a transformed list
+ tests/tracing/test_fluent.py:529:13: PERF401 Use `list.extend` to create a transformed list

indico/indico (+13 -0 violations, +0 -0 fixes)

+ indico/core/celery/core.py:181:88: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
+ indico/core/celery/core.py:184:83: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
+ indico/modules/events/registration/lists.py:123:93: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
+ indico/modules/rb/models/rooms.py:512:31: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
+ indico/modules/rb/models/rooms.py:519:31: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
+ indico/modules/rb/models/rooms.py:530:40: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
... 7 additional changes omitted for rule RUF100
+ indico/web/http_api/hooks/base.py:155:21: PERF401 Use `list.extend` to create a transformed list
... 6 additional changes omitted for project

python-trio/trio (+1 -0 violations, +0 -0 fixes)

+ src/trio/_tools/gen_exports.py:96:52: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)

Changes by rule (2 rules affected)

code total + violation - violation + fix - fix
PERF401 73 8 65 0 0
RUF100 14 14 0 0 0

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+22 -65 violations, +0 -0 fixes in 8 projects; 47 projects unchanged)

apache/airflow (+2 -37 violations, +0 -0 fixes)

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

- airflow-core/docs/conf.py:168:13: PERF401 Use `list.extend` to create a transformed list
- airflow-core/docs/conf.py:172:13: PERF401 Use `list.extend` to create a transformed list
- airflow-core/src/airflow/cli/commands/dag_command.py:506:13: PERF401 Use `list.extend` to create a transformed list
- airflow-core/src/airflow/dag_processing/manager.py:1020:21: PERF401 Use `list.extend` to create a transformed list
- airflow-core/tests/integration/otel/test_otel.py:218:9: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/commands/ci_commands.py:362:25: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/commands/sbom_commands.py:1086:13: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/utils/airflow_release_validator.py:180:29: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/utils/airflow_release_validator.py:211:29: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/utils/publish_docs_to_s3.py:260:17: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/utils/reproducible.py:115:17: PERF401 Use `list.extend` to create a transformed list
+ dev/breeze/src/airflow_breeze/utils/run_tests.py:554:13: PERF401 Use `list.extend` to create a transformed list
- dev/breeze/src/airflow_breeze/utils/selective_checks.py:1330:17: PERF401 Use `list.extend` to create a transformed list
- dev/stats/get_important_pr_candidates.py:908:17: PERF401 Use `list.extend` to create a transformed list
- devel-common/src/sphinx_exts/docs_build/fetch_inventories.py:128:9: PERF401 Use `list.extend` to create a transformed list
- devel-common/src/sphinx_exts/docs_build/fetch_inventories.py:136:9: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py:500:21: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py:706:21: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/log/s3_task_handler.py:163:17: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/operators/sagemaker.py:1262:17: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/operators/sagemaker.py:390:17: PERF401 Use `list.extend` to create a transformed list
- providers/amazon/src/airflow/providers/amazon/aws/operators/sagemaker.py:396:17: PERF401 Use `list.extend` to create a transformed list
... 17 additional changes omitted for project

apache/superset (+1 -21 violations, +0 -0 fixes)

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

- scripts/benchmark_migration.py:128:21: PERF401 Use `list.extend` to create a transformed list
+ superset/commands/dashboard/update.py:140:21: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:224:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:614:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:620:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:630:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:648:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lib.py:659:9: PERF401 Use `list.extend` to create a transformed list
- superset/db_engine_specs/lint_metadata.py:576:9: PERF401 Use `list.extend` to create a transformed list
- superset/mcp_service/chart/validation/dataset_validator.py:180:17: PERF401 Use `list.extend` to create a transformed list
- superset/mcp_service/chart/validation/dataset_validator.py:213:13: PERF401 Use `list.extend` to create a transformed list
- superset/mcp_service/chart/validation/runtime/format_validator.py:119:21: PERF401 Use `list.extend` to create a transformed list
... 10 additional changes omitted for project

bokeh/bokeh (+1 -4 violations, +0 -0 fixes)

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

- src/bokeh/layouts.py:475:25: PERF401 Use `list.extend` to create a transformed list
+ src/bokeh/server/contexts.py:88:17: PERF401 Use `list.extend` to create a transformed list
- src/bokeh/settings.py:780:21: PERF401 Use `list.extend` to create a transformed list
- src/bokeh/settings.py:791:21: PERF401 Use `list.extend` to create a transformed list
- tests/codebase/test_windows_reserved_filenames.py:43:17: PERF401 Use `list.extend` to create a transformed list

langchain-ai/langchain (+1 -0 violations, +0 -0 fixes)

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

+ libs/langchain/tests/unit_tests/agents/test_agent.py:524:47: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)

latchbio/latch (+1 -3 violations, +0 -0 fixes)

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

+ src/latch_cli/menus.py:15:13: PERF401 Use `list.extend` to create a transformed list
- src/latch_cli/snakemake/config/utils.py:288:9: PERF401 Use `list.extend` to create a transformed list
- src/latch_cli/snakemake/workflow.py:155:21: PERF401 Use `list.extend` to create a transformed list
- src/latch_cli/snakemake/workflow.py:158:21: PERF401 Use `list.extend` to create a transformed list

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

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

+ tests/dspy/test_save.py:511:13: PERF401 Use `list.extend` to create a transformed list
+ tests/tracing/test_fluent.py:529:13: PERF401 Use `list.extend` to create a transformed list

indico/indico (+13 -0 violations, +0 -0 fixes)

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

+ indico/core/celery/core.py:181:88: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
+ indico/core/celery/core.py:184:83: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
+ indico/modules/events/registration/lists.py:123:93: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
+ indico/modules/rb/models/rooms.py:512:31: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
+ indico/modules/rb/models/rooms.py:519:31: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
+ indico/modules/rb/models/rooms.py:530:40: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)
... 7 additional changes omitted for rule RUF100
+ indico/web/http_api/hooks/base.py:155:21: PERF401 Use `list.extend` to create a transformed list
... 6 additional changes omitted for project

python-trio/trio (+1 -0 violations, +0 -0 fixes)

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

+ src/trio/_tools/gen_exports.py:96:52: RUF100 [*] Unused `noqa` directive (unused: `PERF401`)

Changes by rule (2 rules affected)

code total + violation - violation + fix - fix
PERF401 73 8 65 0 0
RUF100 14 14 0 0 0

@danparizher
Copy link
Copy Markdown
Contributor Author

The ecosystem check findings (decreased PERF401 violations, increased RUF100 violations) are expected, as we are no longer flagging the "slow" cases that users may have suppressed with noqa

Comment on lines -181 to -183
if arg
.as_name_expr()
.is_some_and(|arg| arg.id == *for_stmt_target_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I should have noticed this earlier, but as the comment notes, this is an intentional "false negative" to avoid a duplicate diagnostic with manual-list-copy (PERF402). On this branch:

just run check --select PERF --preview - <<EOF
∙ def main1():
    ret = []
    for x in range(8):
        for i in range(x):
            ret.append(i)
∙ EOF
PERF402 Use `list` or `list.copy` to create a copy of a list
 --> -:5:13
  |
3 |     for x in range(8):
4 |         for i in range(x):
5 |             ret.append(i)
  |             ^^^^^^^^^^^^^
  |

PERF401 Use `list.extend` to create a transformed list
 --> -:5:13
  |
3 |     for x in range(8):
4 |         for i in range(x):
5 |             ret.append(i)
  |             ^^^^^^^^^^^^^
  |
help: Replace for loop with list.extend

vs a released branch:

PERF402 Use `list` or `list.copy` to create a copy of a list
 --> -:5:13
  |
3 |     for x in range(8):
4 |         for i in range(x):
5 |             ret.append(i)
  |             ^^^^^^^^^^^^^
  |

Found 1 error.

However, the PERF402 diagnostic is the subject of another bug report: #8070, so I would actually be supportive of moving this case from PERF402 to PERF401 in preview. I would also suggest adding a test case where both rules are active to avoid any regressions here in the future.

I think we should handle that change and the perf-related extend changes in separate PRs to make review easier. I also think if we're modifying the extend behavior for PERF401, we should do the same for PERF403. I know the needs-decision Awaiting a decision from a maintainer label was removed from #21890, but I still don't feel like we have strong consensus on how to handle these rules, which is another reason to separate out the changes.

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

Labels

rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PERF401 extend suggestion has false negatives and makes code slower

2 participants