Skip to content

Commit 38caf90

Browse files
jbrockmendelclaude
andauthored
BUG: fix loc setitem with list of tuples on object-dtype column (#65264)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 10e1594 commit 38caf90

4 files changed

Lines changed: 37 additions & 1 deletion

File tree

doc/source/whatsnew/v3.1.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ Indexing
266266
- Bug in :meth:`DataFrame.at` raising ``TypeError`` when accessing a :class:`MultiIndex` with a partial date string on a :class:`DatetimeIndex` level (:issue:`43395`)
267267
- Bug in :meth:`DataFrame.duplicated` returning an empty :class:`Series` without the DataFrame's index when the DataFrame had no columns (:issue:`61191`)
268268
- Bug in :meth:`DataFrame.iloc` setitem raising ``AttributeError`` when assigning a :class:`Series` or :class:`Index` with a nullable EA dtype (e.g. ``Int64``, ``Float64``, ``boolean``) into a column with a NumPy dtype (:issue:`47776`)
269+
- Bug in :meth:`DataFrame.loc` raising ``ValueError`` when assigning a list of tuples to an object-dtype column with a boolean mask on a mixed-dtype DataFrame (:issue:`37629`)
269270
- Bug in :meth:`DataFrame.loc` with a :class:`MultiIndex` returning wrong results instead of raising ``KeyError`` when passing string keys for numeric index levels (:issue:`60104`)
270271
- Bug in :meth:`DataFrame.mask` with ``inplace=True`` where incorrect values were produced when ``other`` was a :class:`Series` with :class:`ExtensionArray` values (:issue:`64635`)
271272
- Bug in :meth:`DataFrame.where` and :meth:`DataFrame.mask` raising ``TypeError`` when ``cond`` is a :class:`Series` and ``axis=1`` (:issue:`58190`)

pandas/core/indexing.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2579,7 +2579,11 @@ def _setitem_with_indexer_split_path(self, indexer, value, name: str):
25792579
if isinstance(value, ABCDataFrame):
25802580
self._setitem_with_indexer_frame_value(indexer, value, name)
25812581

2582-
elif _is_2d_value(value):
2582+
elif _is_2d_value(value) and not (
2583+
isinstance(value, list)
2584+
and isinstance(value[0], tuple)
2585+
and len(value[0]) != len(ilocs)
2586+
):
25832587
self._setitem_with_indexer_2d_value(indexer, value)
25842588

25852589
elif len(ilocs) == 1 and lplane_indexer == len(value) and not is_scalar(pi):

pandas/core/internals/blocks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from pandas.core.dtypes.cast import (
4242
LossySetitemError,
4343
can_hold_element,
44+
construct_1d_object_array_from_listlike,
4445
convert_dtypes,
4546
find_result_type,
4647
np_can_hold_element,
@@ -1133,6 +1134,15 @@ def setitem(self, indexer, value) -> Block:
11331134
num_set = len(values[indexer])
11341135
casted = setitem_datetimelike_compat(values, num_set, casted)
11351136

1137+
if (
1138+
isinstance(casted, list)
1139+
and len(casted) > 0
1140+
and isinstance(casted[0], (tuple, list, np.ndarray))
1141+
):
1142+
# Prevent numpy from unpacking nested containers
1143+
# (e.g. tuples) during boolean-indexed assignment. GH#37629
1144+
casted = construct_1d_object_array_from_listlike(casted)
1145+
11361146
self = self._maybe_copy(inplace=True)
11371147
values = cast("np.ndarray", self.values.T)
11381148
if isinstance(casted, np.ndarray) and casted.ndim == 1 and len(casted) == 1:

pandas/tests/indexing/test_indexing.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,27 @@ def test_scalar_setitem_series_with_nested_value_length1(value, indexer_sli):
11421142
assert ser.loc[0] == value
11431143

11441144

1145+
def test_loc_setitem_list_of_tuples_on_object_column():
1146+
# GH#37629 - assigning list of tuples to object-dtype column
1147+
# with a boolean mask on a mixed-dtype DataFrame
1148+
df = DataFrame({"a": [1, 1, 2, 1], "b": [(1, 1, 0)] * 4})
1149+
1150+
# list of tuples matching selected row count
1151+
df.loc[df["a"] == 1, "b"] = [(0, 0, 1), (0, 0, 1), (0, 0, 1)]
1152+
expected = DataFrame(
1153+
{"a": [1, 1, 2, 1], "b": [(0, 0, 1), (0, 0, 1), (1, 1, 0), (0, 0, 1)]}
1154+
)
1155+
tm.assert_frame_equal(df, expected)
1156+
# verify tuples are preserved as tuples
1157+
assert isinstance(df["b"].iloc[0], tuple)
1158+
1159+
# doubly-nested list: [[(tuple)]] treated as 2D with 1 row x 1 col,
1160+
# tuple value broadcast to all matching rows
1161+
df2 = DataFrame({"a": [1, 1, 2, 1], "b": [(1, 1, 0)] * 4})
1162+
df2.loc[df2["a"] == 1, "b"] = [[(0, 0, 1)]]
1163+
tm.assert_frame_equal(df2, expected)
1164+
1165+
11451166
def test_object_dtype_series_set_series_element():
11461167
# GH 48933
11471168
s1 = Series(dtype="O", index=["a", "b"])

0 commit comments

Comments
 (0)