Repository: pandas-dev/pandas
Pull request: BUG: __eq__ raising for new arrow string dtype for incompatible objects
- [ ] closes #56008 (Replace xxxx with the GitHub issue number)
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Problem statement:
BUG: __eq__ raises NotImplementedError for new arrow string dtype
### Reproducible Example

```python
ser = pd.Series([False, True])
ser2 = pd.Series(["a", "b"], dtype="string[pyarrow_numpy]")
ser == ser2
```


### Issue Description

This shouldn't raise if we want to emulate object dtype behavior

### Expected Behavior

all False return value
cc @jorisvandenbossche thoughts?

### Installed Versions

<details>

Replace this line with the output of pd.show_versions()

</details>

Hints from issue discussion:
Regardless of exactly emulating object dtype, we should maybe ask ourselves the more general question: how does pandas handle equality comparisons of incompatible dtypes in general? (or at least with the non-arrow dtypes)

At the moment, it seems we just never raise an error, with one exception for comparing two categoricals (based on a quick experiment, didn't include every possible dtype):

```python
serieses =[
    pd.Series(["a", "b"]),
    pd.Series(["a", "b"], dtype="string"),
    pd.Series([True, False]),
    pd.Series([True, False], dtype="boolean"),
    pd.Series([1, 2]),
    pd.Series([1, 2], dtype="Int64"),
    pd.Series([0.1, 0.2]),
    pd.Series([0.1, 0.2], dtype="Float64"),
    pd.Series(pd.date_range("2012-01-01", periods=2)),
    pd.Series(pd.timedelta_range("1 days", periods=2)),
    pd.Series(pd.period_range("2012", periods=2)),
    pd.Series(['a', 'b'], dtype="category"),
    pd.Series([1, 2], dtype="category"),
]

for left in serieses:
    for right in serieses:
        try:
            left == right
        except:
            print(f"Exception for {left.dtype} and {right.dtype}")
```

gives

```
Exception for category and category
Exception for category and category
```

So if we want to keep that "rule" consistent, then I think the new default string dtype should also never raise in comparisons, but give Falses instead.
in general we follow python semantics for non-comparable dtypes: == returns all-False, != returns all-True, and inequalities raise.  A boilerplate version of this logic is in ops.invalid_comparison

Commit message: Merge branch 'main' into string_comparison

Patch to review:
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index 70039cc697b8a..cda4da9d76c42 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -604,6 +604,7 @@ Strings
 - Bug in :meth:`Series.str.find` when ``start < 0`` for :class:`ArrowDtype` with ``pyarrow.string`` (:issue:`56411`)
 - Bug in :meth:`Series.str.replace` when ``n < 0`` for :class:`ArrowDtype` with ``pyarrow.string`` (:issue:`56404`)
 - Bug in :meth:`Series.str.startswith` and :meth:`Series.str.endswith` with arguments of type ``tuple[str, ...]`` for ``string[pyarrow]`` (:issue:`54942`)
+- Bug in comparison operations for ``dtype="string[pyarrow_numpy]"`` raising if dtypes can't be compared (:issue:`56008`)
 
 Interval
 ^^^^^^^^
diff --git a/pandas/core/arrays/string_arrow.py b/pandas/core/arrays/string_arrow.py
index 32ab3054c0f51..488e1fb97c4db 100644
--- a/pandas/core/arrays/string_arrow.py
+++ b/pandas/core/arrays/string_arrow.py
@@ -41,6 +41,7 @@
     BaseStringArray,
     StringDtype,
 )
+from pandas.core.ops import invalid_comparison
 from pandas.core.strings.object_array import ObjectStringArrayMixin
 
 if not pa_version_under10p1:
@@ -662,7 +663,10 @@ def _convert_int_dtype(self, result):
         return result
 
     def _cmp_method(self, other, op):
-        result = super()._cmp_method(other, op)
+        try:
+            result = super()._cmp_method(other, op)
+        except pa.lib.ArrowNotImplementedError:
+            return invalid_comparison(self, other, op)
         if op == operator.ne:
             return result.to_numpy(np.bool_, na_value=True)
         else:
diff --git a/pandas/tests/series/test_logical_ops.py b/pandas/tests/series/test_logical_ops.py
index 153b4bfaaf444..d9c94e871bd4b 100644
--- a/pandas/tests/series/test_logical_ops.py
+++ b/pandas/tests/series/test_logical_ops.py
@@ -530,3 +530,19 @@ def test_int_dtype_different_index_not_bool(self):
 
         result = ser1 ^ ser2
         tm.assert_series_equal(result, expected)
+
+    def test_pyarrow_numpy_string_invalid(self):
+        # GH#56008
+        pytest.importorskip("pyarrow")
+        ser = Series([False, True])
+        ser2 = Series(["a", "b"], dtype="string[pyarrow_numpy]")
+        result = ser == ser2
+        expected = Series(False, index=ser.index)
+        tm.assert_series_equal(result, expected)
+
+        result = ser != ser2
+        expected = Series(True, index=ser.index)
+        tm.assert_series_equal(result, expected)
+
+        with pytest.raises(TypeError, match="Invalid comparison"):
+            ser > ser2


Code review comments:
diff_hunk: @@ -662,7 +663,10 @@ def _convert_int_dtype(self, result):
         return result
 
     def _cmp_method(self, other, op):
-        result = super()._cmp_method(other, op)
+        try:
+            result = super()._cmp_method(other, op)
+        except pa.lib.ArrowNotImplementedError:
path: pandas/core/arrays/string_arrow.py
text: @user1:
```suggestion
        except pa.ArrowNotImplementedError:
```