Repository: python/mypy

Issue:
Add truthy-callable-bool check
**Feature**

Using a method as a property is a common bug that can be detected by a type checker:
```python
if my_obj.my_method:
   ...
```

I'd like to introduce a way to enable a check like `truthy-bool` (#10666) but limited to callable types.

**Pitch**

Users users might be hesitant to enable `truthy-bool` due to false positives. For callable types, there's a much higher degree of confidence that evaluating a callable as a boolean is unintentional.

- The error message should be specific, e.g.
   ```patch
   - error: Member "my_method" has type "function" which does not implement __bool__ or __len__ so it could always be true in boolean context 
   + error: Member "my_method" is a method (are you trying to use it as a property?)
   ```
   (happy to wordsmith this more, only trying to illustrate that I want it to be friendlier and more actionable)
- The feature could be a separate error-code (thus splitting "callable evaluated in boolean context" from `truthy-bool`), or perhaps a way to configure `truthy-bool` to have a reduced scope? (Is there a common idiom for that?)


Discussion:
@hauntsaninja what do you think?
FWIW, I recently added [this check](https://github.com/microsoft/pyright/commit/ab1fbfc0d6c20c911bb5bf3576939b2223bb5c0d) to pyright, and it was well received. This is a common programming error, and it can be detected with a negligible false positive rate. It would be great if mypy could detect and report this error.
I'm in favour, the reduced scope means we could likely have it on by default.

Resolving patch:
diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py
--- a/mypy/errorcodes.py
+++ b/mypy/errorcodes.py
@@ -139,6 +139,11 @@ def __str__(self) -> str:
     "General",
     default_enabled=False,
 )
+TRUTHY_FUNCTION: Final[ErrorCode] = ErrorCode(
+    "truthy-function",
+    "Warn about function that always evaluate to true in boolean contexts",
+    "General",
+)
 NAME_MATCH: Final = ErrorCode(
     "name-match", "Check that type definition has consistent naming", "General"
 )
diff --git a/mypy/message_registry.py b/mypy/message_registry.py
--- a/mypy/message_registry.py
+++ b/mypy/message_registry.py
@@ -145,7 +145,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
     code=codes.TRUTHY_BOOL,
 )
 FUNCTION_ALWAYS_TRUE: Final = ErrorMessage(
-    "Function {} could always be true in boolean context", code=codes.TRUTHY_BOOL
+    "Function {} could always be true in boolean context", code=codes.TRUTHY_FUNCTION
 )
 NOT_CALLABLE: Final = "{} not callable"
 TYPE_MUST_BE_USED: Final = "Value of type {} must be used"


Test patch:
diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test
--- a/test-data/unit/check-errorcodes.test
+++ b/test-data/unit/check-errorcodes.test
@@ -841,19 +841,21 @@ if bad_union:  # E: "__main__.bad_union" has type "Union[Foo, object]" of which
 if not bad_union:  # E: "__main__.bad_union" has type "object" which does not implement __bool__ or __len__ so it could always be true in boolean context  [truthy-bool]
     pass
 
-def f():
-    pass
-if f:  # E: Function "Callable[[], Any]" could always be true in boolean context  [truthy-bool]
-    pass
-if not f:  # E: Function "Callable[[], Any]" could always be true in boolean context  [truthy-bool]
-    pass
-conditional_result = 'foo' if f else 'bar'  # E: Function "Callable[[], Any]" could always be true in boolean context  [truthy-bool]
-
 lst: List[int] = []
 if lst:
     pass
 [builtins fixtures/list.pyi]
 
+[case testTruthyFunctions]
+# flags: --strict-optional
+def f():
+    pass
+if f:  # E: Function "Callable[[], Any]" could always be true in boolean context  [truthy-function]
+    pass
+if not f:  # E: Function "Callable[[], Any]" could always be true in boolean context  [truthy-function]
+    pass
+conditional_result = 'foo' if f else 'bar'  # E: Function "Callable[[], Any]" could always be true in boolean context  [truthy-function]
+
 [case testNoOverloadImplementation]
 from typing import overload
 
diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test
--- a/test-data/unit/check-incremental.test
+++ b/test-data/unit/check-incremental.test
@@ -6016,12 +6016,15 @@ tmp/m.py:2: error: Argument "age" to "foo" has incompatible type "str"; expected
 [case testDisableEnableErrorCodesIncremental]
 # flags: --disable-error-code truthy-bool
 # flags2: --enable-error-code truthy-bool
-def foo() -> int: ...
+class Foo:
+    pass
+
+foo = Foo()
 if foo:
     ...
 [out]
 [out2]
-main:4: error: Function "Callable[[], int]" could always be true in boolean context
+main:7: error: "__main__.foo" has type "Foo" which does not implement __bool__ or __len__ so it could always be true in boolean context
 
 [case testModuleAsProtocolImplementationSerialize]
 import m
diff --git a/test-data/unit/check-inline-config.test b/test-data/unit/check-inline-config.test
--- a/test-data/unit/check-inline-config.test
+++ b/test-data/unit/check-inline-config.test
@@ -166,9 +166,11 @@ main:1: error: Setting "strict" not supported in inline configuration: specify i
 [case testInlineErrorCodes]
 # flags: --strict-optional
 # mypy: enable-error-code="ignore-without-code,truthy-bool"
+class Foo:
+    pass
 
-def foo() -> int: ...
-if foo: ...  # E: Function "Callable[[], int]" could always be true in boolean context
+foo = Foo()
+if foo: ...  # E: "__main__.foo" has type "Foo" which does not implement __bool__ or __len__ so it could always be true in boolean context
 42 + "no"  # type: ignore  # E: "type: ignore" comment without error code (consider "type: ignore[operator]" instead)
 
 [case testInlineErrorCodesOverrideConfig]
@@ -178,8 +180,10 @@ import tests.bar
 import tests.baz
 [file foo.py]
 # mypy: disable-error-code="truthy-bool"
+class Foo:
+    pass
 
-def foo() -> int: ...
+foo = Foo()
 if foo: ...
 42 + "no"  # type: ignore  # E: "type: ignore" comment without error code (consider "type: ignore[operator]" instead)
 
@@ -193,8 +197,10 @@ if foo: ...  # E: Function "Callable[[], int]" could always be true in boolean c
 
 [file tests/baz.py]
 # mypy: disable-error-code="truthy-bool"
+class Foo:
+    pass
 
-def foo() -> int: ...
+foo = Foo()
 if foo: ...
 42 + "no"  # type: ignore
 
diff --git a/test-data/unit/check-python38.test b/test-data/unit/check-python38.test
--- a/test-data/unit/check-python38.test
+++ b/test-data/unit/check-python38.test
@@ -310,7 +310,7 @@ def f(x: int = (c := 4)) -> int:
         z2: NT  # E: Variable "NT" is not valid as a type \
                 # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
 
-    if Alias := int:
+    if Alias := int:  # E: Function "Type[int]" could always be true in boolean context
         z3: Alias  # E: Variable "Alias" is not valid as a type \
                    # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
 
diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test
--- a/test-data/unit/check-unreachable-code.test
+++ b/test-data/unit/check-unreachable-code.test
@@ -936,7 +936,8 @@ class Case1:
         return False and self.missing()  # E: Right operand of "and" is never evaluated
 
     def test2(self) -> bool:
-        return not self.property_decorator_missing and self.missing()  # E: Right operand of "and" is never evaluated
+        return not self.property_decorator_missing and self.missing()  # E: Function "Callable[[], bool]" could always be true in boolean context \
+            # E: Right operand of "and" is never evaluated
 
     def property_decorator_missing(self) -> bool:
         return True