Repository: python/mypy

Issue:
Regression for truthy bool with getattr
The following code generates an error on latest master but no on latest release (with `--enable-error-codes=truthy-bool`):
```python
def f(obj: object) -> None:
    print("{}".format(
        getattr(type(obj), "__foo__", "") or "unknown"  # E: "builtins.getattr" returns "object" which does not implement __bool__ or __len__ so it could always be true in boolean context  [truthy-bool]
    ))

f("")
```

It's clearly a false positive, if your run the code you see that it prints `unknown`.




Discussion:
Using @hauntsaninja's mypy_primer, it pointed to #14178. 
```
$ cat test.py
# flags: --enable-error-code truthy-bool
def f(obj: object) -> None:
    print("{}".format(
        getattr(type(obj), "__foo__", "") or "unknown"  # E: "builtins.getattr" returns "object" which does not implement __bool__ or __len__ so it could always be true in boolean context  [truthy-bool]
    ))
$ python3 mypy_primer.py -p test.py --bisect --debug --old v0.991
...
4471c7e76f27ee51eb8d47a4803097ec15c62128 is the first bad commit
...
```

This is likely the same underlying issue as #14481.

@ilevkivskyi 


Here's a bit simpler example that reproduces the issue:
```py
def f(t: type[object]) -> None:
    o: object = getattr(t, "x", "") or ""
```
This looks different from #14481, as there are no TypedDicts involved here.

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -5151,6 +5151,7 @@ def _is_truthy_type(self, t: ProperType) -> bool:
                 and bool(t.type)
                 and not t.type.has_readable_member("__bool__")
                 and not t.type.has_readable_member("__len__")
+                and t.type.fullname != "builtins.object"
             )
             or isinstance(t, FunctionLike)
             or (


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
@@ -803,12 +803,15 @@ from typing_extensions import TypedDict
 
 Foo = TypedDict("Bar", {})  # E: First argument "Bar" to TypedDict() does not match variable name "Foo"  [name-match]
 [builtins fixtures/dict.pyi]
+
 [case testTruthyBool]
 # flags: --enable-error-code truthy-bool
-from typing import List, Union
+from typing import List, Union, Any
 
 class Foo:
     pass
+class Bar:
+    pass
 
 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  [truthy-bool]
@@ -836,15 +839,30 @@ if good_union:
 if not good_union:
     pass
 
-bad_union: Union[Foo, object] = Foo()
-if bad_union:  # E: "__main__.bad_union" has type "Union[Foo, object]" of which no members implement __bool__ or __len__ so it could always be true in boolean context  [truthy-bool]
+bad_union: Union[Foo, Bar] = Foo()
+if bad_union:  # E: "__main__.bad_union" has type "Union[Foo, Bar]" of which no members implement __bool__ or __len__ so it could always be true in boolean context  [truthy-bool]
+    pass
+if not bad_union:  # E: "__main__.bad_union" has type "Union[Foo, Bar]" of which no members implement __bool__ or __len__ so it could always be true in boolean context  [truthy-bool]
+    pass
+
+# 'object' is special and is treated as potentially falsy
+obj: object = Foo()
+if obj:
     pass
-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]
+if not obj:
     pass
 
 lst: List[int] = []
 if lst:
     pass
+
+a: Any
+if a:
+    pass
+
+any_or_object: Union[object, Any]
+if any_or_object:
+    pass
 [builtins fixtures/list.pyi]
 
 [case testTruthyFunctions]