BUG: Ensure stable data address in scalar __array_interface__ by riku-sakamoto · Pull Request #30538 · numpy/numpy · GitHub
Skip to content

BUG: Ensure stable data address in scalar __array_interface__ - #30538

Merged
seberg merged 16 commits into
numpy:mainfrom
riku-sakamoto:fix_scalar_array_interface
Jan 27, 2026
Merged

seberg merged 16 commits into
numpy:mainfrom
riku-sakamoto:fix_scalar_array_interface

Conversation

@riku-sakamoto

@riku-sakamoto riku-sakamoto commented Dec 29, 2025

Copy link
Copy Markdown
Contributor

Changes

  • Implement __array_interface__ for NumPy scalar types to avoid creating a temporary array.
  • Set the read-only flag in the __array_interface__ dict for NumPy scalars.

As a consequence of this change, numpy.ctypeslib.as_ctypes now raises TypeError when NumPy scalar types are passed directly.

Closes #30354

Scalar types expose `__array_interface__` by creating a temporary array.
To ensure the address remains valid, the temporary array must be kept alive.
(See numpy#30354)
@charris charris changed the title FIX: keep a reference to the __ref value BUG: keep a reference to the __ref value Dec 29, 2025
@riku-sakamoto riku-sakamoto changed the title BUG: keep a reference to the __ref value BUG: Ensure stable data address in scalar __array_interface__ Dec 29, 2025

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.

Aren't you decrefing a pointer that has to be NULL here?

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 also indicates this error path isn't tested - might be worth adding a test!

@riku-sakamoto riku-sakamoto Dec 30, 2025

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.

Aren't you decrefing a pointer that has to be NULL here?

Thank you for catching this. you're absolutely right.
I fixed the incorrect decref.

this also indicates this error path isn't tested - might be worth adding a test!

This if branch seems to be unreachable because the array object always provides __array_interface__.
Instead of adding a direct test for this path, I improved the test coverage by:

  • increasing the number of scalar types under test
  • adding checks for unconversible types

@charris charris added the 09 - Backport-Candidate PRs tagged should be backported label Dec 29, 2025

@ngoldbaum ngoldbaum 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.

There's some code that is doing questionable things and it only happens to work because scalars are read-only. See comments inline.

Comment on lines +1793 to +1795
int _flags = flags;
_flags &= ~(NPY_ARRAY_WRITEBACKIFCOPY | NPY_ARRAY_OWNDATA);
_flags |= NPY_ARRAY_NOTSWAPPED;

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 all a no-op because _flags isn't used below. See my other comment below for speculation about why you defined this variable.


return Py_BuildValue(
"NO", PyLong_FromVoidPtr(scalar_value(self, NULL)),
((flags & PyBUF_WRITEABLE) == PyBUF_WRITEABLE) ? Py_False : Py_True);

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 are you checking PyBUF_WRITEABLE here - that isn't an ndarray flag. PyBUF_WRITEABLE is part of the CPython buffer request API and will only show up in calls directly from CPython for handling the buffer protocol, which has nothing to do with this code as far as I can see.

Looking at the implementation of array_dataptr_get I think you meant to use _flags here and not do the PyBUF_WRITEABLE check at all.

Also I don't think this check for read-only matters at all for scalars because scalars are always read-only:

>>> a = np.int32(5)
>>> a[:] = 7
Traceback (most recent call last):
  File "<python-input-3>", line 1, in <module>
    a[:] = 7
    ~^^^
TypeError: 'numpy.int32' object does not support item assignment
>>> a.data
<memory at 0x200012a6e50>
>>> a.data[:] = 7
Traceback (most recent call last):
  File "<python-input-5>", line 1, in <module>
    a.data[:] = 7
    ~~~~~~^^^
TypeError: cannot modify read-only memory

Maybe you can avoid all this messing with flags and just set statically that this is read-only.

Also why not set the other items in the __array_interface__ dict like shape etc? Seems like there's no reason not to and it should be trivial to do that for scalars. I'd follow the ndarray implementation in array_interface_get.

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.

Thank you for taking time to review my PR.

I’d like to briefly explain my original reasoning, and then align with your suggestions.

  • Why I do not set read-only flag for scalar array?

As you point out, scalars are always read-only.
However, according to the code below in ctypeslib, if the array is read-only, as_ctypes raise an error. So, I decided to follow the way flags are created in __array_struct__.

if readonly:
raise TypeError("readonly arrays unsupported")

Maybe, is it better to set flag as writable ? (Or forbid as_ctypes for scalars altogether?)

  • use of PyBUF_WRITEABLE flag

This was a clear mistake on my part. I confused ndarray's flags with buffer protocol's flags. As you pointed out, PyBUF_WRITEABLE is not appropriate here, and I will remove this entirely.

  • _flags variable

You are also right here. I intended to use _flags to check writability, but the current code does not do that correctly.
I am sorry for the confusion.

why not set the other items in the array_interface dict like shape etc? Seems like there's no reason not to and it should be trivial to do that for scalars. I'd follow the ndarray implementation in array_interface_get.

Sure! I will implement it following to array_interface_get.

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 updated the PR accordingly except for the what kind of flags to set for scalar arrays.


/* NOTE: sclar array is always readonly, but we need writable flags
* to use `ctypeslib.as_ctyps` via `__array_interface__`.
* So, we set the flag borrowing from the temporary array.

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 doesn't really work out, I think. The content of the scalar is clearly readonly (except for void ones).
It seems that __array_interface__ right now doesn't consider it to be readonly though, which is technically true, because it refers to __ref which is undocumented.

We have two options here:

  1. Document that __ref thing
  2. Change the flag to return readonly here (maybe always, also for void).

I would be tempted to do the second one but that might backfire in practice and we need to ask a few others.

In the second case, ctypes needs to work around whatever we do here. That may be just ignoring the readonly part or creating a copy explicitly.

Actually, it may make sense to simply error in ctypeslib because ctypeslib was clearly always very broken with a reasonably high chance of generating bad results without an error.

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.

Thanks for the comment!

I agree with the second option. Under this option, we would not change anything in ctypeslib.as_ctypes.
As a result, scalar arrays would no longer be usable with ctypeslib.as_ctypes, but this behavior seems more robust.

we need to ask a few others.

Is there anyone I should ping about this, or would it be appropriate to bring this topic to the next triage meeting?

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.

@mtsokol do you have any context about where your initial report about this came from?

@mtsokol mtsokol Jan 6, 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.

It appeared in our custom compiler:
https://github.com/finch-tensor/finch-tensor-lite/pull/248/files#diff-70a950803da49a25141ebb1d3bd6a6e33d417e2c3e0c42b0a34ed662bd690b2cL211 - within code meant for serializing NumPy scalars to C. We ended up using 0-d arrays here so we won't be impacted if as_ctypes stops supporting scalars.

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.

Yeah, just to be clear "asking around", I was just thinking of brining it up at the triage meeting and if there is a general consensus to try, try it (but not backport!).

I doubt we'll easily find someone who might actually be affected. The problem is that the main use-case may be something like a Cython function that uses a typed memory view (like double data[::1]) that could use a const (signalling read-only is OK), but doesn't...

And we should maybe think whether such Cython use-cases would hit this (I am not sure), because if they do, than that might be an unfortunately large change.

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 __array_interface__ so doesn't matter for Cython of course, i.e. the chance of someone actually caring/noticing is a lot smaller.

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.

Thanks everyone for the discussion.
It seems reasonable to try this out, but just in case, I’ve added it as a topic for the next triage meeting.

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 updated the flags in __array_interface__ for scalars, following the discussion in the last triage meeting.
I've also updated the tests accordingly.

I checked repositories under the numba organization to see whether ctypeslib.as_ctypes is used and did not find any usage.
For reference:
https://github.com/search?q=org%3Anumba+%22as_ctypes%28%22&type=code

@seberg seberg added the triage review Issue/PR to be discussed at the next triage meeting label Jan 6, 2026
@seberg seberg added triaged Issue/PR that was discussed in a triage meeting and removed triage review Issue/PR to be discussed at the next triage meeting labels Jan 21, 2026
@riku-sakamoto

Copy link
Copy Markdown
Contributor Author

@ngoldbaum @seberg

Just a gentle ping.
I’ve updated the code based on previous triage mtg.
Please let me know if you have any remaining concerns!
I’m happy to address them.

@ngoldbaum

Copy link
Copy Markdown
Member

I think this needs a release note.

Can you also update the PR description to match what it actually does? The PR description will show up in the final merge commit.

@ngoldbaum ngoldbaum added the 56 - Needs Release Note. Needs an entry in doc/release/upcoming_changes label Jan 23, 2026

@ngoldbaum ngoldbaum 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.

The C API uses and reference counting all look correct to me.

@riku-sakamoto
riku-sakamoto force-pushed the fix_scalar_array_interface branch from 076e8c7 to 2b58cfc Compare January 25, 2026 07:43
@riku-sakamoto

Copy link
Copy Markdown
Contributor Author

@ngoldbaum
Thank you for taking another look.
I've updated the PR description to clarify the behavior and added a release note.

@seberg seberg removed 56 - Needs Release Note. Needs an entry in doc/release/upcoming_changes 09 - Backport-Candidate PRs tagged should be backported labels Jan 26, 2026

@seberg seberg 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.

LGTM, one nit is that we should also note the change in __array_interface__ maybe?

I would prefer if we could refactor this so that we have a single function that build the array interface but gets strides, shape, ptr and descr passed in.
(Right now we have twice practically the identical code.)

But considering how long this has been sitting, I am also happy if we don't do that.

This change was made to avoid the issue `gh-30354 <https://github.com/numpy/numpy/issues/30354>`__
and to enforce the readonly nature of scalar types in NumPy.
Users who need to convert scalar types to ctypes should first convert them to an array
(e.g., ``numpy.asarray``) before passing them to ``numpy.ctypeslib.as_ctypes``.

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.

Might be worth pointing out that this was always shady? :).

Can you add a second note about the change in __array_interface__? (or add both in one.)

}
PyList_SET_ITEM(res, 0, dobj);
return res;
}

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 would be cool to refactor these so that we don't repeat them in both files (i.e. by making them take the PyArray_Descr *).

But I can live without doing that at this point :).

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.

Thanks for pointing this out.

I've refactored the duplicated logic into a common helper function in descriptor.c and updated the relevant call to use it.

I left some functions, such as gentype_shape_get, unchanged since they are also used in other parts of scalartypes.c.src.
If you'd prefer to refactor those as well, I'm happy to follow up.

This behavior has been removed to avoid the issue `gh-30354 <https://github.com/numpy/numpy/issues/30354>`__.
Now, scalar types will not have a ``__ref`` entry in their ``__array_interface__``.
Instead, ``data`` entry in ``__array_interface__`` will directly point to the memory location of the scalar value and
will be marked as readonly.

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.

Thanks for the quick update! I was actually thinking the main thing is that we export it as readonly now.

(But maybe I'll just think about it a bit myself and push an update later)

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.

Thanks! That makes sense. Let me know if you'd like me to make any adjustments on my side.

Comment thread doc/release/upcoming_changes/30538.change.rst Outdated
Comment thread numpy/_core/src/multiarray/descriptor.c Outdated

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.

Hmm, thanks for the update, unfortunately PyArray_DescrFromScalar returns a new reference (and actually can in theory error). So we need to do refcounting here unfortunately.
(I was going to just "suggest" it, but it spans the next block also, a goto finish: that also cleans up inter may be nicer.

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.

Thanks for pointing it out.

I've updated this commit to handle the refcounting for PyArray_DescrFromScalar
in scalartypes.c.src:
5632bf0

In addition, I also refactored the code to use a common helper when creating
__array_interface__ for both arrays and scalars.

@seberg seberg 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.

OK, thanks @riku-sakamoto for all the follow-ups and checking what it might break. Let's get this in. (Small nit/comment: I think the building function would be slightly nicer to take the dtype/descr object to consolidate more of the logic there, but doens't matter if you feel about it, you can follow up).

If anyone finds this PR because the readonly or ctypes changes affect you, please let us know. I can see this affecting downstream, and if it does we have to check what to do about ctypes.

(I know that using ctypes is quite typical, my assumption is that it is not all that typical to use it on scalars and since it was broken memory ownership wise, I am hoping it isn't used much -- but I realize that the e.g. the fact that we cache this type of small temporary arrays may have made this work in practice!)

@seberg
seberg dismissed ngoldbaum’s stale review January 27, 2026 08:47

Review from an earlier iteration of the PR.

@seberg
seberg merged commit 1e5b6c5 into numpy:main Jan 27, 2026
75 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

00 - Bug triaged Issue/PR that was discussed in a triage meeting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: ctypes conversions of numpy scalars share the same buffer

5 participants