Skip to content

Conversation

@yunchipang
Copy link
Contributor

parent: #15584
issue: #16163

@yunchipang yunchipang mentioned this pull request May 6, 2025
71 tasks
@github-actions
Copy link
Contributor

github-actions bot commented May 6, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@AlexWaygood AlexWaygood added the documentation Improvements or additions to documentation label May 6, 2025
Comment on lines 23 to 31
/// ## Fix safety
/// This fix is always unsafe. Flattening nested `min()` or `max()` may change
/// code behavior when the inner call is the outer call’s only argument and the
/// inner call has multiple arguments.
/// ```python
/// print(min(min([2, 3], [4, 1]))) # before: 2
/// print(min([2, 3], [4, 1])) # after: [2, 3]
/// ```
///
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't really a fix safety issue, this is a bug with an open issue that needs to be fixed.

I went through the git blame on this one, and I'm not really sure it actually needs to be unsafe. It was first converted from Fix::unspecified to Fix::suggested in #5251, which became Fix::sometimes_applies, which became Fix::unsafe_edit. I didn't see any specific discussion of the fix being unsafe, so I think it may have just been the more conservative option.

All that to say, it's not clear to me why this rule would need to be unsafe, at least from its description and development history. We even check for comment ranges and avoid a fix entirely if comments are present, so let's hold off on this for now.

Choose a reason for hiding this comment

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

The min fix is unsafe when __lt__ is not commutative. The max fix is unsafe when __gt__ is not commutative.

$ cat >plw3301.py <<'# EOF'
print(min(2.0, min(float("nan"), 1.0)))
print(max(1.0, max(float("nan"), 2.0)))
# EOF

$ python plw3301.py
2.0
1.0

$ ruff check --isolated --select PLW3301 plw3301.py --unsafe-fixes --fix
Found 2 errors (2 fixed, 0 remaining).

$ python plw3301.py
1.0
2.0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ntBre @dscorbett thanks so much for the input! I've updated the docs, though I'm not exactly sure, referencing the above case, is this rule always or sometimes unsafe? In general, what's makes a rule always/sometimes unsafe?

thanks a lot.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah great catch as always, @dscorbett! I was hoping you might see this one.

I may have mentioned this on another PR, but the way I check for safety is to search the file for safe_edit. If you find cases of Fix::safe_edit and Fix::unsafe_edit, then the rule is sometimes safe. If you only find Fix::unsafe_edit, as I believe is the case here, the rule is always unsafe. You'll have to be slightly more careful with your search range in files with multiple rules, but the general strategy still applies.

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.

Thanks, this is looking good.

Comment on lines 24 to 25
/// This fix is always unsafe. The `min` fix is unsafe when `__lt__` is not commutative,
/// while the `max` fix is unsafe when `__gt__` is not commutative.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd probably phrase this more like

This fix is always unsafe because it can change the program's behavior in cases where the underlying dunder methods (__lt__ for min and __gt__ for max) are not commutative, as is the case for floats, for example:

Copy link

@dscorbett dscorbett May 7, 2025

Choose a reason for hiding this comment

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

“Commutative” is the wrong word: I meant “associative”. Sorry for the confusion. Actually, I’m not sure associativity is precisely what is necessary and sufficient here. It’s probably best to avoid the technical jargon unless someone proves it’s correct.

@yunchipang
Copy link
Contributor Author

@nefrob @dscorbett thanks for the reviews! I’ve adjusted the wording slightly based on my understanding—hope it’s clearer and aligns with the intent. Let me know if there’s any part that could be further improved!

Comment on lines 24 to 27
/// This fix is always unsafe and may change the program's behavior when expanding
/// `min()` or `max()` calls. The underlying dunder methods (`__lt__` for `min` and
/// `__gt__` for `max`) are not associative, particularly when dealing with special
/// floating-point values like `NaN` (Not a Number).
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// This fix is always unsafe and may change the program's behavior when expanding
/// `min()` or `max()` calls. The underlying dunder methods (`__lt__` for `min` and
/// `__gt__` for `max`) are not associative, particularly when dealing with special
/// floating-point values like `NaN` (Not a Number).
/// This fix is always unsafe and may change the program's behavior for
/// types without full equivalence relations, such as float comparisons involving `NaN`.

I think associativity is the correct property, but this is another possibility that I borrowed from the Rust PartialEq docs. I think we could also phrase it as "types without a total ordering" or something. Maybe associative is close enough to stick with that 😅

@yunchipang yunchipang requested a review from ntBre May 9, 2025 21:35
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.

Looks good, thanks! I just pushed one small change putting the section back at the end.

@ntBre ntBre enabled auto-merge (squash) May 12, 2025 20:35
@ntBre ntBre merged commit f549dfe into astral-sh:main May 12, 2025
32 of 33 checks passed
@yunchipang yunchipang deleted the docs/fix-safety-nested-min-max branch May 12, 2025 21:34
dcreager added a commit that referenced this pull request May 13, 2025
…eepish

* origin/main:
  [ty] Induct into instances and subclasses when finding and applying generics (#18052)
  [ty] Allow classes to inherit from `type[Any]` or `type[Unknown]` (#18060)
  [ty] Allow a class to inherit from an intersection if the intersection contains a dynamic type and the intersection is not disjoint from `type` (#18055)
  [ty] Narrowing for `hasattr()` (#18053)
  Update reference documentation for `--python-version` (#18056)
  [`flake8-bugbear`] Ignore `B028` if `skip_file_prefixes` is present (#18047)
  [`airflow`] Apply try-catch guard to all AIR3 rules (`AIR3`) (#17887)
  [`pylint`] add fix safety section (`PLW3301`) (#17878)
  Update `--python` to accept paths to executables in virtual environments (#17954)
  [`pylint`] add fix safety section (`PLE4703`) (#17824)
  [`ruff`] Implement a recursive check for `RUF060` (#17976)
  [`flake8-use-pathlib`] `PTH*` suppress diagnostic for all `os.*` functions that have the `dir_fd` parameter (#17968)
  [`refurb`] Mark autofix as safe only for number literals in `FURB116` (#17692)
  [`flake8-simplify`] Fix `SIM905` autofix for `rsplit` creating a reversed list literal (#18045)
  Avoid initializing progress bars early (#18049)
dcreager added a commit that referenced this pull request May 13, 2025
…eep-dish

* origin/main:
  [ty] Infer parameter specializations of generic aliases (#18021)
  [ty] Understand homogeneous tuple annotations (#17998)
  [ty] Induct into instances and subclasses when finding and applying generics (#18052)
  [ty] Allow classes to inherit from `type[Any]` or `type[Unknown]` (#18060)
  [ty] Allow a class to inherit from an intersection if the intersection contains a dynamic type and the intersection is not disjoint from `type` (#18055)
  [ty] Narrowing for `hasattr()` (#18053)
  Update reference documentation for `--python-version` (#18056)
  [`flake8-bugbear`] Ignore `B028` if `skip_file_prefixes` is present (#18047)
  [`airflow`] Apply try-catch guard to all AIR3 rules (`AIR3`) (#17887)
  [`pylint`] add fix safety section (`PLW3301`) (#17878)
  Update `--python` to accept paths to executables in virtual environments (#17954)
  [`pylint`] add fix safety section (`PLE4703`) (#17824)
  [`ruff`] Implement a recursive check for `RUF060` (#17976)
  [`flake8-use-pathlib`] `PTH*` suppress diagnostic for all `os.*` functions that have the `dir_fd` parameter (#17968)
  [`refurb`] Mark autofix as safe only for number literals in `FURB116` (#17692)
  [`flake8-simplify`] Fix `SIM905` autofix for `rsplit` creating a reversed list literal (#18045)
  Avoid initializing progress bars early (#18049)
Glyphack pushed a commit to Glyphack/ruff that referenced this pull request May 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants