Repository: pylint-dev/pylint

Issue:
Inconsistency between ``is``/``is not`` and ``==``/``!=`` when comparing types for ``unidiomatic-typecheck``
### Bug description

I think that pylint should detect the case when `type()` is used to compare two types, in which case IMHO `isisntance()` should not be used:


```python
if type(a) == type(b):
    ...
```

### Configuration

```ini

```

### Command used

```shell
pylint
```

### Pylint output

```
C0123: Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck)
```

### Expected behavior

No error triggered.

### Pylint version

```shell
pylint 3.0.3
astroid 3.0.2
Python 3.12.3 (main, Nov  6 2024, 18:32:19) [GCC 13.2.0]
```

### OS / Environment

_No response_

### Additional dependencies

```python

```


Discussion:
thank you for highlighting this case. currently, no message is raised when the `is` operator is used to compare two types:
```
type(a) is type(b)
```
and imo this seems like an inconsistency as both cases (`type()` comparisons with `==` and `is`) should be semantically equivalent in most practical scenarios.
I like the idea of encouraging `is`, so I'm actually surprised we don't have some other message for discouraging `==` here.
Hi,

I'd like to work on fixing this issue. Could you confirm whether the behavior described in the table below reflects the expected behavior? (Perhaps add a new message suggesting the use of 'is' instead of '==').
| Code Example                           | Current Behavior            | Proposed Behavior                                      |
|----------------------------------------|-----------------------------|-------------------------------------------------------|
| `type(a) == type(b):`               | `C0123` raised              | Detect and suggest using `is` instead of `==`.       |
| `type(a) is type(b):`               | `C0123` raised                  | No message.                          |
| `type(a) == int:`                   | `C0123` raised                  | Detect and suggest using `is` instead of `==`.                               |
| `type(a) is int:`                   | `C0123` raised                  | No message.                              |
| `isinstance(a, type(b)):`           | No message                  | No message (no change).                              |


Thanks for expressing interest, @nedelcu-ioan. A bit of context about this issue, OP is referring to a "strict" type comparison here. In most cases, we want to use the idiomatic way `isinstance` to perform type checks, which accounts for inheritance. i.e. Given that `class C extends class B`, `isinstance(C(), B)` returns `True`.

But there may be specific cases where we want to perform a "strict" comparison to check if both types are strictly equal, that's when `==` and `is` should be used over `isinstance`.

That's why we should maintain current behavior for all cases except for `type(a) == type(b)` - which should not raise `C0123`. @jacobtylerwalls suggests that we should raise a different message (in another check) for `type(a) == type(b)`, because `is` is safer than `==` in this scenario, where `==` behavior can be overridden.

```
type(a) == type(b)  # do not raise C0123, and perhaps suggest using "is" instead of "==" in another check
type(a) is type(b)  # do not raise C0123 (no change in behavior)
type(a) == int  # raise C0123 (no change in behavior)
type(a) is int  # raise C0123 (no change in behavior)
isinstance(a, type(b))  # do not raise C0123 (no change in behavior)
```
Agree with @zenlyj's analysis. Sorry if my tangent was distracting. I'd be interested to explore expanding `comparison-with-callable` to include classes (constructors), not just functions, but we'd need to look at how disruptive that would be and should probably be discussed in a separate issue.



Patch:
diff --git a/doc/whatsnew/fragments/10161.false_positive b/doc/whatsnew/fragments/10161.false_positive
new file mode 100644
index 0000000000..dc343e05ae
--- /dev/null
+++ b/doc/whatsnew/fragments/10161.false_positive
@@ -0,0 +1,3 @@
+Comparisons between two calls to `type()` won't raise an ``unidiomatic-typecheck`` warning anymore, consistent with the behavior applied only for ``==`` previously.
+
+Closes #10161
diff --git a/pylint/checkers/base/comparison_checker.py b/pylint/checkers/base/comparison_checker.py
index 353f74383e..a2c5b10f11 100644
--- a/pylint/checkers/base/comparison_checker.py
+++ b/pylint/checkers/base/comparison_checker.py
@@ -323,18 +323,13 @@ def _check_unidiomatic_typecheck(self, node: nodes.Compare) -> None:
         if operator in TYPECHECK_COMPARISON_OPERATORS:
             left = node.left
             if _is_one_arg_pos_call(left):
-                self._check_type_x_is_y(node, left, operator, right)
+                self._check_type_x_is_y(node=node, left=left, right=right)
             elif isinstance(left, nodes.Name) and _is_one_arg_pos_call(right):
-                self._check_type_x_is_y(
-                    node=node, left=right, operator=operator, right=left
-                )  # transforming Y == type(x) case to type(x) == Y
+                # transforming Y == type(x) case to type(x) == Y
+                self._check_type_x_is_y(node=node, left=right, right=left)
 
     def _check_type_x_is_y(
-        self,
-        node: nodes.Compare,
-        left: nodes.NodeNG,
-        operator: str,
-        right: nodes.NodeNG,
+        self, node: nodes.Compare, left: nodes.NodeNG, right: nodes.NodeNG
     ) -> None:
         """Check for expressions like type(x) == Y."""
         left_func = utils.safe_infer(left.func)
@@ -343,7 +338,7 @@ def _check_type_x_is_y(
         ):
             return
 
-        if operator in {"is", "is not"} and _is_one_arg_pos_call(right):
+        if _is_one_arg_pos_call(right):
             right_func = utils.safe_infer(right.func)
             if (
                 isinstance(right_func, nodes.ClassDef)


Test patch:
diff --git a/tests/functional/u/unidiomatic_typecheck.py b/tests/functional/u/unidiomatic_typecheck.py
index 1a87280cae..784668b8a6 100644
--- a/tests/functional/u/unidiomatic_typecheck.py
+++ b/tests/functional/u/unidiomatic_typecheck.py
@@ -61,14 +61,28 @@ def parameter_shadowing_inference_negatives(type):
     type(42) in [int]
     type(42) not in [int]
 
-def deliberate_subclass_check_negatives(b):
+
+def deliberate_subclass_check_negatives(a, b):
     type(42) is type(b)
     type(42) is not type(b)
+    type(42) == type(b)
+    type(42) != type(b)
+    type(a) is type(b)
+    type(a) is not type(b)
+    type(a) == type(b)
+    type(a) != type(b)
+
 
 def type_of_literals_positives(a):
-    type(a) is type([]) # [unidiomatic-typecheck]
-    type(a) is not type([]) # [unidiomatic-typecheck]
-    type(a) is type({}) # [unidiomatic-typecheck]
-    type(a) is not type({}) # [unidiomatic-typecheck]
-    type(a) is type("") # [unidiomatic-typecheck]
-    type(a) is not type("") # [unidiomatic-typecheck]
+    type(a) is type([])  # [unidiomatic-typecheck]
+    type(a) is not type([])  # [unidiomatic-typecheck]
+    type(a) is type({})  # [unidiomatic-typecheck]
+    type(a) is not type({})  # [unidiomatic-typecheck]
+    type(a) is type("")  # [unidiomatic-typecheck]
+    type(a) is not type("")  # [unidiomatic-typecheck]
+    type(a) == type([])  # [unidiomatic-typecheck]
+    type(a) != type([])  # [unidiomatic-typecheck]
+    type(a) == type({})  # [unidiomatic-typecheck]
+    type(a) != type({})  # [unidiomatic-typecheck]
+    type(a) == type("")  # [unidiomatic-typecheck]
+    type(a) != type("")  # [unidiomatic-typecheck]
diff --git a/tests/functional/u/unidiomatic_typecheck.txt b/tests/functional/u/unidiomatic_typecheck.txt
index 5916407bd9..76369310a6 100644
--- a/tests/functional/u/unidiomatic_typecheck.txt
+++ b/tests/functional/u/unidiomatic_typecheck.txt
@@ -10,9 +10,15 @@ unidiomatic-typecheck:16:4:16:20:simple_inference_positives:Use isinstance() rat
 unidiomatic-typecheck:17:4:17:24:simple_inference_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
 unidiomatic-typecheck:18:4:18:20:simple_inference_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
 unidiomatic-typecheck:19:4:19:20:simple_inference_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
-unidiomatic-typecheck:69:4:69:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
-unidiomatic-typecheck:70:4:70:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
-unidiomatic-typecheck:71:4:71:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
-unidiomatic-typecheck:72:4:72:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
-unidiomatic-typecheck:73:4:73:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
-unidiomatic-typecheck:74:4:74:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:77:4:77:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:78:4:78:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:79:4:79:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:80:4:80:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:81:4:81:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:82:4:82:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:83:4:83:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:84:4:84:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:85:4:85:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:86:4:86:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:87:4:87:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED
+unidiomatic-typecheck:88:4:88:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED