Skip to content

Commit

Permalink
DEPR: Series setitem/getitem treating ints as positional
Browse files Browse the repository at this point in the history
  • Loading branch information
jbrockmendel committed Mar 30, 2024
1 parent c468028 commit d90bfbb
Show file tree
Hide file tree
Showing 11 changed files with 76 additions and 218 deletions.
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 @@ -206,6 +206,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 passing a pandas type to :meth:`Index.view` (:issue:`55709`)
- Removed "freq" keyword from :class:`PeriodArray` constructor, use "dtype" instead (:issue:`52462`)
- Removed deprecated "method" and "limit" keywords from :meth:`Series.replace` and :meth:`DataFrame.replace` (:issue:`53492`)
Expand Down
89 changes: 5 additions & 84 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,19 +899,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 @@ -956,35 +946,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 @@ -1074,27 +1035,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 @@ -1153,28 +1095,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

0 comments on commit d90bfbb

Please sign in to comment.