astral-sh / ruff

An extremely fast Python linter and code formatter, written in Rust.
https://docs.astral.sh/ruff
MIT License
32.67k stars 1.09k forks source link

`typed-argument-default-in-stub` (`PYI011`) - false positive when default value intentionally used to infer the default value of a generic #12999

Open DetachHead opened 2 months ago

DetachHead commented 2 months ago

not sure if this is something that can be determined without type analysis (#3893), but sometimes PYI011 conflicts with pyright's reportInvalidTypeVarUse rule. for example, in the following .pyi file:

# foo.pyi

a: int

def foo[T](value: T | None = a) -> T: ... # error: PYI011
# usage.py

from foo import foo
reveal_type(foo()) # int

if we take ruff's advice and omit the default value here, it prevents type checkers from being able to infer the generic based on the default value, which is why pyright reports an error:

# foo.pyi

a: int

def foo[T](value: T | None = ...) -> T: ... # no ruff error, but now pyright reports reportInvalidTypeVarUse
# usage.py

from foo import foo
reveal_type(foo()) # Any
Avasam commented 2 months ago

A couple notes:

Note that assigning a default value to a generic to infer the default generic type is currently not supported in mypy and will result in type Never:

main.py:3: error: Missing return statement  [empty-body]
main.py:3: error: Incompatible default for argument "value" (default has type "int", argument has type "T | None")  [assignment]
main.py:5: note: Revealed type is "Never"

Tracked here: https://github.com/python/mypy/issues/3737 (feel free to upvote, I too hit that issue and wished it worked like in pyright)

Whilst this being flagged by PYI011 is incidental, I'd recommend avoiding that form unless you're writing private stubs (using pyright only), and instead use something like:

from typing import overload

@overload
def foo() -> int: ...
@overload
def foo[T](value: T | None) -> T: ... 

Now as for PYI011 itself, it's more of a convention in typeshed to not bother with non-literal defaults. I don't think there's anything inherently wrong with your example[^1] (assuming it works in the type-checkers you're interested in). I personally don't think it's a false-positive, you may simply not be interested in the rule for your project.

[^1]: In your example,

    reveal_type(foo(None))
would be `Unknown` / `Any`. But that has no effect on the mypy issue mentioned above or `PYI011`, so I'll ignore that