Skip to content

BUG: fix "boolean value of NA is ambiguous" in iloc setitem with nullable Series RHS - #68312

Open
IMGillusion wants to merge 1 commit into
pandas-dev:mainfrom
IMGillusion:fix-62473-iloc-setitem-nullable-na
Open

IMGillusion wants to merge 1 commit into
pandas-dev:mainfrom
IMGillusion:fix-62473-iloc-setitem-nullable-na

Conversation

@IMGillusion

Copy link
Copy Markdown
Contributor

xref: #62473

Issue and Reproducible Example

Assigning a nullable (e.g. Int64) Series containing a missing value as
the right-hand side of a Series.iloc setitem raised

TypeError: boolean value of NA is ambiguous

when the RHS had a non-default index:

import pandas as pd

ser = pd.Series([4, 6, 9, None, 10, 13, 15], index=[6, 1, 5, 0, 3, 2, 4], dtype="Int64")
indices = pd.Series([6, 1, 5, 0, 3, 2, 4], dtype="int64")
values = pd.Series([4, 6, 9, None, 10, 13, 15], index=[4, 1, 2, 6, 0, 5, 3], dtype="Int64")

ser.iloc[indices] = values

Root Cause

In _coerce_to_data_and_mask (pandas/core/arrays/numeric.py), the
precision-loss check compares the positionally-coerced float value against the
original:

idx = np.nanargmax(values)
if int(values[idx]) != original[idx]:

values is positionally aligned with the input, but when the input is a
Series with a non-default index, original[idx] performs a label lookup
and can land on a missing (NA) element even when the value at position idx
is concrete. Comparing a concrete value against NA yields NA, and the if
then raises "boolean value of NA is ambiguous".

Fix

Access the original value positionally (a Series is read via .iloc;
arrays and lists are already read positionally) and skip the check when the
original value is null. np.nanargmax already guarantees values[idx] is
concrete, so the positional original is not the missing element in practice; the
null guard is defensive.

Tests

test_setitem_iloc_rhs_series_non_default_index_with_na (new, in
pandas/tests/series/indexing/test_setitem.py) reproduces the issue and
asserts the outcome is positional: a non-default-indexed RHS yields the same
result as the equivalent default-indexed RHS.


This contribution was developed with the assistance of an AI coding assistant.

Comment thread pandas/core/arrays/numeric.py Outdated
# is a Series with a non-default index, ``original[idx]`` would do
# a label lookup and can land on a missing element even when
# ``values[idx]`` is concrete (GH#62473). Access positionally.
original_idx = getattr(original, "iloc", original)[idx]

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.

instead of getattr, can you use a more explicit isinstance

Comment thread pandas/core/arrays/numeric.py Outdated
else:
idx = np.nanargmax(values)
if int(values[idx]) != original[idx]:
# `values` is positionally aligned with the input; when the input

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.

Please ask your AI to make comments "maximally concise"

@IMGillusion

Copy link
Copy Markdown
Contributor Author

Pushed 84c6bc5 to address both comments: the getattr is replaced with an explicit isinstance(original, ABCSeries) check, and the comment is trimmed to one line.

@github-actions github-actions Bot removed the Awaiting Review PR in the maintainers' court; exempt from staleness label Sep 15, 2026
Comment thread pandas/core/arrays/numeric.py Outdated
else:
original_idx = original[idx]
if (
not libmissing.checknull(original_idx)

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.

dont we know we have floats here, so we can just use np.isnan?

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.

actually, is this even reachable?

Comment thread doc/source/whatsnew/v3.1.0.rst Outdated
- Bug in setitem (e.g. :meth:`Series.iloc`) silently storing wrong values when assigning a nullable or :class:`ArrowDtype` extension array into a NumPy-dtype column that could not hold it losslessly, such as a negative value into an unsigned-integer column (``-1`` became ``18446744073709551615``) or a value overflowing a narrower float dtype (:issue:`47776`)
- Bug in setitem on nullable masked arrays (``Int64``, ``Float64``, ``boolean``, ...) silently coercing list-like values that the equivalent scalar setitem would reject — e.g. ``arr[[0]] = ['1']`` parsed the string into the numeric dtype, ``arr[[0]] = [True]`` on ``Int64``/``Float64`` coerced the boolean to ``1``, and an out-of-bounds value such as ``arr[[0]] = [1000]`` on ``Int8`` silently wrapped instead of raising (:issue:`45404`)
- Fixed bug in :meth:`DataFrame.loc` where assigning an iterable to a single cell in an ``object`` dtype column incorrectly raised a ``ValueError`` (:issue:`26333`, :issue:`57962`)
- Bug in :meth:`Series.iloc` setitem raising ``TypeError`` ("boolean value of NA is ambiguous") when the right-hand side is a nullable :class:`Series` (e.g. ``Int64``) containing a missing value with a non-default index, because the coercion compared the value positionally against the original by label (:issue:`62473`)

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.

i suspect this also addresses cases without a missing value, e.g.

ser = pd.Series([1, 2], dtype="Int64")
ser.iloc[[0, 1]] = pd.Series([1.0, 2.0], index=["a", "b"])

?

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.

if so a test would be worthwhile too

@IMGillusion

Copy link
Copy Markdown
Contributor Author

Addressed in ed45f9e. To answer the second question first: the checknull(original_idx) guard was unreachable, so I removed it rather than swap in np.isnan. idx = np.nanargmax(values) skips NaN slots, and values = np.asarray(original) keeps NaN exactly at original's NA positions — so the positionally-read original_idx is never NA and the guard could never fire. The positional read (original.iloc[idx]) is the actual fix; the guard was dead code.

@IMGillusion

Copy link
Copy Markdown
Contributor Author

Yes, confirmed — and added the test. ser.iloc[[0,1]] = pd.Series([1.0, 2.0], index=['a','b']) raises KeyError on main with no missing value: same label-vs-position root cause. ed45f9e adds a no-NA regression test and broadens the whatsnew entry to cover both the TypeError (NA present) and KeyError (no NA) shapes.

@github-actions github-actions Bot added the Stale label Oct 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has had no activity from its author for 14 days, so I've marked it stale. If you've already addressed the feedback, re-request a review (the ↻ next to the reviewer) to move it back into the review queue. If you're still working on it, push a commit, reply to a review comment, or leave a comment — here or on the linked issue — and the Stale label clears itself. Otherwise it will be closed in 7 days to keep the queue manageable. Your branch still remains, and you can ask a maintainer to reopen this PR to continue. See the contributing guide for how the pull request lifecycle works.

Note: labels update via a once-a-day job, so Stale may take up to a day to clear.

@IMGillusion

Copy link
Copy Markdown
Contributor Author

Thanks for the heads-up. The review feedback is fully addressed (84c6bc5: explicit isinstance check; ed45f9e: removed the unreachable checknull guard, added the no-NA regression test, broadened the whatsnew entry; 8502428: ruff format fix). CI is green on the current head. A re-review would be appreciated when there is time.

@github-actions github-actions Bot removed the Stale label Oct 15, 2026
@github-actions github-actions Bot added the Stale label Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has had no activity from its author for 14 days, so I've marked it stale. If you've already addressed the feedback, re-request a review (the ↻ next to the reviewer) to move it back into the review queue. If you're still working on it, push a commit, reply to a review comment, or leave a comment — here or on the linked issue — and the Stale label clears itself. Otherwise it will be closed in 7 days to keep the queue manageable. Your branch still remains, and you can ask a maintainer to reopen this PR to continue. See the contributing guide for how the pull request lifecycle works.

Note: labels update via a once-a-day job, so Stale may take up to a day to clear.

@IMGillusion

Copy link
Copy Markdown
Contributor Author

Pinging again ahead of the stale auto-close. All of jbrockmendel's review feedback from 09-14 and 09-28 is addressed: isinstance check instead of getattr, the unreachable checknull guard removed, a no-NA regression test added, the whatsnew entry broadened to cover both the NA (TypeError) and no-NA (KeyError) shapes, and the follow-up ruff-format fix (head 8502428). CI is fully green on the current head. A re-review would be appreciated when there is time.

@github-actions github-actions Bot removed the Stale label Oct 30, 2026

def test_setitem_iloc_rhs_series_non_default_index_with_na(self):
# GH#62473
# Assigning a nullable Int64 Series with an NA as the RHS of an iloc

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 this comment be any more concise?

Comment thread doc/source/whatsnew/v3.1.0.rst Outdated
- Bug in setitem (e.g. :meth:`Series.iloc`) silently storing wrong values when assigning a nullable or :class:`ArrowDtype` extension array into a NumPy-dtype column that could not hold it losslessly, such as a negative value into an unsigned-integer column (``-1`` became ``18446744073709551615``) or a value overflowing a narrower float dtype (:issue:`47776`)
- Bug in setitem on nullable masked arrays (``Int64``, ``Float64``, ``boolean``, ...) silently coercing list-like values that the equivalent scalar setitem would reject — e.g. ``arr[[0]] = ['1']`` parsed the string into the numeric dtype, ``arr[[0]] = [True]`` on ``Int64``/``Float64`` coerced the boolean to ``1``, and an out-of-bounds value such as ``arr[[0]] = [1000]`` on ``Int8`` silently wrapped instead of raising (:issue:`45404`)
- Fixed bug in :meth:`DataFrame.loc` where assigning an iterable to a single cell in an ``object`` dtype column incorrectly raised a ``ValueError`` (:issue:`26333`, :issue:`57962`)
- Bug in :meth:`Series.iloc` setitem when the right-hand side is a nullable :class:`Series` (e.g. ``Int64``) with a non-default index, raising ``TypeError`` ("boolean value of NA is ambiguous") when the right-hand side contains a missing value or ``KeyError`` otherwise, because the coercion looked the assigned values up in the original by label instead of by position (:issue:`62473`)

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.

needs to move to 3.2, also avoid discussing implementation details

# by label, which raised (or misfired) for a non-default index.
ser = pd.Series([1, 2], dtype="Int64")
result = ser.copy()
result.iloc[[0, 1]] = pd.Series([1.0, 2.0], index=["a", "b"])

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.

i think this still passes even if the assignment totally failed? could we set values that dont happen to already match whats there?

@IMGillusion
IMGillusion force-pushed the fix-62473-iloc-setitem-nullable-na branch from 1ade738 to 9ee9677 Compare November 7, 2026 05:15

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants