Skip to content

DEPR: Series setitem/getitem treating ints as positional #58089

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ Removal of prior version deprecations/changes
- :meth:`SeriesGroupBy.agg` no longer pins the name of the group to the input passed to the provided ``func`` (:issue:`51703`)
- All arguments except ``name`` in :meth:`Index.rename` are now keyword only (:issue:`56493`)
- All arguments except the first ``path``-like argument in IO writers are now keyword only (:issue:`54229`)
- Changed behavior of :meth:`Series.__getitem__` and :meth:`Series.__setitem__` to always treat integer keys as labels, never as positional, consistent with :class:`DataFrame` behavior (:issue:`50617`)
- Disallow allowing logical operations (``||``, ``&``, ``^``) between pandas objects and dtype-less sequences (e.g. ``list``, ``tuple``); wrap the objects in :class:`Series`, :class:`Index`, or ``np.array`` first instead (:issue:`52264`)
- Disallow automatic casting to object in :class:`Series` logical operations (``&``, ``^``, ``||``) between series with mismatched indexes and dtypes other than ``object`` or ``bool`` (:issue:`52538`)
- Disallow calling :meth:`Series.replace` or :meth:`DataFrame.replace` without a ``value`` and with non-dict-like ``to_replace`` (:issue:`33302`)
Expand Down
89 changes: 5 additions & 84 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,19 +901,9 @@ def __getitem__(self, key):
if isinstance(key, (list, tuple)):
key = unpack_1tuple(key)

if is_integer(key) and self.index._should_fallback_to_positional:
warnings.warn(
# GH#50617
"Series.__getitem__ treating keys as positions is deprecated. "
"In a future version, integer keys will always be treated "
"as labels (consistent with DataFrame behavior). To access "
"a value by position, use `ser.iloc[pos]`",
FutureWarning,
stacklevel=find_stack_level(),
)
return self._values[key]

elif key_is_scalar:
# Note: GH#50617 in 3.0 we changed int key to always be treated as
# a label, matching DataFrame behavior.
return self._get_value(key)

# Convert generator to list before going through hashable part
Expand Down Expand Up @@ -958,35 +948,6 @@ def _get_with(self, key):
elif isinstance(key, tuple):
return self._get_values_tuple(key)

elif not is_list_like(key):
# e.g. scalars that aren't recognized by lib.is_scalar, GH#32684
return self.loc[key]

if not isinstance(key, (list, np.ndarray, ExtensionArray, Series, Index)):
key = list(key)

key_type = lib.infer_dtype(key, skipna=False)

# Note: The key_type == "boolean" case should be caught by the
# com.is_bool_indexer check in __getitem__
if key_type == "integer":
# We need to decide whether to treat this as a positional indexer
# (i.e. self.iloc) or label-based (i.e. self.loc)
if not self.index._should_fallback_to_positional:
return self.loc[key]
else:
warnings.warn(
# GH#50617
"Series.__getitem__ treating keys as positions is deprecated. "
"In a future version, integer keys will always be treated "
"as labels (consistent with DataFrame behavior). To access "
"a value by position, use `ser.iloc[pos]`",
FutureWarning,
stacklevel=find_stack_level(),
)
return self.iloc[key]

# handle the dup indexing case GH#4246
return self.loc[key]

def _get_values_tuple(self, key: tuple):
Expand Down Expand Up @@ -1076,27 +1037,8 @@ def __setitem__(self, key, value) -> None:
except KeyError:
# We have a scalar (or for MultiIndex or object-dtype, scalar-like)
# key that is not present in self.index.
if is_integer(key):
if not self.index._should_fallback_to_positional:
# GH#33469
self.loc[key] = value
else:
# positional setter
# can't use _mgr.setitem_inplace yet bc could have *both*
# KeyError and then ValueError, xref GH#45070
warnings.warn(
# GH#50617
"Series.__setitem__ treating keys as positions is deprecated. "
"In a future version, integer keys will always be treated "
"as labels (consistent with DataFrame behavior). To set "
"a value by position, use `ser.iloc[pos] = value`",
FutureWarning,
stacklevel=find_stack_level(),
)
self._set_values(key, value)
else:
# GH#12862 adding a new key to the Series
self.loc[key] = value
# GH#12862 adding a new key to the Series
self.loc[key] = value

except (TypeError, ValueError, LossySetitemError):
# The key was OK, but we cannot set the value losslessly
Expand Down Expand Up @@ -1155,28 +1097,7 @@ def _set_with(self, key, value) -> None:
# Without this, the call to infer_dtype will consume the generator
key = list(key)

if not self.index._should_fallback_to_positional:
# Regardless of the key type, we're treating it as labels
self._set_labels(key, value)

else:
# Note: key_type == "boolean" should not occur because that
# should be caught by the is_bool_indexer check in __setitem__
key_type = lib.infer_dtype(key, skipna=False)

if key_type == "integer":
warnings.warn(
# GH#50617
"Series.__setitem__ treating keys as positions is deprecated. "
"In a future version, integer keys will always be treated "
"as labels (consistent with DataFrame behavior). To set "
"a value by position, use `ser.iloc[pos] = value`",
FutureWarning,
stacklevel=find_stack_level(),
)
self._set_values(key, value)
else:
self._set_labels(key, value)
self._set_labels(key, value)

def _set_labels(self, key, value) -> None:
key = com.asarray_tuplesafe(key)
Expand Down
11 changes: 6 additions & 5 deletions pandas/tests/copy_view/test_indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,16 +622,17 @@ def test_series_subset_set_with_indexer(backend, indexer_si, indexer):
s_orig = s.copy()
subset = s[:]

warn = None
msg = "Series.__setitem__ treating keys as positions is deprecated"
if (
indexer_si is tm.setitem
and isinstance(indexer, np.ndarray)
and indexer.dtype.kind == "i"
):
warn = FutureWarning
with tm.assert_produces_warning(warn, match=msg):
indexer_si(subset)[indexer] = 0
# In 3.0 we treat integers as always-labels
with pytest.raises(KeyError):
indexer_si(subset)[indexer] = 0
return

indexer_si(subset)[indexer] = 0
expected = Series([0, 0, 3], index=["a", "b", "c"])
tm.assert_series_equal(subset, expected)

Expand Down
9 changes: 4 additions & 5 deletions pandas/tests/extension/base/getitem.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,10 @@ def test_get(self, data):
result = s.get("Z")
assert result is None

msg = "Series.__getitem__ treating keys as positions is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
assert s.get(4) == s.iloc[4]
assert s.get(-1) == s.iloc[-1]
assert s.get(len(s)) is None
# As of 3.0, getitem with int keys treats them as labels
assert s.get(4) is None
assert s.get(-1) is None
assert s.get(len(s)) is None

# GH 21257
s = pd.Series(data)
Expand Down
12 changes: 2 additions & 10 deletions pandas/tests/indexing/test_coercion.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,8 @@ def test_setitem_index_object(self, val, exp_dtype):
obj = pd.Series([1, 2, 3, 4], index=pd.Index(list("abcd"), dtype=object))
assert obj.index.dtype == object

if exp_dtype is IndexError:
temp = obj.copy()
warn_msg = "Series.__setitem__ treating keys as positions is deprecated"
msg = "index 5 is out of bounds for axis 0 with size 4"
with pytest.raises(exp_dtype, match=msg):
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
temp[5] = 5
else:
exp_index = pd.Index(list("abcd") + [val], dtype=object)
self._assert_setitem_index_conversion(obj, val, exp_index, exp_dtype)
exp_index = pd.Index(list("abcd") + [val], dtype=object)
self._assert_setitem_index_conversion(obj, val, exp_index, exp_dtype)

@pytest.mark.parametrize(
"val,exp_dtype", [(5, np.int64), (1.1, np.float64), ("x", object)]
Expand Down
15 changes: 6 additions & 9 deletions pandas/tests/indexing/test_floats.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ def test_scalar_non_numeric(self, index, frame_or_series, indexer_sl):
],
)
def test_scalar_non_numeric_series_fallback(self, index):
# fallsback to position selection, series only
# starting in 3.0, integer keys are always treated as labels, no longer
# fall back to positional.
s = Series(np.arange(len(index)), index=index)

msg = "Series.__getitem__ treating keys as positions is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with pytest.raises(KeyError, match="3"):
s[3]
with pytest.raises(KeyError, match="^3.0$"):
s[3.0]
Expand All @@ -118,12 +118,9 @@ def test_scalar_with_mixed(self, indexer_sl):
indexer_sl(s3)[1.0]

if indexer_sl is not tm.loc:
# __getitem__ falls back to positional
msg = "Series.__getitem__ treating keys as positions is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
result = s3[1]
expected = 2
assert result == expected
# as of 3.0, __getitem__ no longer falls back to positional
with pytest.raises(KeyError, match="^1$"):
s3[1]

with pytest.raises(KeyError, match=r"^1\.0$"):
indexer_sl(s3)[1.0]
Expand Down
7 changes: 0 additions & 7 deletions pandas/tests/series/indexing/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ def test_fancy_getitem():

s = Series(np.arange(len(dti)), index=dti)

msg = "Series.__getitem__ treating keys as positions is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
assert s[48] == 48
assert s["1/2/2009"] == 48
assert s["2009-1-2"] == 48
assert s[datetime(2009, 1, 2)] == 48
Expand All @@ -57,10 +54,6 @@ def test_fancy_setitem():

s = Series(np.arange(len(dti)), index=dti)

msg = "Series.__setitem__ treating keys as positions is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
s[48] = -1
assert s.iloc[48] == -1
s["1/2/2009"] = -2
assert s.iloc[48] == -2
s["1/2/2009":"2009-06-05"] = -3
Expand Down
26 changes: 8 additions & 18 deletions pandas/tests/series/indexing/test_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,8 @@ def test_get_with_default():
assert s.get("e", "z") == "z"
assert s.get("e", "e") == "e"

msg = "Series.__getitem__ treating keys as positions is deprecated"
warn = None
if index is d0:
warn = FutureWarning
with tm.assert_produces_warning(warn, match=msg):
assert s.get(10, "z") == "z"
assert s.get(10, 10) == 10
assert s.get(10, "z") == "z"
assert s.get(10, 10) == 10


@pytest.mark.parametrize(
Expand Down Expand Up @@ -201,13 +196,10 @@ def test_get_with_ea(arr):
result = ser.get("Z")
assert result is None

msg = "Series.__getitem__ treating keys as positions is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
assert ser.get(4) == ser.iloc[4]
with tm.assert_produces_warning(FutureWarning, match=msg):
assert ser.get(-1) == ser.iloc[-1]
with tm.assert_produces_warning(FutureWarning, match=msg):
assert ser.get(len(ser)) is None
# As of 3.0, ints are treated as labels
assert ser.get(4) is None
assert ser.get(-1) is None
assert ser.get(len(ser)) is None

# GH#21257
ser = Series(arr)
Expand All @@ -216,16 +208,14 @@ def test_get_with_ea(arr):


def test_getitem_get(string_series, object_series):
msg = "Series.__getitem__ treating keys as positions is deprecated"

for obj in [string_series, object_series]:
idx = obj.index[5]

assert obj[idx] == obj.get(idx)
assert obj[idx] == obj.iloc[5]

with tm.assert_produces_warning(FutureWarning, match=msg):
assert string_series.get(-1) == string_series.get(string_series.index[-1])
# As of 3.0, ints are treated as labels
assert string_series.get(-1) is None
assert string_series.iloc[5] == string_series.get(string_series.index[5])


Expand Down
Loading