Skip to content

BUG: fix a number of issues around iterators and StringDType - #32156

Merged
ngoldbaum merged 5 commits into
numpy:mainfrom
ngoldbaum:fix-finalize-descr-syncing
Aug 8, 2026
Merged

ngoldbaum merged 5 commits into
numpy:mainfrom
ngoldbaum:fix-finalize-descr-syncing

Conversation

@ngoldbaum

Copy link
Copy Markdown
Member

PR summary

Fixes #32153. Also fixes a number of other issues that are caused by the same problem: array creation can replace descriptors. The iterator code in particular had a number of spots that didn't handle this case.

I added a new NPY_DT_has_finalize predicate to branch based on whether or not there is a finalize_descr implementation, which might replace dtype instances.

I also added a new npy_resync_finalized_descr, which does the necessary reference bookkeeping when a descriptor has been replaced.

Also deletes an assert in the np.concatenate implementation that could trigger for StringDType if a descriptor is replaced.

There are also a few minor unrelated C error checking fixes included as well.

AI Disclosure

An AI model found the issue and I used it to help develop and debug the fix and new tests.

@ngoldbaum ngoldbaum added 00 - Bug 09 - Backport-Candidate PRs tagged should be backported component: numpy.strings String dtypes and functions labels Jul 30, 2026
@ngoldbaum
ngoldbaum requested a review from ikrommyd August 5, 2026 19:42
@ngoldbaum

Copy link
Copy Markdown
Member Author

@ikrommyd I requested for you to give this a look but feel free to ping me if you're unsure about details and want me to walk you through this

@ngoldbaum

Copy link
Copy Markdown
Member Author

@MaanasArora any chance you can give this one a look? Thanks!!

@ikrommyd ikrommyd left a comment

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.

Looks overall good to me, left a few comments though

NPY_OP_ITFLAG_READ) &&
PyArray_NDIM(op[iop]) == 0) {
PyArray_NDIM(op[iop]) == 0 &&
!NPY_DT_has_finalize(NPY_DTYPE(op_dtype[iop]))) {

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.

Why doesn't this branch resync as well? Reproducer by claude below:

import numpy as np
s = np.array("Z" * 30, dtype="T")
dt = np.dtypes.StringDType()
np.empty(1, dtype=dt)   # attach dt, so a temp created from it gets a replaced descriptor
it = np.nditer([s], flags=["refs_ok"], op_dtypes=[dt], casting="unsafe")
str(next(it))

raises TypeError: Iterator operand required copying or buffering, but neither copying nor buffering was enabled. Wouldn't resyncing fix that?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes it should. After adding resyncing here I also realized that the fancy-index assignment was also subtly broken, see the change at line 2148 of array_assign_subscript.

Comment thread numpy/_core/src/umath/ufunc_object.c Outdated
Comment on lines +3077 to +3084
/*
* descrs[0] and descrs[2] both describe op[0], which is used as both
* the previous-value input and the output (see `dataptr_copy` below).
*/
npy_resync_finalized_descr(&descrs[0], op[0]);
npy_resync_finalized_descr(&descrs[1], op[1]);
npy_resync_finalized_descr(&descrs[2], op[0]);

@ikrommyd ikrommyd Aug 7, 2026 •

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.

This is in PyUFunc_Accumulate. Does PyUFunc_Reduce (probably inside PyUFunc_ReduceWrapper) or others like reduceat etc need the same treatment?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I added some code to re-sync the ArrayMethod contexts in PyUFunc_ReduceWrapper. It can't use this pattern of the new NpyIter_GetArrayMethodContext I also added because reductions alias descriptors. However I could use NpyIter_GetArrayMethodContext in PyUFunc_GeneralizedFunctionInternal and execute_ufunc_loop where it nicely simplifies things.

Comment on lines 638 to 639

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 did not look at this unseen code in the diff but fable mentioned that these two should probably be reversed such that NpyIter_Deallocate should see that PyErr_Occurred since it branches on it in line 657 now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Missed this comment on my first pass but I just pushed a fix in the last commit.

assert_array_equal(out, a)


def test_nditer_copy_and_writeback():

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.

it.copy() is not called tested in this test

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

See test_nditer_distinct_allocators, unless you were thinking of something different

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.

The ufunc.accumulate changes are untested.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

see test_reduce_distinct_allocators

@ngoldbaum
ngoldbaum requested a review from ikrommyd August 7, 2026 19:48
@ngoldbaum

Copy link
Copy Markdown
Member Author

@mhvk you may be interested in this too. The bulk of the PR is tests and the C changs are all in the guts of the iterator internals. If we get one more review on this I think we can merge this in time for the bugfix release this weekend.

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yikes, this really gets into the weeds. Two possible relevant queries, two suggestions that can be ignored...

if (PyArray_GetDTypeTransferFunction(
1, itemsize, itemsize,
descr, PyArray_DESCR(self),
NpyIter_GetDescrArray(mit->outer)[mit->num_fancy],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can this never happen in the subspace_iter above?

@MaanasArora MaanasArora Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FWIW, seems that the subspace path in PyArray_MapIterNew replaces descr (in this case PyArray_DESCR(self)), so hopefully not!

extra_op = (PyArrayObject *)PyArray_NewFromDescr(&PyArray_Type,
extra_op_dtype,
mit->nd_fancy + PyArray_NDIM(subspace),
mit->dimensions, strides,
NULL, 0, NULL);
if (extra_op == NULL) {
goto fail;
}
// extra_op_dtype might have been replaced, so get a new reference
extra_op_dtype = PyArray_DESCR(extra_op);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, subspace_iter is unbuffered so the descriptors match. I added StringDType tests for this though.

Comment thread numpy/_core/src/umath/reduction.c Outdated

/*
* op_dtypes tracks the loop's descriptors so the initial-value buffers
* use the same ones. The context must not outlive `iter`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe "...not outlive iter, as we are filled with borrowed references" (maybe, "like for PyIter_GetArrayMethodContext")
Also perhap: "Reminder: inputs have nout inputs that hold results
and one input that provides the values, i.e., nin = nout+1"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added.

Comment thread numpy/_core/src/umath/ufunc_object.c Outdated
}
}

/*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can anything happen to descrs outside of the iterator block? And even then, what can happen to 0 and 1? If not, I'd prefer moving it there (and have assert here if you wish). But I may be missing something.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right, descrs was used for two different purposes. I gave the loiop a separate borrowed descriptor array built from the final operands, which resolves the need to resync here.

@ikrommyd ikrommyd left a comment

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.

All good from my side, I don't see anything else. There's just the remaining comments from Marten left.
Some conflicts may come from this PR to my reduction loops in reduceat and accumulate PRs but shouldn't be anything particularly annoying.

@MaanasArora

Copy link
Copy Markdown
Contributor

Sorry, could only get through a brief pass, this gets very deep! But looks good to me.

@ngoldbaum

Copy link
Copy Markdown
Member Author

Thanks all! Marten pointed out something that let me simplify this even more.

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks all OK now!

@ngoldbaum

Copy link
Copy Markdown
Member Author

@charris just in case there's still time for one more backport, I'm merging this

@ngoldbaum
ngoldbaum merged commit 87310db into numpy:main Aug 8, 2026
91 checks passed
@charris

charris commented Aug 8, 2026

Copy link
Copy Markdown
Member

Looking at the backport, the remaining diff of descriptor.c with main is:

diff --git a/numpy/_core/src/multiarray/descriptor.c b/numpy/_core/src/multiarray/descriptor.c
index ddb61a5912..7e82a702a7 100644
--- a/numpy/_core/src/multiarray/descriptor.c
+++ b/numpy/_core/src/multiarray/descriptor.c
@@ -526,8 +526,9 @@ _convert_from_array_descr(PyObject *obj, int align)
                     "Field elements must be tuples with at most 3 elements, got '%R'", item);
             goto fail;
         }
-        if (_reject_unsupported_field_dtype(conv) < 0) {
-            Py_DECREF(conv);
+        if (PyObject_IsInstance((PyObject *)conv, (PyObject *)&PyArray_StringDType)) {
+            PyErr_Format(PyExc_TypeError,
+                         "StringDType is not currently supported for structured dtype fields.");
             goto fail;
         }
         if ((PyDict_GetItemWithError(fields, name) != NULL) // noqa: borrowed-ref OK
@@ -670,10 +671,6 @@ _convert_from_list(PyObject *obj, int align)
         if (conv == NULL) {
             goto fail;
         }
-        if (_reject_unsupported_field_dtype(conv) < 0) {
-            Py_DECREF(conv);
-            goto fail;
-        }
         dtypeflags |= (conv->flags & NPY_FROM_FIELDS);
         if (align) {
             int _align = conv->alignment;
@@ -1167,12 +1164,6 @@ _convert_from_dict(PyObject *obj, int align)
             Py_DECREF(ind);
             goto fail;
         }
-        if (_reject_unsupported_field_dtype(newdescr) < 0) {
-            Py_DECREF(newdescr);
-            Py_DECREF(tup);
-            Py_DECREF(ind);
-            goto fail;
-        }
         PyTuple_SET_ITEM(tup, 0, (PyObject *)newdescr);
         int _align = 1;
         if (align) {
@@ -1638,9 +1629,7 @@ _convert_from_type(PyObject *obj) {
     else {
         PyObject *DType = PyArray_DiscoverDTypeFromScalarType(typ);
         if (DType != NULL) {
-            PyArray_Descr *ret = PyArray_GetDefaultDescr((PyArray_DTypeMeta *)DType);
-            Py_DECREF(DType);
-            return ret;
+            return PyArray_GetDefaultDescr((PyArray_DTypeMeta *)DType);
         }
         PyArray_Descr *ret = _try_convert_from_dtype_attr(obj);
         if ((PyObject *)ret != Py_NotImplemented) {

Note that _reject_unsupported_field_dtype is now defined, but unused in the 2.5.x branch. I can either remove it, or bring in the rest of descriptor.c from main.

@charris

charris commented Aug 8, 2026

Copy link
Copy Markdown
Member

I am tempted to bring down the whole descriptor.c from main, the additional changes seem largely independent and unlikely to cause trouble.

@ngoldbaum

Copy link
Copy Markdown
Member Author

@charris I'd drop the descriptor.c changes from the backport and remove _reject_unsupported_field_dtype. Those are only there to support #32027, which probably shouldn't be backported. Although that said, I could see a case for backporting #32027 too since it fixes possible crashes. It also makes things that happened to work no longer work, so I erred on the side of not marking it for backport.

@charris

charris commented Aug 8, 2026

Copy link
Copy Markdown
Member

OK, I'll leave descriptor.c unchanged as everything there looks #32027 related.

@charris

charris commented Aug 8, 2026

Copy link
Copy Markdown
Member

There is one leak fix, however, I'll put that back in.

@charris

charris commented Aug 8, 2026

Copy link
Copy Markdown
Member

There are also problems with PyUFunc_ReduceWrapper, whose signature has also changed in main so I don't trust the changes in the backport. I think the best option is to not backport this, main has diverged too much from 2.5.x.

@ngoldbaum

Copy link
Copy Markdown
Member Author

Fair enough

@ikrommyd

ikrommyd commented Aug 9, 2026 •

Copy link
Copy Markdown
Member

I am responsible for the changes in PyUFunc_ReduceWrapper in main. They should all be due to the reduction loops introduction. I could backport this PR safely if you want for the next 2.5.x release. When it comes to reductions, all Nathan's code needs is just to be collapsed down to 1-output reductions instead of N-output reductions. The signature change shouldn't affect anything as the change was from PyArrayObject -> PyObject just to cover the tuple of arrays the N-output reductions return.

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

Labels

00 - Bug component: numpy.strings String dtypes and functions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: segfault in stringdtype fancy-index array assignment

5 participants