Skip to content

API: Make np.squeeze always return an ndarray for scalar inputs#31561

Open
spandan11106 wants to merge 2 commits into
numpy:mainfrom
spandan11106:squeezeing-scalars
Open

API: Make np.squeeze always return an ndarray for scalar inputs#31561
spandan11106 wants to merge 2 commits into
numpy:mainfrom
spandan11106:squeezeing-scalars

Conversation

@spandan11106

Copy link
Copy Markdown

PR summary

Closes #30109.
Currently, np.squeeze behaves inconsistently with scalar-like inputs:

>>> np.squeeze(1)           # Python int → 0d array
array(1)
>>> np.squeeze(np.array(1)) # 0d ndarray → 0d array
array(1)
>>> np.squeeze(np.int_(1))  # np.generic scalar → scalar (unexpected behavior)
np.int64(1)

The issue is that np.generic subclasses have a .squeeze() method that returns self (the scalar itself). The current squeeze implementation in fromnumeric.py tries a.squeeze first, which succeeds for np.generic scalars and returns the scalar unchanged. This contradicts the docstring, which states:

"Note that if all axes are squeezed, the result is a 0d array and not a scalar."
This PR converts np.generic inputs to 0d arrays using ellipsis indexing a[...] before invoking a.squeeze.

Changes:

  • Runtime Fix: Converts np.generic inputs to 0d arrays in numpy/_core/fromnumeric.py.
  • Type Stubs: Updates numpy/_core/fromnumeric.pyi to remove the scalar-returning overload of squeeze (so it falls back to returning NDArray[ScalarT]).
  • Tests: Adds tests in numpy/_core/tests/test_multiarray.py checking various scalar types, axis=0, and Python scalars.
  • Release Note: Adds a compatibility news fragment in doc/release/upcoming_changes/27709.compatibility.rst.

First time committer introduction

Hi everyone! I am Spandan. I use NumPy for development/data science work, and noticed the inconsistent behavior when squeezing NumPy scalars compared to Python scalars. I wanted to contribute to the repository to clean up this edge case.


AI Disclosure

Antigravity was used to assist in researching the issue and drafting the test cases.

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

Thanks for the PR. Some comments on the tests in-line, but before worrying about those, I guess we'll need to think what the best behaviour is. As you note, the docstring is pretty clear that an array should be returned but of course the actual implementation is to defer to an object's squeeze() method. And a problem with the approach here is that np.squeeze(scalar) and scalar.squeeze() will now no longer do the same thing. I think that would be even more surprising.

I think we'll need to make a call. Either,

  • Adjust np.generic.squeeze() to return an array; or
  • Adjust the docstring of np.squeeze() to note explicitly it defers to .squeeze() and that numpy scalars remain scalars.

I'm not sure what is best, though in the face of doubt, I think I slightly prefer to adjust the docstring to match reality rather than the reverse. Let me ping @seberg, @mattip for other opinions.

p.s. Unrelated, but I don't really like that np.squeeze(scalar, axis=0) does not error.

assert result[()] == scalar

# axis=0 should also work for scalar inputs
result = np.squeeze(np.int_(1), axis=0)

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.

I don't know that I would bother to test what to me seems surprising behaviour (I would expect an error, like for axis=1...)

assert_raises(ValueError, a.squeeze, axis=(1,))
assert_equal(a.squeeze(axis=(2,)), [[1, 2, 3]])

def test_squeeze_scalar_returns_0d_array(self):

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.

To me, it would make more sense to move the tests to test_numeric.py, since we're testing a function from fromnumeric

def test_squeeze_scalar_returns_0d_array(self):
# np.squeeze should always return an ndarray, even for
# np.generic scalar inputs (gh-27709)
for scalar in [np.int_(1), np.float64(2.5), np.complex128(1+2j)]:

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.

I'd use @pytest.mark.parametrize on the function. You can then include a python scalar too.

# np.generic scalar inputs (gh-27709)
for scalar in [np.int_(1), np.float64(2.5), np.complex128(1+2j)]:
result = np.squeeze(scalar)
assert isinstance(result, np.ndarray), (

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.

No need for the explanation in the assert.

@seberg

seberg commented Jun 4, 2026

Copy link
Copy Markdown
Member

I'm not sure what is best, though in the face of doubt, I think I slightly prefer to adjust the docstring to match reality rather than the reverse. Let me ping @seberg, @mattip for other opinions.

Yeah, I asked the same on the issue. I think Joren would prefer to make it typing clear one way or another. For myself, it feels a bit silly for scalar.squeeze() to return an array, so I wouldn't mind just keeping things as is...
(Maybe the real trick would be to get away from the method forwarding behavior, in which case this silly function could actually use np.asarray() and the split becomes more sane, because this approach feels a bit ad-hoc to me. -- but of course that is a can of worms :/)

EDIT: We should probably move to the issue, but I am not sure we can move this PR forward until we have a decision there.

@seberg seberg added the 57 - Close? Issues which may be closable unless discussion continued label Jun 4, 2026
@mhvk

mhvk commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@seberg - oops, should have seen that this was to fix an issue, I've now moved discussion there. FWIW, I'm now leaning even more to just have the docstring match the implementation instead of the reverse.

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

Labels

57 - Close? Issues which may be closable unless discussion continued

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HUH: squeezeing scalars

3 participants