Skip to content

BUG: apply the NEP 50 scalar rules in concatenate and choose, and make casting="no" match "equiv" - #32497

Merged
ngoldbaum merged 8 commits into
numpy:mainfrom
ngoldbaum:pyscalar-concat-choose
Sep 15, 2026
Merged

ngoldbaum merged 8 commits into
numpy:mainfrom
ngoldbaum:pyscalar-concat-choose

Conversation

@ngoldbaum

@ngoldbaum ngoldbaum commented Sep 3, 2026 •

Copy link
Copy Markdown
Member

PR summary

This makes np.concatenate(axis=None) and np.choose cast the temporary array made from a Python scalar instead of converting the scalar with the resolved dtype.

Before:

>>> np.concatenate((np.ones(2, "int8"), 300), axis=None)
array([ 1,  1, 44], dtype=int8)
>>> np.choose([0], (-1, np.array([1], dtype=np.uint8)))
array([255], dtype=uint8)

After:

>>> np.concatenate((np.ones(2, "int8"), 300), axis=None)
OverflowError: Python integer 300 out of bounds for int8
>>> np.choose([0], (-1, np.array([1], dtype=np.uint8)))
OverflowError: Python integer -1 out of bounds for uint8

This also makes casting='no' an error in spots where casting='equiv' is an error. Closes #32491, see discussion there for details.

While we're at it, also adds reference docs for the NumPy scalar casting rules.

I searched for open source codebases that this change would break and couldn't find any, but I could have easily missed things. Maybe the new errors should be deprecations instead?

The first commit duplicates #32496 because the tests in this PR crash without that fix. I'm making that PR separately from this one to backport the crash fix.

AI Disclosure

I iterated with an AI model on the code changes and new tests.

@ngoldbaum

Copy link
Copy Markdown
Member Author

Ping @mhvk since you pointed out the inconsistency this fixes in NumPy's python scalar casting rules in code review for #32356.

for casting in ["no", "equiv", "safe", "same_kind", "unsafe"]:
res = np.concatenate((arr, "z"), axis=None, casting=casting)
assert res[1] == "z"

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.

this change hasn't made it into a release yet, so there's no backward compatibility concern

[(1000, np.array([1], dtype=np.uint8)),
(-1, np.array([1], dtype=np.uint8)),
[(100, np.array([1], dtype=np.uint8)),
(-1, np.array([1], dtype=np.int8)),

@ngoldbaum ngoldbaum Sep 3, 2026 •

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.

This changed because 1000 now triggers an overflow error. Should it merely be a deprecation instead?

@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 good! And especially good to update the docs -- so that is also where my attention went: a few in-line comments to help ensure we do not forget that for float we allow loss of precision with "safe".

treats a Python ``int``, ``float`` or ``complex`` by its kind, since the
scalar has no precision of its own. Converting a Python ``int`` to any NumPy
integer dtype or a Python ``float`` to any NumPy floating point dtype counts
as "safe", even though the value may not fit. A value that does not fit

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.

"may not fit or loose precision" (for float).

scalar has no precision of its own. Converting a Python ``int`` to any NumPy
integer dtype or a Python ``float`` to any NumPy floating point dtype counts
as "safe", even though the value may not fit. A value that does not fit
raises ``OverflowError`` on conversion, as shown above. Lowering the kind,

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.

"While lowering a python float to a lower-precision type is considered safe,, lowering it to an integer dtype requires casting="unsafe". ..."

as "safe", even though the value may not fit. A value that does not fit
raises ``OverflowError`` on conversion, as shown above. Lowering the kind,
such as a Python ``float`` into an integer dtype, requires
``casting="unsafe"``. `numpy.choose` applies the same rules with "safe"

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.

Why is numpy.choose mentioned here (could perhaps be at the end?)

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.

Thanks. I just dropped it because this section isn't really about that and it's an obscure detail.

@ngoldbaum

ngoldbaum commented Sep 9, 2026 •

Copy link
Copy Markdown
Member Author

@seberg any chance I can get your opinion here? This is docs and two fixes for things that were missed in the NEP 50 transition.

@ngoldbaum ngoldbaum added the triage review Issue/PR to be discussed at the next triage meeting label Sep 15, 2026
@ngoldbaum
ngoldbaum force-pushed the pyscalar-concat-choose branch from b2f2a63 to 59accf2 Compare September 15, 2026 16:49
@ngoldbaum

Copy link
Copy Markdown
Member Author

I went ahead and pushed another commit that removes use of asanyarray in the np.append implementation, which nicely makes it end up as a one-line alias for np.concatenate!

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

Thanks this looks nice. The question is if we dare do the concatenate change as it could well break someone.

A deprecation is always nice I guess, but overall, I think I can live with it (until someone sounds the warning bells that they are hitting it of course!).
Really, I have never seen anyone rely on concatenate with axis=None and even if someone does it, the plausible issue is niche around:

concatenate((-1, arr, -1), dtype="uint8")

or so. Plausible, but strange.

But maybe I am getting a bit too relaxed about this these days. I just really think this'll break at most a number of people countable on one hand (of course if one of those is a large library).

scalars with the result dtype, as ufuncs and `numpy.copyto` do. A Python
integer that does not fit the result dtype raises ``OverflowError`` instead of
wrapping. For example, ``np.concatenate((np.ones(2, "int8"), 300), axis=None)``
previously returned ``44`` for the new element.

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, I guess this change would be the one to worry about. But concatenating numbers with axis=None seems so awkward that...
(I have seen concatenate(([1], array)) more, although this actually probably is what those would want to use :))

@ngoldbaum ngoldbaum Sep 15, 2026 •

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 just did another AI-assisted scan and couldn't find cases where the new concatenate behavior would cause breakage in popular downstream libraries.

However the append change does lead to breakage (numba encodes our current behavior, for one), so I'm going to go ahead and back that out from this PR and then do a second PR that introduces a deprecation warning cycle.

...
TypeError: Cannot cast scalar from dtype('float64') to dtype('int8') according to the rule 'same_kind'

Under ``casting="equiv"`` and ``casting="no"`` a Python scalar must convert

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 might be tempted to not worry about this, but I guess for completeness sake it makes sense.
(I really don't see why a user would ever want to do this, except maybe being surprised by it actually being so strict.)

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.

Which is why I think it's safe to change. I'm only changing this because Marten noticed it looking at another PR and because I like fixing consistency holes like this.

if arr.ndim != 1:
arr = arr.ravel()
values = ravel(values)
axis = arr.ndim - 1

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.

heh fun!

@ngoldbaum
ngoldbaum force-pushed the pyscalar-concat-choose branch from 59accf2 to 2a2aaa7 Compare September 15, 2026 17:14
@seberg

seberg commented Sep 15, 2026

Copy link
Copy Markdown
Member

(Just saw the release note trimming, looks good too, and thanks, I like them short!)

@ngoldbaum ngoldbaum changed the title BUG: apply the NEP 50 scalar rules in concatenate and choose, and make casting="no" match "equiv" BUG: apply the NEP 50 scalar rules in append, concatenate, and choose, and make casting="no" match "equiv" Sep 15, 2026
@ngoldbaum ngoldbaum changed the title BUG: apply the NEP 50 scalar rules in append, concatenate, and choose, and make casting="no" match "equiv" BUG: apply the NEP 50 scalar rules in concatenate and choose, and make casting="no" match "equiv" Sep 15, 2026

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

Super, all looks good!

@ngoldbaum
ngoldbaum merged commit b245332 into numpy:main Sep 15, 2026
91 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ENH: make "no" and "equiv" casting consistent for Python scalars in ufuncs and copyto

3 participants