Repository: python/mypy

Issue:
TypeGuard/TypeIs broken on __call__ with fresh class

**Bug Report**

Using `TypeGuard` or `TypeIs` on the `__call__` method on an object that is created in the if statement is broken.


**To Reproduce**

```python
from typing import TypeGuard

class E:
    def __init__(self) -> None: ...
    def __call__(self, o: object) -> TypeGuard[int]:
        return True

x = object()

if E()(x):
    reveal_type(x)
```

or

```python
from typing_extensions import TypeIs

class E:
    def __init__(self) -> None: ...
    def __call__(self, o: object) -> TypeIs[int]:
        return True

x = object()

if E()(x):
    reveal_type(x)
```

**Expected Behavior**

This should return:

```
main.py:11: note: Revealed type is "builtins.int"
```

**Actual Behavior**

Actually returns

```
main.py:11: note: Revealed type is "builtins.object"
```

**Working Versions**

Note that if you define the object first it acts correctly:

```python
y = object()
o = E()
if o(y):
    reveal_type(y)
```

```
main.py:16: note: Revealed type is "builtins.int"
```

Or if it is a different method, it also works correctly:

```python
from typing import TypeGuard

class E:
    def __init__(self) -> None: ...
    def m(self, o: object) -> TypeGuard[int]:
        return True

x = object()

if E().m(x):
    reveal_type(x)
```

```
main.py:11: note: Revealed type is "builtins.int"
```


**Your Environment**

Mypy playground 1.17.0 on Python 3.12

**EDIT**: made examples smaller by removing generics



Discussion:

Patch:
diff --git a/mypy/checker.py b/mypy/checker.py
index dfbfa753d5f2..442e09b62a2c 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -6183,21 +6183,26 @@ def find_isinstance_check_helper(
                 attr = try_getting_str_literals(node.args[1], self.lookup_type(node.args[1]))
                 if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1:
                     return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0])
-            elif isinstance(node.callee, RefExpr):
-                if node.callee.type_guard is not None or node.callee.type_is is not None:
+            else:
+                type_is, type_guard = None, None
+                called_type = self.lookup_type_or_none(node.callee)
+                if called_type is not None:
+                    called_type = get_proper_type(called_type)
+                    # TODO: there are some more cases in check_call() to handle.
+                    # If the callee is an instance, try to extract TypeGuard/TypeIs from its __call__ method.
+                    if isinstance(called_type, Instance):
+                        call = find_member("__call__", called_type, called_type, is_operator=True)
+                        if call is not None:
+                            called_type = get_proper_type(call)
+                    if isinstance(called_type, CallableType):
+                        type_is, type_guard = called_type.type_is, called_type.type_guard
+
+                # If the callee is a RefExpr, extract TypeGuard/TypeIs directly.
+                if isinstance(node.callee, RefExpr):
+                    type_is, type_guard = node.callee.type_is, node.callee.type_guard
+                if type_guard is not None or type_is is not None:
                     # TODO: Follow *args, **kwargs
                     if node.arg_kinds[0] != nodes.ARG_POS:
-                        # the first argument might be used as a kwarg
-                        called_type = get_proper_type(self.lookup_type(node.callee))
-
-                        # TODO: there are some more cases in check_call() to handle.
-                        if isinstance(called_type, Instance):
-                            call = find_member(
-                                "__call__", called_type, called_type, is_operator=True
-                            )
-                            if call is not None:
-                                called_type = get_proper_type(call)
-
                         # *assuming* the overloaded function is correct, there's a couple cases:
                         #  1) The first argument has different names, but is pos-only. We don't
                         #     care about this case, the argument must be passed positionally.
@@ -6210,9 +6215,7 @@ def find_isinstance_check_helper(
                                 # we want the idx-th variable to be narrowed
                                 expr = collapse_walrus(node.args[idx])
                             else:
-                                kind = (
-                                    "guard" if node.callee.type_guard is not None else "narrower"
-                                )
+                                kind = "guard" if type_guard is not None else "narrower"
                                 self.fail(
                                     message_registry.TYPE_GUARD_POS_ARG_REQUIRED.format(kind), node
                                 )
@@ -6223,15 +6226,15 @@ def find_isinstance_check_helper(
                         # considered "always right" (i.e. even if the types are not overlapping).
                         # Also note that a care must be taken to unwrap this back at read places
                         # where we use this to narrow down declared type.
-                        if node.callee.type_guard is not None:
-                            return {expr: TypeGuardedType(node.callee.type_guard)}, {}
+                        if type_guard is not None:
+                            return {expr: TypeGuardedType(type_guard)}, {}
                         else:
-                            assert node.callee.type_is is not None
+                            assert type_is is not None
                             return conditional_types_to_typemaps(
                                 expr,
                                 *self.conditional_types_with_intersection(
                                     self.lookup_type(expr),
-                                    [TypeRange(node.callee.type_is, is_upper_bound=False)],
+                                    [TypeRange(type_is, is_upper_bound=False)],
                                     expr,
                                     consider_runtime_isinstance=False,
                                 ),


Test patch:
diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test
index fdcfcc969adc..93e665e4548c 100644
--- a/test-data/unit/check-typeguard.test
+++ b/test-data/unit/check-typeguard.test
@@ -731,6 +731,53 @@ assert a(x=x)
 reveal_type(x)  # N: Revealed type is "builtins.int"
 [builtins fixtures/tuple.pyi]
 
+# https://github.com/python/mypy/issues/19575
+[case testNoCrashOnDunderCallTypeGuardTemporaryObject]
+from typing_extensions import TypeGuard
+class E:
+    def __init__(self) -> None: ...
+    def __call__(self, o: object) -> TypeGuard[int]:
+        return True
+x = object()
+if E()(x):
+    reveal_type(x)  # N: Revealed type is "builtins.int"
+[builtins fixtures/tuple.pyi]
+
+[case testNoCrashOnDunderCallTypeIsTemporaryObject]
+from typing_extensions import TypeIs
+class E:
+    def __init__(self) -> None: ...
+    def __call__(self, o: object) -> TypeIs[int]:
+        return True
+x = object()
+if E()(x):
+    reveal_type(x)  # N: Revealed type is "builtins.int"
+[builtins fixtures/tuple.pyi]
+
+[case testNoCrashOnDunderCallTypeIsTemporaryObjectGeneric]
+from typing import Generic, TypeVar
+from typing_extensions import TypeIs
+T = TypeVar("T")
+class E(Generic[T]):
+    def __init__(self) -> None: ...
+    def __call__(self, o: object) -> TypeIs[T]:
+        return True
+x = object()
+if E[int]()(x):
+    reveal_type(x)  # N: Revealed type is "builtins.int"
+[builtins fixtures/tuple.pyi]
+
+[case testTypeGuardTemporaryObjectWithKeywordArg]
+from typing_extensions import TypeGuard
+class E:
+    def __init__(self) -> None: ...
+    def __call__(self, o: object) -> TypeGuard[int]:
+        return True
+x = object()
+if E()(o=x):
+    reveal_type(x)  # N: Revealed type is "builtins.int"
+[builtins fixtures/tuple.pyi]
+
 [case testTypeGuardRestrictAwaySingleInvariant]
 from typing import List
 from typing_extensions import TypeGuard