Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
43be6b7
EA attribute specifying whether copy=False is ignored
Aniketsy Nov 16, 2025
251ff22
EA attribute specifying whether copy=False is ignored
Aniketsy Nov 17, 2025
0d8bae3
Merge remote-tracking branch 'upstream/main' into fix-63040
Aniketsy Nov 20, 2025
0df38db
EA attribute specifying whether copy=False is ignored
Aniketsy Nov 20, 2025
453b25f
EA attribute specifying whether copy=False is ignored
Aniketsy Nov 22, 2025
5421db1
EA attribute specifying whether copy=False is ignored
Aniketsy Nov 22, 2025
3f6333e
EA attribute specifying whether copy=False is ignored
Aniketsy Nov 22, 2025
964a6d2
EA attribute specifying whether copy=False is ignored
Aniketsy Nov 22, 2025
1bf16a2
EA attribute specifying whether copy=False is ignored
Aniketsy Nov 22, 2025
871f164
EA attribute specifying whether copy=False is ignored
Aniketsy Dec 3, 2025
33f796f
EA attribute specifying whether copy=False is ignored
Aniketsy Dec 3, 2025
b7325fa
EA attribute specifying whether copy=False is ignored
Aniketsy Dec 3, 2025
cbf76eb
EA attribute specifying whether copy=False is ignored
Aniketsy Dec 6, 2025
5a373d5
update from support -> respect
Aniketsy Mar 18, 2026
4178f66
EA attribute specifying whether copy=False is ignored
Aniketsy Mar 19, 2026
963bd84
Update pandas/tests/extension/decimal/array.py
Aniketsy Mar 26, 2026
d0bc508
EA attribute specifying whether copy=False is ignored
Aniketsy Mar 26, 2026
e3da0bc
pass copy=copy
Aniketsy Mar 28, 2026
cdfd705
Revert xfail-test and copy=copy
Aniketsy Mar 31, 2026
9584cd1
EA attribute specifying whether copy=False is ignored
Aniketsy Apr 2, 2026
7fedd5c
EA attribute specifying whether copy=False is ignored
Aniketsy Apr 5, 2026
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
11 changes: 10 additions & 1 deletion pandas/core/arrays/_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,16 @@ def fillna(self, value, limit: int | None = None, copy: bool = True) -> Self:
new_values = self.copy()
else:
new_values = self[:]
new_values[mask] = value
if self.dtype.kind == "O" and isinstance(value, tuple):
# Ensure tuple values are treated as scalars for masked assignment.
# Otherwise NumPy may interpret the tuple as a sequence.
fill_values = np.empty(
np.asarray(mask, dtype=np.bool_).sum(), dtype=object
)
fill_values[:] = [value]
new_values[mask] = fill_values
else:
new_values[mask] = value
else:
# We validate the fill_value even if there is nothing to fill
self._validate_setitem_value(value)
Expand Down
21 changes: 15 additions & 6 deletions pandas/tests/extension/base/missing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class BaseMissingTests:
_respects_fillna_copy_false = True

def test_isna(self, data_missing):
expected = np.array([True, False])

Expand Down Expand Up @@ -123,18 +125,25 @@ def test_fillna_no_op_returns_copy(self, data):
tm.assert_extension_array_equal(result, data)

def test_fillna_readonly(self, data_missing):
fill_value = data_missing[1]
data = data_missing.copy()
data._readonly = True

# by default fillna(copy=True), then this works fine
result = data.fillna(data_missing[1])
assert result[0] == data_missing[1]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you restore this line somewhere (to check that the NAs actually got filled)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tm.assert_extension_array_equal(res_no_copy, res_copy), i think this already check, i also checked by restoring and got this failure on testing

    def test_fillna_readonly(self, data_missing):
        fill_value = data_missing[1]
        data = data_missing.copy()
        data._readonly = True

        # by default fillna(copy=True), then this works fine
        res_copy = data.fillna(fill_value, copy=True)
>       assert res_copy[0] == fill_value
               ^^^^^^^^^^^^^^^^^^^^^^^^^
E       AssertionError

pandas\tests\extension\base\missing.py:134: AssertionError

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That suggests that the fillna isn't actually working. That would indicate a real bug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've used llm to debug and updated with some changes and restored as per your suggestion.
On running locally all tests were passing

res_copy = data.fillna(fill_value, copy=True)
assert res_copy[0] == fill_value
tm.assert_extension_array_equal(data, data_missing)

# but with copy=False, this raises for EAs that respect the copy keyword
with pytest.raises(ValueError, match="Cannot modify read-only array"):
data.fillna(data_missing[1], copy=False)
tm.assert_extension_array_equal(data, data_missing)
if self._respects_fillna_copy_false:
with pytest.raises(ValueError, match="Cannot modify read-only array"):
data.fillna(fill_value, copy=False)
tm.assert_extension_array_equal(data, data_missing)
else:
# EAs that do not respect the copy keyword, copy=False is ignored
res_no_copy = data.fillna(fill_value, copy=False)
assert res_no_copy[0] == fill_value
tm.assert_extension_array_equal(res_no_copy, res_copy)
tm.assert_extension_array_equal(data, data_missing)

def test_fillna_series(self, data_missing):
fill_value = data_missing[1]
Expand Down
2 changes: 2 additions & 0 deletions pandas/tests/extension/decimal/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def data_for_grouping():


class TestDecimalArray(base.ExtensionTests):
_respects_fillna_copy_false = False

def _get_expected_exception(
self, op_name: str, obj, other
) -> type[Exception] | tuple[type[Exception], ...] | None:
Expand Down
26 changes: 11 additions & 15 deletions pandas/tests/extension/test_arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,17 @@ def data_for_twos(data):


class TestArrowArray(base.ExtensionTests):
_respects_fillna_copy_false = False

def test_fillna_readonly(self, data_missing, monkeypatch):
pa_dtype = data_missing.dtype.pyarrow_dtype
if pa.types.is_duration(pa_dtype):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its ugly but you could patch _respects_fillna_copy_false inside a context

with monkeypatch.context() as m:
m.setattr(self, "_respects_fillna_copy_false", False)
super().test_fillna_readonly(data_missing)
else:
super().test_fillna_readonly(data_missing)

def _construct_for_combine_add(self, left, right):
dtype = left.dtype

Expand Down Expand Up @@ -687,21 +698,6 @@ def test_fillna_no_op_returns_copy(self, data):
assert result is not data
tm.assert_extension_array_equal(result, data)

def test_fillna_readonly(self, data_missing):
data = data_missing.copy()
data._readonly = True

# by default fillna(copy=True), then this works fine
result = data.fillna(data_missing[1])
assert result[0] == data_missing[1]
tm.assert_extension_array_equal(data, data_missing)

# fillna(copy=False) is generally not honored by Arrow-backed array,
# but always returns new data -> same result as above
result = data.fillna(data_missing[1])
assert result[0] == data_missing[1]
tm.assert_extension_array_equal(data, data_missing)

@pytest.mark.xfail(
reason="GH 45419: pyarrow.ChunkedArray does not support views", run=False
)
Expand Down
1 change: 1 addition & 0 deletions pandas/tests/extension/test_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def data_for_grouping():

class TestIntervalArray(base.ExtensionTests):
divmod_exc = TypeError
_respects_fillna_copy_false = False

def _supports_reduction(self, ser: pd.Series, op_name: str) -> bool:
return op_name in ["min", "max"]
Expand Down
5 changes: 0 additions & 5 deletions pandas/tests/extension/test_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,11 +342,6 @@ def test_fillna_frame(self, data_missing):
# Non-scalar "scalar" values.
super().test_fillna_frame(data_missing)

@skip_nested
def test_fillna_readonly(self, data_missing):
# Non-scalar "scalar" values.
super().test_fillna_readonly(data_missing)

@skip_nested
def test_setitem_invalid(self, data, invalid_scalar):
# object dtype can hold anything, so doesn't raise
Expand Down
17 changes: 2 additions & 15 deletions pandas/tests/extension/test_sparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def data_for_compare(request):


class TestSparseArray(base.ExtensionTests):
_respects_fillna_copy_false = False

def _supports_reduction(self, obj, op_name: str) -> bool:
return True

Expand Down Expand Up @@ -237,21 +239,6 @@ def test_isna(self, data_missing):
def test_fillna_no_op_returns_copy(self, data, request):
super().test_fillna_no_op_returns_copy(data)

def test_fillna_readonly(self, data_missing):
# copy keyword is ignored by SparseArray.fillna
# -> copy=True vs False doesn't make a difference
data = data_missing.copy()
data._readonly = True

result = data.fillna(data_missing[1])
assert result[0] == data_missing[1]
tm.assert_extension_array_equal(data, data_missing)

# fillna(copy=False) is ignored -> so same result as above
result = data.fillna(data_missing[1], copy=False)
assert result[0] == data_missing[1]
tm.assert_extension_array_equal(data, data_missing)

@pytest.mark.xfail(reason="Unsupported")
def test_fillna_series(self, data_missing):
# this one looks doable.
Expand Down
23 changes: 7 additions & 16 deletions pandas/tests/extension/test_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,23 +170,14 @@ def test_fillna_no_op_returns_copy(self, data):
tm.assert_extension_array_equal(result, data)

def test_fillna_readonly(self, data_missing):
data = data_missing.copy()
data._readonly = True

# by default fillna(copy=True), then this works fine
result = data.fillna(data_missing[1])
assert result[0] == data_missing[1]
tm.assert_extension_array_equal(data, data_missing)

# fillna(copy=False) is generally not honored by Arrow-backed array,
# but always returns new data -> same result as above
if data.dtype.storage == "pyarrow":
result = data.fillna(data_missing[1])
assert result[0] == data_missing[1]
if data_missing.dtype.storage == "pyarrow":
# pyarrow-backed strings are immutable, copy=False is ignored,
# always returns a new array without raising.
self._respects_fillna_copy_false = False
else:
with pytest.raises(ValueError, match="Cannot modify read-only array"):
data.fillna(data_missing[1], copy=False)
tm.assert_extension_array_equal(data, data_missing)
# python-backed strings respect copy=False and raise on read-only.
self._respects_fillna_copy_false = True
super().test_fillna_readonly(data_missing)

def _get_expected_exception(
self, op_name: str, obj, other
Expand Down
Loading