Repository: pandas-dev/pandas (Python)

Issue:
BUG: AttributeError on method call after binary operation in eval expression
### Pandas version checks

- [x] I have checked that this issue has not already been reported.

- [x] I have confirmed this bug exists on the [latest version](https://pandas.pydata.org/docs/whatsnew/index.html) of pandas.

- [ ] I have confirmed this bug exists on the [main branch](https://pandas.pydata.org/docs/dev/getting_started/install.html#installing-the-development-version-of-pandas) of pandas.


### Reproducible Example

```python
import pandas as pd

x = pd.Series([1,2,3,5])
y = pd.Series([2,3,4])

pd.eval("(x + y).dropna()")
# raises AttributeError: 'BinOp' object has no attribute 'value'

# Note that something like pd.eval("(x.dropna() + y)") works!
```

### Issue Description

Also effects other Series methods called on the result of a binary operation, which work well if applied to one operand.

See also https://github.com/pandas-dev/pandas/issues/24670

### Expected Behavior

Should return a Series of sums with nan values removed. Generally I would expect that it should be possible to apply a Series method to the result of a binary operation as part of an eval expression.

### Installed Versions

<details>

INSTALLED VERSIONS
------------------
commit                : 0691c5cf90477d3503834d983f69350f250a6ff7
python                : 3.13.2
python-bits           : 64
OS                    : Linux
OS-release            : 6.13.7-arch1-1
Version               : #1 SMP PREEMPT_DYNAMIC Thu, 13 Mar 2025 18:12:00 +0000
machine               : x86_64
processor             : 
byteorder             : little
LC_ALL                : None
LANG                  : de_DE.UTF-8
LOCALE                : de_DE.UTF-8

pandas                : 2.2.3
numpy                 : 2.2.3
pytz                  : 2025.1
dateutil              : 2.9.0.post0
pip                   : 25.0.1
Cython                : None
sphinx                : None
IPython               : 9.0.1
adbc-driver-postgresql: None
adbc-driver-sqlite    : None
bs4                   : 4.13.3
blosc                 : None
bottleneck            : None
dataframe-api-compat  : None
fastparquet           : None
fsspec                : 2025.2.0
html5lib              : None
hypothesis            : None
gcsfs                 : None
jinja2                : 3.1.5
lxml.etree            : 5.3.1
matplotlib            : 3.10.1
numba                 : None
numexpr               : 2.10.2
odfpy                 : None
openpyxl              : None
pandas_gbq            : None
psycopg2              : None
pymysql               : None
pyarrow               : None
pyreadstat            : None
pytest                : 8.3.5
python-calamine       : None
pyxlsb                : None
s3fs                  : None
scipy                 : 1.15.2
sqlalchemy            : 2.0.38
tables                : 3.10.2
tabulate              : None
xarray                : 2025.1.2
xlrd                  : None
xlsxwriter            : None
zstandard             : None
tzdata                : 2025.1
qtpy                  : None
pyqt5                 : None

</details>



Discussion:
Thanks for reporting the issue. For now, depending on your use case, I would recommend moving the method call to outside the eval expression if possible. This works fine in the example you outlined, and would likely perform better in most circumstances. (If you need to choose the method dynamically, getattr() can handle that.)

Alternatively, you could use the default eval() function in Python, which also works with the given example, but I'd avoid it if possible due to performance and security implications.
Confirmed on main. PRs to fix are welcome.
Take
Unassign
Take



Patch:
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 2b437734a451a..2d9355c319f16 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -825,6 +825,7 @@ Other
 - Bug in :class:`DataFrame` when passing a ``dict`` with a NA scalar and ``columns`` that would always return ``np.nan`` (:issue:`57205`)
 - Bug in :class:`Series` ignoring errors when trying to convert :class:`Series` input data to the given ``dtype`` (:issue:`60728`)
 - Bug in :func:`eval` on :class:`ExtensionArray` on including division ``/`` failed with a ``TypeError``. (:issue:`58748`)
+- Bug in :func:`eval` where method calls on binary operations like ``(x + y).dropna()`` would raise ``AttributeError: 'BinOp' object has no attribute 'value'`` (:issue:`61175`)
 - Bug in :func:`eval` where the names of the :class:`Series` were not preserved when using ``engine="numexpr"``. (:issue:`10239`)
 - Bug in :func:`eval` with ``engine="numexpr"`` returning unexpected result for float division. (:issue:`59736`)
 - Bug in :func:`to_numeric` raising ``TypeError`` when ``arg`` is a :class:`Timedelta` or :class:`Timestamp` scalar. (:issue:`59944`)
diff --git a/pandas/core/computation/expr.py b/pandas/core/computation/expr.py
index 14a393b02409c..b53596fe28e70 100644
--- a/pandas/core/computation/expr.py
+++ b/pandas/core/computation/expr.py
@@ -644,7 +644,11 @@ def visit_Attribute(self, node, **kwargs):
         ctx = node.ctx
         if isinstance(ctx, ast.Load):
             # resolve the value
-            resolved = self.visit(value).value
+            visited_value = self.visit(value)
+            if hasattr(visited_value, "value"):
+                resolved = visited_value.value
+            else:
+                resolved = visited_value(self.env)
             try:
                 v = getattr(resolved, attr)
                 name = self.env.add_tmp(v)


Test patch:
diff --git a/pandas/tests/computation/test_eval.py b/pandas/tests/computation/test_eval.py
index 3c0bf6c35866c..9fed65faee896 100644
--- a/pandas/tests/computation/test_eval.py
+++ b/pandas/tests/computation/test_eval.py
@@ -2006,3 +2006,24 @@ def test_eval_float_div_numexpr():
     result = pd.eval("1 / 2", engine="numexpr")
     expected = 0.5
     assert result == expected
+
+
+def test_method_calls_on_binop():
+    # GH 61175
+    x = Series([1, 2, 3, 5])
+    y = Series([2, 3, 4])
+
+    # Method call on binary operation result
+    result = pd.eval("(x + y).dropna()")
+    expected = (x + y).dropna()
+    tm.assert_series_equal(result, expected)
+
+    # Test with other binary operations
+    result = pd.eval("(x * y).dropna()")
+    expected = (x * y).dropna()
+    tm.assert_series_equal(result, expected)
+
+    # Test with method chaining
+    result = pd.eval("(x + y).dropna().reset_index(drop=True)")
+    expected = (x + y).dropna().reset_index(drop=True)
+    tm.assert_series_equal(result, expected)