Skip to content

Commit 5bfa90b

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 5bfa90b

File tree

4 files changed

+49
-38
lines changed

4 files changed

+49
-38
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

+42-22
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: ``+``, ``-``, ``*``,
@@ -207,18 +206,24 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
207206
scope. Most users will **not** need to change this parameter.
208207
target : a target object for assignment, optional, default is None
209208
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.
209+
inplace : bool, default False
210+
If `target` is provided, and the expression mutates `target`, whether
211+
to modify `target` inplace. Otherwise, return a copy of `target` with
212+
the mutation.
213213
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.
214+
If `inplace=True`, but `target` cannot be modified inplace, a
215+
ValueError will be raised. Examples of targets that cannot be
216+
modified inplace are integers and strings. Examples of targets
217+
that can be modified inplace are lists and class instances.
217218
218219
Returns
219220
-------
220221
ndarray, numeric scalar, DataFrame, Series
221222
223+
Raises
224+
------
225+
ValueError : `inplace=True`, but the provided `target` could not be
226+
modified inplace.
222227
Notes
223228
-----
224229
The ``dtype`` of any objects involved in an arithmetic ``%`` operation are
@@ -232,8 +237,23 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
232237
pandas.DataFrame.query
233238
pandas.DataFrame.eval
234239
"""
235-
inplace = validate_bool_kwarg(inplace, 'inplace')
236-
first_expr = True
240+
241+
def is_modifiable(obj):
242+
target_type = type(obj).__name__
243+
return hasattr(obj, "__dict__") or target_type == "dict"
244+
245+
inplace = validate_bool_kwarg(inplace, "inplace")
246+
247+
# "Primitives" cannot be modified inplace because
248+
# they have no attributes that we can modify.
249+
#
250+
# The only exceptions are lists and dictionaries,
251+
# whose elements can be modified inplace.
252+
modifiable = is_modifiable(target)
253+
254+
if inplace and not modifiable:
255+
raise ValueError("Cannot modify the provided target 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)
@@ -272,15 +295,13 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
272295

273296
# assign if needed
274297
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
283298

299+
if not is_modifiable(env.target):
300+
raise ValueError("Cannot assign expression output to target")
301+
302+
target_modified = True
303+
304+
# Cannot assign to the target if it is not assignable.
284305
# if returning a copy, copy only on the first assignment
285306
if not inplace and first_expr:
286307
target = env.target.copy()
@@ -304,7 +325,6 @@ def eval(expr, parser='pandas', engine=None, truediv=True,
304325
ret = None
305326
first_expr = False
306327

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