-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
warn-unreachable false-positive when method updates attribute after assert #11969
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
In this scenario mypy pretty much assumes everything is immutable and only takes into consideration explicit mutation, not side effects from any function calls. This is because otherwise most narrowing would be invalidated instantly. And to check each function to see if and what variables it mutates would be extremely slow and complicated. |
Yes, I agree with @KotlinIsland. This is intentional. You can use |
Methods that mutate attributes are surely a common occurrence, and I feel that assuming that they don't exist for the purpose of detecting unreachable code is the wrong thing to do, as it would lead to too many false positives. I don't know what sort of narrowing would be invalidated by not assuming everything is immutable, but could it be possible to only "invalidate" it for warn-unreachable purposes? |
I agree with @jwodder - this is common and its a bad assumption variables can't change. Typescript for example also has type narrowing and does not have this issue: https://www.typescriptlang.org/docs/handbook/2/narrowing.html
|
TypeScript also assumes that variables are not changed "behind the back" of the type checker in another execution scope. Without this assumption, it wouldn't be feasible to apply type narrowing to any member access expressions. class Foo {
bar: number | string = 0;
setBarToString() {
this.bar = '';
}
}
function test() {
const f = new Foo();
f.bar = 1;
const val1 = f.bar; // Type of "val1" is revealed as "number"
f.setBarToString();
const val2 = f.bar; // Type of "val2" is still revealed as "number"
} |
Kotlin handles this correctly by only narrowing class Foo(
val foo: Any,
var bar: Any,
)
fun test() {
val f = Foo(1, 1);
if (f.foo is String) {
val v2 = f.foo // Type of "v2" is revealed as "String"
}
if (f.bar is String) {
val v2 = f.bar // Type of "v2" is still revealed as "Any"
}
(f.bar as? String)?.let {
val v2 = it // Type of "v2" is now revealed as "String"
}
} This way Kotlin is able to maintain perfect correctness while still achieving useful narrowing. |
I asked about this in SO where user sytech introduced a workaround using type guard functions using TypeGuard. They're essentially functions that must return a boolean, and if they return True the first positional argument (or first after self in methods) is guaranteed to have type I wrote two helper functions,
Full example: import sys
from typing import Type, TypeVar
if sys.version_info < (3, 10):
from typing_extensions import TypeGuard
else:
from typing import TypeGuard
from dataclasses import dataclass
@dataclass
class Foo:
value: int
modified: bool = False
def update(self) -> None:
self.value += 1
self.modified = True
_T = TypeVar("_T")
def object_is(obj, value: _T) -> TypeGuard[_T]:
return obj is value
def is_instance(val, klass: Type[_T]) -> TypeGuard[_T]:
return isinstance(val, klass)
foo = Foo(42)
assert object_is(foo.modified, False)
foo.update()
assert object_is(foo.modified, True)
print("Reached") This uses typing-extensions to make the solution to work on older versions, as TypeGuard was introduced in Python 3.10. I tested this with mypy 1.9.0 and it works well on Python 3.7 to 3.12. |
This problem continues to plague otherwise valid code. Take the following reproducer (which even uses class Foo:
@property
def bool_prop(self) -> bool:
pass
f = Foo()
assert f.bool_prop
assert not f.bool_prop
x = 0 $ mypy mypy_bug.py
mypy_bug.py: note: At top level:
mypy_bug.py:19:1: error: Statement is unreachable [unreachable]
x = 0
^~~~~
Found 1 error in 1 file (checked 1 source file) Mypy's decision to not address this on the grounds that type narrowing would be broken is strange to say the least. Python type checking already relies entirely on promises by the programmer. If my code says def foo() -> Bar:
return typing.cast(Bar, 12345) And Also it seems that with this feature |
mypy is very unlikely to make any changes here. If you want an easy way to reset what mypy thinks it knows about the attributes of a variable, you can do |
@hauntsaninja inserting |
I said foo = foo |
Consider the following code:
When this is run, the
print("Reached")
line is reached, but mypy doesn't seem to realize that. Runningmypy --warn-unreachable --pretty
on the above code gives:Your Environment
--warn-unreachable --pretty
mypy.ini
(and other config files): noneThe text was updated successfully, but these errors were encountered: