bool(obj) does not infer from obj.__bool__()
Bug Report
(Although there are other issues that allude to this problem, I haven't been able to locate an issue which specifically addresses truthiness where __bool__() is defined and @final. The search term "truthy bool" may be helpful for finding related issues.)
Implicit truthiness does not (always?) take an object's __bool__() into account, even when that method is @final.
To Reproduce
This example is posted to mypy-play.net.
from typing import *
@final
class SomeClass:
def __bool__(self) -> Literal[True]:
return True
instance: SomeClass
reveal_type(instance.__bool__()) # 'Literal[True]' - correct
reveal_type(bool(instance)) # 'bool' - incorrect (should be 'Literal[True]')
if instance:
reveal_type(instance) # 'SomeClass' - correct
else:
assert_never(instance) # ...'SomeClass' - incorrect (should be silent)
(Whether the class or method is decorated @final makes no difference.)
Expected Behavior
if instance: ... and bool(instnace) should take into account that SomeClass has a @final __bool__() with return type Literal[True], and consistently treat instances of SomeClass as definitely truthy.
In general, implicit truthiness for an object should take into account the truthiness of the return type of any @final __bool__() the object's class defines.
Comments inline in the above code highlight differences from expected behaviour.
Actual Behavior
main.py:10: note: Revealed type is "Literal[True]"
main.py:11: note: Revealed type is "builtins.bool"
main.py:14: note: Revealed type is "__main__.SomeClass"
main.py:16: error: Argument 1 to "assert_never" has incompatible type "SomeClass"; expected "NoReturn" [arg-type]
Found 1 error in 1 file (checked 1 source file)
Your Environment
- Mypy version used: 1.4.0, master (2023-06-26)
- Mypy command-line flags: (none)
- Mypy configuration options from
mypy.ini(and other config files): (none) - Python version used: 3.11
I think this is a duplicate of #7008, or at least that issue was kept open because of this specific problem.
Because of the Liskov Substitution Principle a narrowed return type can't be unnarrowed by a subclass so @final isn't necessary for this example.
In case it matters for triage: #7008 has been closed without solving this issue.
(I can confirm the issue is reproducible on the master branch as of this date.)
A similar situation where this issue is prevalent is where the dunder method is known to always raise an exception. Pandas logic and its corresponding issue is an example where this exists.
Both of the below examples are not identified (example):
from typing import *
@final
class NeverBoolClass:
def __bool__(self) -> NoReturn:
raise NotImplementedError()
@final
class NeverLenClass:
def __len__(self) -> NoReturn:
raise NotImplementedError()
I did notice that https://github.com/python/mypy/pull/10666 previously added similar checks but I'm not confident that area would be the correct one for addressing this issue. If anyone can provide some guidance as to if that is a good place to implement a fix or if it would be better suited somewhere else, I'd be happy to take a stab at the PR.