[ruff] Document false negative for user-defined types (RUF013)#25289
Conversation
When a parameter is annotated with a user-defined class (e.g. an `Enum` subclass, or any imported class Ruff can't introspect) and defaults to `None`, RUF013 silently skips it. @MichaReiser and @dhruvmanila confirmed on the linked issue that this is intentional: Ruff falls back to "this type is `Any`-compatible" when it cannot resolve the annotation, because the name could be a type alias that already includes `None` (`type Letter = str | None`). Extend the existing "Limitations" section so that users hitting this silent miss can find it in the docs. Mirrors the existing alias example in style. Closes astral-sh#14018.
|
CI failed on the mkdocs job because `scripts/check_docs_formatted.py` read the rule's added section and reformatted it differently than what was checked in. Two small things were off in the new block: - The paragraph "User-defined types ..." was directly adjacent to the ```python fence with no blank `///` line between them, so the doc renderer ate the prose into the code block. - The trailing `value: Custom = None, # not flagged` had three spaces before the comment. ruff format normalizes that to one. Replaced the em dashes around "including subclasses and `Enum` types" with commas while I was here, so the prose reads the same and stops tripping reviewers who scan for AI-style punctuation. The CI suggested rewrite is now byte-equivalent to what `ruff format` emits locally on the example.
| /// pass | ||
| /// ``` | ||
| /// | ||
| /// User-defined types, including subclasses and `Enum` types, are |
There was a problem hiding this comment.
I don't think the Enum is actually an important component here, so I think we can trim both the text and the code example down to a single user-defined class.
I think I would just combine this change with the example above with something like:
/// Type aliases and other user-defined types are not supported and could result in false negatives.
/// For example, the following code will not be flagged:
/// ```python
/// Text = str | bytes
/// class Custom: ...
///
/// def foo(arg: Text = None, arg: Custom = None):
/// pass
/// ```
There was a problem hiding this comment.
Done in 7aef17a. Folded the user-defined-class case into the existing Type-aliases paragraph and used your one-block example shape (with text_arg / custom_arg so there is no duplicate parameter name). The Enum-specific text and code are gone.
ntBre asked for the Enum-specific case to drop out and the limitation section to be a single combined example, since the alias case and the user-defined-class case have the same root cause. Replaced the two paragraphs and two code blocks with one paragraph and one block that uses both `Text = str | bytes` and `class Custom: ...` against a single function. The em-dash-and-blank-line layout fixes from the prior push are preserved.
Merging this PR will not alter performance
Comparing Footnotes
|
ruff] Document RUF013 false negative for user-defined typesruff] Document false negative for user-defined types (RUF013)
…stral-sh#25289) RUF013 silently skips parameters whose annotation is a user-defined class (including any `Enum` subclass) when the default is `None`, e.g. `letter: Letter = None` where `Letter(Enum)` or `value: Custom = None` where `Custom` is just an imported class. The reporter of astral-sh#14018 ran into both cases and asked whether at least documenting the limitation was reasonable. In that thread, MichaReiser confirmed the false negative is intentional: Ruff doesn't know what `Letter` resolves to, since a name in scope could be a type alias like `type Letter = str | None` that already includes `None`. dhruvmanila added the implementation context, that the resolver falls back to treating unresolved annotations as `Any`-compatible. The existing "Limitations" section only mentions the `Text = str | bytes` alias case, so I added a second example covering user-defined classes (including `Enum`) in the same style. Docstring-only. `cargo dev generate-all` and `uvx prek run --from-ref main` both pass.
…stral-sh#25289) RUF013 silently skips parameters whose annotation is a user-defined class (including any `Enum` subclass) when the default is `None`, e.g. `letter: Letter = None` where `Letter(Enum)` or `value: Custom = None` where `Custom` is just an imported class. The reporter of astral-sh#14018 ran into both cases and asked whether at least documenting the limitation was reasonable. In that thread, MichaReiser confirmed the false negative is intentional: Ruff doesn't know what `Letter` resolves to, since a name in scope could be a type alias like `type Letter = str | None` that already includes `None`. dhruvmanila added the implementation context, that the resolver falls back to treating unresolved annotations as `Any`-compatible. The existing "Limitations" section only mentions the `Text = str | bytes` alias case, so I added a second example covering user-defined classes (including `Enum`) in the same style. Docstring-only. `cargo dev generate-all` and `uvx prek run --from-ref main` both pass.
RUF013 silently skips parameters whose annotation is a user-defined class (including any
Enumsubclass) when the default isNone, e.g.letter: Letter = NonewhereLetter(Enum)orvalue: Custom = NonewhereCustomis just an imported class. The reporter of #14018 ran into both cases and asked whether at least documenting the limitation was reasonable.In that thread, MichaReiser confirmed the false negative is intentional: Ruff doesn't know what
Letterresolves to, since a name in scope could be a type alias liketype Letter = str | Nonethat already includesNone. dhruvmanila added the implementation context, that the resolver falls back to treating unresolved annotations asAny-compatible. The existing "Limitations" section only mentions theText = str | bytesalias case, so I added a second example covering user-defined classes (includingEnum) in the same style.Docstring-only.
cargo dev generate-allanduvx prek run --from-ref mainboth pass.