Skip to content

Commit 8d197ba

Browse files
gfyoungjreback
authored andcommitted
BUG/MAINT: Change default of inplace to False in pd.eval (#16732)
1 parent 1c37523 commit 8d197ba

File tree

4 files changed

+150
-48
lines changed

4 files changed

+150
-48
lines changed

doc/source/whatsnew/v0.21.0.txt

+48
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,52 @@ Other Enhancements
4545
Backwards incompatible API changes
4646
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4747

48+
Improved error handling during item assignment in pd.eval
49+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50+
51+
.. _whatsnew_0210.api_breaking.pandas_eval:
52+
53+
:func:`eval` will now raise a ``ValueError`` when item assignment malfunctions, or
54+
inplace operations are specified, but there is no item assignment in the expression (:issue:`16732`)
55+
56+
.. ipython:: python
57+
58+
arr = np.array([1, 2, 3])
59+
60+
Previously, if you attempted the following expression, you would get a not very helpful error message:
61+
62+
.. code-block:: ipython
63+
64+
In [3]: pd.eval("a = 1 + 2", target=arr, inplace=True)
65+
...
66+
IndexError: only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`)
67+
and integer or boolean arrays are valid indices
68+
69+
This is a very long way of saying numpy arrays don't support string-item indexing. With this
70+
change, the error message is now this:
71+
72+
.. code-block:: python
73+
74+
In [3]: pd.eval("a = 1 + 2", target=arr, inplace=True)
75+
...
76+
ValueError: Cannot assign expression output to target
77+
78+
It also used to be possible to evaluate expressions inplace, even if there was no item assignment:
79+
80+
.. code-block:: ipython
81+
82+
In [4]: pd.eval("1 + 2", target=arr, inplace=True)
83+
Out[4]: 3
84+
85+
However, this input does not make much sense because the output is not being assigned to
86+
the target. Now, a ``ValueError`` will be raised when such an input is passed in:
87+
88+
.. code-block:: ipython
89+
90+
In [4]: pd.eval("1 + 2", target=arr, inplace=True)
91+
...
92+
ValueError: Cannot operate inplace if there is no assignment
93+
4894
- Support has been dropped for Python 3.4 (:issue:`15251`)
4995
- The Categorical constructor no longer accepts a scalar for the ``categories`` keyword. (:issue:`16022`)
5096
- Accessing a non-existent attribute on a closed :class:`HDFStore` will now
@@ -79,6 +125,7 @@ Removal of prior version deprecations/changes
79125
- The ``pd.options.display.mpl_style`` configuration has been dropped (:issue:`12190`)
80126
- ``Index`` has dropped the ``.sym_diff()`` method in favor of ``.symmetric_difference()`` (:issue:`12591`)
81127
- ``Categorical`` has dropped the ``.order()`` and ``.sort()`` methods in favor of ``.sort_values()`` (:issue:`12882`)
128+
- :func:`eval` and :method:`DataFrame.eval` have changed the default of ``inplace`` from ``None`` to ``False`` (:issue:`11149`)
82129

83130

84131
.. _whatsnew_0210.performance:
@@ -145,3 +192,4 @@ Categorical
145192

146193
Other
147194
^^^^^
195+
- Bug in :func:`eval` where the ``inplace`` parameter was being incorrectly handled (:issue:`16732`)

pandas/core/computation/eval.py

+57-30
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""Top level ``eval`` module.
44
"""
55

6-
import warnings
76
import tokenize
87
from pandas.io.formats.printing import pprint_thing
98
from pandas.core.computation import _NUMEXPR_INSTALLED
@@ -148,7 +147,7 @@ def _check_for_locals(expr, stack_level, parser):
148147

149148
def eval(expr, parser='pandas', engine=None, truediv=True,
150149
local_dict=None, global_dict=None, resolvers=(), level=0,
151-
target=None, inplace=None):
150+
target=None, inplace=False):
152151
"""Evaluate a Python expression as a string using various backends.
153152
154153
The following arithmetic operations are supported: ``+``, ``-``, ``*``,
@@ -205,20 +204,40 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
205204
level : int, optional
206205
The number of prior stack frames to traverse and add to the current
207206
scope. Most users will **not** need to change this parameter.
208-
target : a target object for assignment, optional, default is None
209-
essentially this is a passed in resolver
210-
inplace : bool, default True
211-
If expression mutates, whether to modify object inplace or return
212-
copy with mutation.
213-
214-
WARNING: inplace=None currently falls back to to True, but
215-
in a future version, will default to False. Use inplace=True
216-
explicitly rather than relying on the default.
207+
target : object, optional, default None
208+
This is the target object for assignment. It is used when there is
209+
variable assignment in the expression. If so, then `target` must
210+
support item assignment with string keys, and if a copy is being
211+
returned, it must also support `.copy()`.
212+
inplace : bool, default False
213+
If `target` is provided, and the expression mutates `target`, whether
214+
to modify `target` inplace. Otherwise, return a copy of `target` with
215+
the mutation.
217216
218217
Returns
219218
-------
220219
ndarray, numeric scalar, DataFrame, Series
221220
221+
Raises
222+
------
223+
ValueError
224+
There are many instances where such an error can be raised:
225+
226+
- `target=None`, but the expression is multiline.
227+
- The expression is multiline, but not all them have item assignment.
228+
An example of such an arrangement is this:
229+
230+
a = b + 1
231+
a + 2
232+
233+
Here, there are expressions on different lines, making it multiline,
234+
but the last line has no variable assigned to the output of `a + 2`.
235+
- `inplace=True`, but the expression is missing item assignment.
236+
- Item assignment is provided, but the `target` does not support
237+
string item assignment.
238+
- Item assignment is provided and `inplace=False`, but the `target`
239+
does not support the `.copy()` method
240+
222241
Notes
223242
-----
224243
The ``dtype`` of any objects involved in an arithmetic ``%`` operation are
@@ -232,8 +251,9 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
232251
pandas.DataFrame.query
233252
pandas.DataFrame.eval
234253
"""
235-
inplace = validate_bool_kwarg(inplace, 'inplace')
236-
first_expr = True
254+
255+
inplace = validate_bool_kwarg(inplace, "inplace")
256+
237257
if isinstance(expr, string_types):
238258
_check_expression(expr)
239259
exprs = [e.strip() for e in expr.splitlines() if e.strip() != '']
@@ -245,7 +265,10 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
245265
raise ValueError("multi-line expressions are only valid in the "
246266
"context of data, use DataFrame.eval")
247267

268+
ret = None
248269
first_expr = True
270+
target_modified = False
271+
249272
for expr in exprs:
250273
expr = _convert_expression(expr)
251274
engine = _check_engine(engine)
@@ -266,28 +289,33 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
266289
eng_inst = eng(parsed_expr)
267290
ret = eng_inst.evaluate()
268291

269-
if parsed_expr.assigner is None and multi_line:
270-
raise ValueError("Multi-line expressions are only valid"
271-
" if all expressions contain an assignment")
292+
if parsed_expr.assigner is None:
293+
if multi_line:
294+
raise ValueError("Multi-line expressions are only valid"
295+
" if all expressions contain an assignment")
296+
elif inplace:
297+
raise ValueError("Cannot operate inplace "
298+
"if there is no assignment")
272299

273300
# assign if needed
274301
if env.target is not None and parsed_expr.assigner is not None:
275-
if inplace is None:
276-
warnings.warn(
277-
"eval expressions containing an assignment currently"
278-
"default to operating inplace.\nThis will change in "
279-
"a future version of pandas, use inplace=True to "
280-
"avoid this warning.",
281-
FutureWarning, stacklevel=3)
282-
inplace = True
302+
target_modified = True
283303

284304
# if returning a copy, copy only on the first assignment
285305
if not inplace and first_expr:
286-
target = env.target.copy()
306+
try:
307+
target = env.target.copy()
308+
except AttributeError:
309+
raise ValueError("Cannot return a copy of the target")
287310
else:
288311
target = env.target
289312

290-
target[parsed_expr.assigner] = ret
313+
# TypeError is most commonly raised (e.g. int, list), but you
314+
# get IndexError if you try to do this assignment on np.ndarray.
315+
try:
316+
target[parsed_expr.assigner] = ret
317+
except (TypeError, IndexError):
318+
raise ValueError("Cannot assign expression output to target")
291319

292320
if not resolvers:
293321
resolvers = ({parsed_expr.assigner: ret},)
@@ -304,7 +332,6 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
304332
ret = None
305333
first_expr = False
306334

307-
if not inplace and inplace is not None:
308-
return target
309-
310-
return ret
335+
# We want to exclude `inplace=None` as being False.
336+
if inplace is False:
337+
return target if target_modified else ret

pandas/core/frame.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -2224,21 +2224,18 @@ def query(self, expr, inplace=False, **kwargs):
22242224
else:
22252225
return new_data
22262226

2227-
def eval(self, expr, inplace=None, **kwargs):
2227+
def eval(self, expr, inplace=False, **kwargs):
22282228
"""Evaluate an expression in the context of the calling DataFrame
22292229
instance.
22302230
22312231
Parameters
22322232
----------
22332233
expr : string
22342234
The expression string to evaluate.
2235-
inplace : bool
2236-
If the expression contains an assignment, whether to return a new
2237-
DataFrame or mutate the existing.
2238-
2239-
WARNING: inplace=None currently falls back to to True, but
2240-
in a future version, will default to False. Use inplace=True
2241-
explicitly rather than relying on the default.
2235+
inplace : bool, default False
2236+
If the expression contains an assignment, whether to perform the
2237+
operation inplace and mutate the existing DataFrame. Otherwise,
2238+
a new DataFrame is returned.
22422239
22432240
.. versionadded:: 0.18.0
22442241

pandas/tests/computation/test_eval.py

+40-10
Original file line numberDiff line numberDiff line change
@@ -1311,14 +1311,6 @@ def assignment_not_inplace(self):
13111311
expected['c'] = expected['a'] + expected['b']
13121312
tm.assert_frame_equal(df, expected)
13131313

1314-
# Default for inplace will change
1315-
with tm.assert_produces_warnings(FutureWarning):
1316-
df.eval('c = a + b')
1317-
1318-
# but don't warn without assignment
1319-
with tm.assert_produces_warnings(None):
1320-
df.eval('a + b')
1321-
13221314
def test_multi_line_expression(self):
13231315
# GH 11149
13241316
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
@@ -1388,14 +1380,52 @@ def test_assignment_in_query(self):
13881380
df.query('a = 1')
13891381
assert_frame_equal(df, df_orig)
13901382

1391-
def query_inplace(self):
1392-
# GH 11149
1383+
def test_query_inplace(self):
1384+
# see gh-11149
13931385
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
13941386
expected = df.copy()
13951387
expected = expected[expected['a'] == 2]
13961388
df.query('a == 2', inplace=True)
13971389
assert_frame_equal(expected, df)
13981390

1391+
df = {}
1392+
expected = {"a": 3}
1393+
1394+
self.eval("a = 1 + 2", target=df, inplace=True)
1395+
tm.assert_dict_equal(df, expected)
1396+
1397+
@pytest.mark.parametrize("invalid_target", [1, "cat", [1, 2],
1398+
np.array([]), (1, 3)])
1399+
def test_cannot_item_assign(self, invalid_target):
1400+
msg = "Cannot assign expression output to target"
1401+
expression = "a = 1 + 2"
1402+
1403+
with tm.assert_raises_regex(ValueError, msg):
1404+
self.eval(expression, target=invalid_target, inplace=True)
1405+
1406+
if hasattr(invalid_target, "copy"):
1407+
with tm.assert_raises_regex(ValueError, msg):
1408+
self.eval(expression, target=invalid_target, inplace=False)
1409+
1410+
@pytest.mark.parametrize("invalid_target", [1, "cat", (1, 3)])
1411+
def test_cannot_copy_item(self, invalid_target):
1412+
msg = "Cannot return a copy of the target"
1413+
expression = "a = 1 + 2"
1414+
1415+
with tm.assert_raises_regex(ValueError, msg):
1416+
self.eval(expression, target=invalid_target, inplace=False)
1417+
1418+
@pytest.mark.parametrize("target", [1, "cat", [1, 2],
1419+
np.array([]), (1, 3), {1: 2}])
1420+
def test_inplace_no_assignment(self, target):
1421+
expression = "1 + 2"
1422+
1423+
assert self.eval(expression, target=target, inplace=False) == 3
1424+
1425+
msg = "Cannot operate inplace if there is no assignment"
1426+
with tm.assert_raises_regex(ValueError, msg):
1427+
self.eval(expression, target=target, inplace=True)
1428+
13991429
def test_basic_period_index_boolean_expression(self):
14001430
df = mkdf(2, 2, data_gen_f=f, c_idx_type='p', r_idx_type='i')
14011431

0 commit comments

Comments
 (0)