diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 0c3ad0f7d8d21..706821afeb237 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -216,6 +216,7 @@ Indexing - Bug in :meth:`DataFrame.iloc` where indexing a single row on a :class:`DataFrame` with a single ExtensionDtype column gave a copy instead of a view on the underlying data (:issue:`45241`) - Bug in :meth:`Series.__setitem__` with a non-integer :class:`Index` when using an integer key to set a value that cannot be set inplace where a ``ValueError`` was raised insead of casting to a common dtype (:issue:`45070`) - Bug when setting a value too large for a :class:`Series` dtype failing to coerce to a common type (:issue:`26049`, :issue:`32878`) +- Bug in :meth:`Series.__setitem__` when setting ``boolean`` dtype values containing ``NA`` incorrectly raising instead of casting to ``boolean`` dtype (:issue:`45462`) - Bug in :meth:`Series.__setitem__` where setting :attr:`NA` into a numeric-dtpye :class:`Series` would incorrectly upcast to object-dtype rather than treating the value as ``np.nan`` (:issue:`44199`) - diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index ac017146b153a..92f3cfdc589ff 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -1997,9 +1997,7 @@ def np_can_hold_element(dtype: np.dtype, element: Any) -> Any: elif not isinstance(tipo, np.dtype): # i.e. nullable IntegerDtype; we can put this into an ndarray # losslessly iff it has no NAs - hasnas = element._mask.any() - # TODO: don't rely on implementation detail - if hasnas: + if element._hasna: raise ValueError return element @@ -2016,9 +2014,7 @@ def np_can_hold_element(dtype: np.dtype, element: Any) -> Any: elif not isinstance(tipo, np.dtype): # i.e. nullable IntegerDtype or FloatingDtype; # we can put this into an ndarray losslessly iff it has no NAs - hasnas = element._mask.any() - # TODO: don't rely on implementation detail - if hasnas: + if element._hasna: raise ValueError return element return element @@ -2047,7 +2043,12 @@ def np_can_hold_element(dtype: np.dtype, element: Any) -> Any: elif dtype.kind == "b": if tipo is not None: - if tipo.kind == "b": # FIXME: wrong with BooleanArray? + if tipo.kind == "b": + if not isinstance(tipo, np.dtype): + # i.e. we have a BooleanArray + if element._hasna: + # i.e. there are pd.NA elements + raise ValueError return element raise ValueError if lib.is_bool(element): diff --git a/pandas/tests/series/indexing/test_setitem.py b/pandas/tests/series/indexing/test_setitem.py index e7261d75dfd30..8501c7146f371 100644 --- a/pandas/tests/series/indexing/test_setitem.py +++ b/pandas/tests/series/indexing/test_setitem.py @@ -21,6 +21,7 @@ Series, Timedelta, Timestamp, + array, concat, date_range, period_range, @@ -573,6 +574,19 @@ def test_setitem_non_bool_into_bool(self, val, indexer_sli, unique): expected = Series([val, val], dtype=object, index=[1, 1]) tm.assert_series_equal(ser, expected) + def test_setitem_boolean_array_into_npbool(self): + # GH#45462 + ser = Series([True, False, True]) + values = ser._values + arr = array([True, False, None]) + + ser[:2] = arr[:2] # no NAs -> can set inplace + assert ser._values is values + + ser[1:] = arr[1:] # has an NA -> cast to boolean dtype + expected = Series(arr) + tm.assert_series_equal(ser, expected) + class SetitemCastingEquivalents: """