Skip to content

Commit 3b02418

Browse files
eendebakptseberg
andauthored
DEP: Deprecate setting the dtype of an numpy.ndarray (#29575)
Co-authored-by: Sebastian Berg <sebastian@sipsolutions.net>
1 parent af6be5d commit 3b02418

12 files changed

Lines changed: 81 additions & 24 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Setting the ``dtype`` attribute is deprecated
2+
---------------------------------------------
3+
Setting the dtype attribute is now deprecated since mutating
4+
an array is unsafe if an array is shared, especially by multiple
5+
threads. As an alternative, you can create a view with a new dtype
6+
via `array.view(dtype=new_dtype)`.

numpy/_core/records.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,11 @@ def __setattr__(self, attr, val):
455455

456456
newattr = attr not in self.__dict__
457457
try:
458-
ret = object.__setattr__(self, attr, val)
458+
if attr == 'dtype':
459+
# gh-29244
460+
ret = self._set_dtype(val)
461+
else:
462+
ret = object.__setattr__(self, attr, val)
459463
except Exception:
460464
fielddict = ndarray.__getattribute__(self, 'dtype').fields or {}
461465
if attr not in fielddict:

numpy/_core/src/multiarray/getset.c

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -341,16 +341,11 @@ array_nbytes_get(PyArrayObject *self, void *NPY_UNUSED(ignored))
341341
* (contiguous or fortran) with compatible dimensions The shape and strides
342342
* will be adjusted in that case as well.
343343
*/
344-
static int
345-
array_descr_set(PyArrayObject *self, PyObject *arg, void *NPY_UNUSED(ignored))
344+
NPY_NO_EXPORT int
345+
array_descr_set_internal(PyArrayObject *self, PyObject *arg)
346346
{
347347
PyArray_Descr *newtype = NULL;
348-
349-
if (arg == NULL) {
350-
PyErr_SetString(PyExc_AttributeError,
351-
"Cannot delete array dtype");
352-
return -1;
353-
}
348+
assert(arg);
354349

355350
if (!(PyArray_DescrConverter(arg, &newtype)) ||
356351
newtype == NULL) {
@@ -496,6 +491,43 @@ array_descr_set(PyArrayObject *self, PyObject *arg, void *NPY_UNUSED(ignored))
496491
return -1;
497492
}
498493

494+
static int
495+
non_unique_reference(PyObject *lhs)
496+
{
497+
// Return 1 if we have a guaranteed non-unique reference
498+
// When 0 is returned, the object can be unique or non-unique
499+
#if defined(PYPY_VERSION)
500+
// on pypy we cannot use reference counting
501+
return 0;
502+
#endif
503+
return Py_REFCNT(lhs) > 1;
504+
}
505+
506+
static int
507+
array_descr_set(PyArrayObject *self, PyObject *arg)
508+
{
509+
if (arg == NULL) {
510+
PyErr_SetString(PyExc_AttributeError,
511+
"Cannot delete array dtype");
512+
return -1;
513+
}
514+
515+
if (non_unique_reference((PyObject *)self)) {
516+
// this will not emit deprecation warnings for all cases, but for most it will
517+
// we skip unique references, so that we will not get a deprecation warning
518+
// when array.view(new_dtype) is called
519+
/* DEPRECATED 2026-02-06, NumPy 2.5 */
520+
int ret = PyErr_WarnEx(PyExc_DeprecationWarning,
521+
"Setting the dtype on a NumPy array has been deprecated in NumPy 2.4.\n"
522+
"Instead of changing the dtype on an array x, create a new array with x.view(new_dtype)",
523+
1);
524+
if (ret) {
525+
return -1;
526+
}
527+
}
528+
return array_descr_set_internal(self, arg);
529+
}
530+
499531
static PyObject *
500532
array_struct_get(PyArrayObject *self, void *NPY_UNUSED(ignored))
501533
{

numpy/_core/src/multiarray/getset.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
extern NPY_NO_EXPORT PyGetSetDef array_getsetlist[];
55

6+
NPY_NO_EXPORT int array_descr_set_internal(PyArrayObject *self, PyObject *arg);
67
NPY_NO_EXPORT int array_shape_set_internal(PyArrayObject *self, PyObject *val);
78

89
#endif /* NUMPY_CORE_SRC_MULTIARRAY_GETSET_H_ */

numpy/_core/src/multiarray/methods.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2556,6 +2556,15 @@ array_trace(PyArrayObject *self,
25562556

25572557
#undef _CHKTYPENUM
25582558

2559+
static PyObject* array__set_dtype(PyObject *self, PyObject *args)
2560+
{
2561+
int r = array_descr_set_internal((PyArrayObject *)self, args);
2562+
2563+
if (r < 0) {
2564+
return NULL;
2565+
}
2566+
Py_RETURN_NONE;
2567+
}
25592568

25602569
static PyObject *
25612570
array_clip(PyArrayObject *self,
@@ -3057,6 +3066,9 @@ NPY_NO_EXPORT PyMethodDef array_methods[] = {
30573066
{"to_device",
30583067
(PyCFunction)array_to_device,
30593068
METH_VARARGS | METH_KEYWORDS, NULL},
3060-
3069+
// For deprecation of ndarray setters
3070+
{"_set_dtype",
3071+
(PyCFunction)array__set_dtype,
3072+
METH_O, NULL},
30613073
{NULL, NULL, 0, NULL} /* sentinel */
30623074
};

numpy/_core/tests/test_deprecations.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import numpy as np
1313
from numpy._core._multiarray_tests import fromstring_null_term_c_api # noqa: F401
14-
from numpy.testing import assert_raises
14+
from numpy.testing import IS_PYPY, assert_raises
1515

1616

1717
class _DeprecationTestCase:
@@ -250,6 +250,11 @@ def test_deprecated_strides_set(self):
250250
x = np.eye(2)
251251
self.assert_deprecated(setattr, args=(x, 'strides', x.strides))
252252

253+
@pytest.mark.skipif(IS_PYPY, reason="PyPy handles refcounts differently")
254+
def test_deprecated_dtype_set(self):
255+
x = np.eye(2)
256+
self.assert_deprecated(setattr, args=(x, "dtype", int))
257+
253258
def test_deprecated_shape_set(self):
254259
x = np.eye(2)
255260
self.assert_deprecated(setattr, args=(x, "shape", (4, 1)))

numpy/_core/tests/test_dtype.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pickle
77
import sys
88
import types
9+
import warnings
910
from itertools import permutations
1011
from typing import Any
1112

@@ -1116,7 +1117,10 @@ def test_zero_stride(self):
11161117
arr = np.broadcast_to(arr, 10)
11171118
assert arr.strides == (0,)
11181119
with pytest.raises(ValueError):
1119-
arr.dtype = "i1"
1120+
with warnings.catch_warnings(): # gh-28901
1121+
warnings.filterwarnings(action="ignore",
1122+
category=DeprecationWarning)
1123+
arr.dtype = "i1"
11201124

11211125
class TestDTypeMakeCanonical:
11221126
def check_canonical(self, dtype, canonical):

numpy/_core/tests/test_numerictypes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ class TestReadValuesNestedMultiple(ReadValuesNested):
335335
class TestEmptyField:
336336
def test_assign(self):
337337
a = np.arange(10, dtype=np.float32)
338-
a.dtype = [("int", "<0i4"), ("float", "<2f4")]
338+
a = a.view(dtype=[("int", "<0i4"), ("float", "<2f4")])
339339
assert_(a['int'].shape == (5, 0))
340340
assert_(a['float'].shape == (5, 2))
341341

numpy/lib/_stride_tricks_impl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def as_strided(
140140
array = np.asarray(DummyArray(interface, base=base))
141141
# The route via `__interface__` does not preserve structured
142142
# dtypes. Since dtype should remain unchanged, we set it explicitly.
143-
array.dtype = base.dtype
143+
array._set_dtype(base.dtype)
144144

145145
view = _maybe_view_as_subclass(base, array)
146146

numpy/ma/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3495,7 +3495,7 @@ def dtype(self):
34953495

34963496
@dtype.setter
34973497
def dtype(self, dtype):
3498-
super(MaskedArray, type(self)).dtype.__set__(self, dtype)
3498+
self._set_dtype(dtype)
34993499
if self._mask is not nomask:
35003500
self._mask = self._mask.view(make_mask_descr(dtype), ndarray)
35013501
# Try to reset the shape of the mask (if we don't have a void).

0 commit comments

Comments
 (0)