Skip to content

Commit ccb2b58

Browse files
committed
MAINT/BUG: Default inplace to False in pd.eval
Deprecated in 0.18.0. xref gh-11149. Also patches bug where we were improperly handling the inplace=False condition, as we were assuming that target input was non-None when that wasn't necessarily enforced.
1 parent 520f87b commit ccb2b58

File tree

4 files changed

+43
-42
lines changed

4 files changed

+43
-42
lines changed

doc/source/whatsnew/v0.21.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Removal of prior version deprecations/changes
7676

7777
- ``pd.read_excel()`` has dropped the ``has_index_names`` parameter (:issue:`10967`)
7878
- ``Categorical`` has dropped the ``.order()`` and ``.sort()`` methods in favor of ``.sort_values()`` (:issue:`12882`)
79+
- ``pd.eval`` and ``DataFrame.eval`` have changed the default of ``inplace`` from ``None`` to ``False`` (:issue:`11149`)
7980

8081

8182
.. _whatsnew_0210.performance:
@@ -138,3 +139,4 @@ Categorical
138139

139140
Other
140141
^^^^^
142+
- Bug in ``pd.eval()`` where the ``inplace`` parameter was being incorrectly handled (:issue:`16732`)

pandas/core/computation/eval.py

+36-26
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: ``+``, ``-``, ``*``,
@@ -206,19 +205,26 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
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.
208207
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.
208+
This is essentially passed into the resolver and is used when there
209+
is 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 : 1) A non-None `target` was passed in that does not support
224+
string item assignment.
225+
2) `inplace=False`, and a non-None `target` was passed in
226+
that does not support `.copy()`.
227+
222228
Notes
223229
-----
224230
The ``dtype`` of any objects involved in an arithmetic ``%`` operation are
@@ -232,8 +238,9 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
232238
pandas.DataFrame.query
233239
pandas.DataFrame.eval
234240
"""
235-
inplace = validate_bool_kwarg(inplace, 'inplace')
236-
first_expr = True
241+
242+
inplace = validate_bool_kwarg(inplace, "inplace")
243+
237244
if isinstance(expr, string_types):
238245
_check_expression(expr)
239246
exprs = [e.strip() for e in expr.splitlines() if e.strip() != '']
@@ -245,7 +252,10 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
245252
raise ValueError("multi-line expressions are only valid in the "
246253
"context of data, use DataFrame.eval")
247254

255+
ret = None
248256
first_expr = True
257+
target_modified = False
258+
249259
for expr in exprs:
250260
expr = _convert_expression(expr)
251261
engine = _check_engine(engine)
@@ -272,22 +282,23 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
272282

273283
# assign if needed
274284
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
285+
target_modified = True
283286

284287
# if returning a copy, copy only on the first assignment
285288
if not inplace and first_expr:
286-
target = env.target.copy()
289+
try:
290+
target = env.target.copy()
291+
except AttributeError:
292+
raise ValueError("Cannot return a copy of the target")
287293
else:
288294
target = env.target
289295

290-
target[parsed_expr.assigner] = ret
296+
# TypeError is most commonly raised (e.g. int, list), but you
297+
# get IndexError if you try to do this assignment on np.ndarray.
298+
try:
299+
target[parsed_expr.assigner] = ret
300+
except (TypeError, IndexError):
301+
raise ValueError("Cannot assign expression output to target")
291302

292303
if not resolvers:
293304
resolvers = ({parsed_expr.assigner: ret},)
@@ -304,7 +315,6 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
304315
ret = None
305316
first_expr = False
306317

307-
if not inplace and inplace is not None:
308-
return target
309-
310-
return ret
318+
# We want to exclude `inplace=None` as being False.
319+
if inplace is False:
320+
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

-8
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]})

0 commit comments

Comments
 (0)