You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
BUG: numpy.str_ and numpy.bytes_ scalars do not support [()] indexing despite having shape == () #31435
numpy.str_ and numpy.bytes_ are 0-dimensional numpy scalars (shape == (), ndim == 0), yet indexing them with [()] (empty tuple, which is the standard way to extract the value from a 0-d array/scalar) raises a TypeError. This is inconsistent with all other numpy scalar types where scalar[()] works correctly.
The issue appears to be that Python's str.__getitem__ / bytes.__getitem__ takes priority over numpy's scalar indexing protocol, interpreting () as a tuple index into the string/bytes sequence rather than as a 0-d array index.
Expected behavior
numpy.str_('hello')[()] should return 'hello' (the Python str value), consistent with how numpy.int64(5)[()] returns 5.
Since numpy.str_ reports shape == () and ndim == 0, it should support the standard 0-d indexing protocol that all other numpy scalars support. Code that generically extracts values from 0-d numpy scalars via val[()] breaks specifically for string and bytes scalars.
.item() works consistently across all numpy scalar types.
Reproduce the code example:
importnumpyasnp# All other numpy scalar types support [()] indexing:assertnp.int64(5)[()] ==5assertnp.float64(3.14)[()] ==3.14assertnp.bool_(True)[()] ==Trueassertnp.complex128(1+2j)[()] == (1+2j)
# numpy.str_ and numpy.bytes_ do NOT, despite having shape == ():s=np.str_('hello')
print(f"type: {type(s)}, shape: {s.shape}, ndim: {s.ndim}") # shape=(), ndim=0s[()] # TypeError: string indices must be integers, not 'tuple'b=np.bytes_(b'hello')
print(f"type: {type(b)}, shape: {b.shape}, ndim: {b.ndim}") # shape=(), ndim=0b[()] # TypeError: byte indices must be integers or slices, not tuple
Error message:
TypeError: string indices must be integers, not 'tuple'
TypeError: byte indices must be integers or slices, not tuple
While I agree with the sentiment, but I am very doubtful about touching this... (its not a 2.4.4 thing, this is the case since the dawn of time)
Right now these actually inherit from string and bytes and those have their own indexing rules. So... it would be nice, but it is also very unclear what the fallout would be if suddenly string style indexing fails for these.
That was my reasoning for the gh-31448 . I only added a special case for the empty-tuple index scalar[()], all other indexing is still delegated to the existing Python str/bytes implementation, so the intent was to preserve the current string-style indexing behavior.
Yeah, but what about [...] or [None] or even [True/False] where the last one actually does something different for strings vs. arrays.
It would be good if @andreas-h-sie (or anyone) could give a clear context where this is problematic and [()] specifically would help, then we may have a basis to for discussing such a change.
I have a library which does IO with numpy arrays and more complex datatypes composed of numpy arrays. It cannot save Python types only numpy arrays. So the code has to handle cornercases where the numpy arrays are scalars (shape == ()). However sometimes these scalar numpy arrays have to be converted back to a Python type especially when they are used to construct more complex types from them. One example is a pandas.Categorical where the categories have to be Python strings and pandas does not like 0-d numpy arrays.
This is why I reference all objects which have a shape == (), to remove the "scalar wrapper".
def _pythonize_numpy_scalars(val: Union[numpy.ndarray, list, str, bytes], topleveltolist: bool = False) -> Any:
"""Pandas does not like numpy strings. Convert them back to python strings. In fact convert all scalar numpys to value"""
if isinstance(val, list):
shape = (len(val),)
elif isinstance(val, (str, bytes)):
return val
elif not hasattr(val, 'shape'):
return val
else:
shape = val.shape
if len(shape) == 0:
return val[()] # <--- crashes here if val is a numpy.str_
if topleveltolist and len(shape) == 1 and not (isinstance(val, numpy.ndarray) and val.dtype != numpy.dtype(object)):
return [
_pythonize_numpy_scalars(val[inx]) for inx in range(shape[0])
]
for inx in ( numpy.ndindex(shape) if len(shape) != 1 else range(shape[0]) ):
v = val[inx]
if isinstance(v, numpy.ndarray):
val[inx] = _pythonize_numpy_scalars(v)
return val
This was working for numpy 1, but for numpy 2 it fails because now there is a numpy.str_ type returned by the subscript operators and this numpy.str_ has a shape attribute.
I understand why numpy.str_ was introduced, but adding a shape == () without allowing () subscript seems inconsistent.
MATLAB for instance, after introducing the true string type (with the double quotes), does not offer a subscript operator for the string at all. (Note in matlab everything is an "ndarray" by default so applying a subscript on a string references the n-d array not the characters). To get characters one uses special functions such as extractBefore
type returned by the subscript operators and this numpy.str_ has a shape attribute.
I agree this is all unfortunate, but we never changed it? NumPy strings always had a shape and always only supported string indexing.
Do you know what the actual change is that suddenly gives you a scalar rather than a 0-D array? We would like to do something about NumPy converting 0-D arrays to scalars, but it is a bit tricky (if you have earlier ufunc calls, new NumPy allows you to pass out=... to avoid this, but this may be more helpful for NumPy itself than for downstream probably).
FWIW, using .item() makes a lot of sense (although it has it's problems with datetime/timedelta :())
MATLAB for instance, after introducing the true string type (with the double quotes), does not offer a subscript operator for the string at all.
I would prefer that if our scalars just didn't do subscription, but there isn't any easy solution so long (a) we don't transition away from converting 0-D arrays to scalars and (b) a possibly painful deprecation for .shape on at least our strings...
(Of course others would get rid of scalars entirely, but I think that is even less plausible.)
Now that I think about it, I guess the issue is that now the array subscript returns numpy.str_ and no longer str.
You see my function, special cases str and bytes. I think this is the code path that was taken in numpy 1 (and it did not arrive at the if len(shape) == 0: ...). Maybe this is the root cause.
I would need to check if pandas likes numpy.str_ instead of Python str though. Changing to .item() might be a better soluton, though - for the reason you mention.
You see my function, special cases str and bytes. I think this is the code path that was taken in numpy 1
Hmmmm, this is also weird :/? np.str_ and np.bytes_ still (and always have) subclassed str and bytes, so just like before you should be hitting that code path (sorry, I didn't notice that before).
Root cause: np.str_ / np.bytes_ are dual-inheritance scalars (tp_base = PyUnicode_Type / PyBytes_Type) with NumPy scalar behavior layered via character → generic. As a result, shape == () is provided by numpy.generic, but __getitem__ dispatch for these types is resolved at the Python str/bytes level before NumPy scalar indexing is reached.
Path split:
Numeric scalars:np.int64(5)[()] → gen_arrtype_subscript → PyArray_FromScalar → array_subscript, where () is treated as a valid 0-d index.
String/bytes scalars:np.str_('x')[()] → Python str/bytes mapping (tp_as_mapping) → tuple index rejected, raising TypeError before NumPy dispatch.
This is longstanding behavior and not a 2.x regression; it follows from inheritance and dispatch precedence.
A minimal safe change would be an ()-only special case via tp_as_mapping for string/bytes scalar types, returning self, while delegating all other keys unchanged to Python str/bytes.
i wouldd like to work on a fix for this. the root cause is that numpy.str_ and numpy.bytes_ inherit Python's str.__getitem__ / bytes.__getitem__, which intercepts [()] before NumPy's 0-d scalar indexing protocol. the fix is to define a custom mp_subscript in scalartypes.c.src that catches the empty-tuple case and delegates to .item(). This approach was outlined in #7260 years ago but never implemented as a proper fix. happy to open a PR
Activity
jorenham commentedon May 14, 2026
I can confirm that this also occurs with 2.4.4
seberg commentedon May 17, 2026
While I agree with the sentiment, but I am very doubtful about touching this... (its not a 2.4.4 thing, this is the case since the dawn of time)
Right now these actually inherit from
stringandbytesand those have their own indexing rules. So... it would be nice, but it is also very unclear what the fallout would be if suddenly string style indexing fails for these.AriGood commentedon May 17, 2026
That was my reasoning for the gh-31448 . I only added a special case for the empty-tuple index scalar[()], all other indexing is still delegated to the existing Python str/bytes implementation, so the intent was to preserve the current string-style indexing behavior.
seberg commentedon May 18, 2026
Yeah, but what about
[...]or[None]or even[True/False]where the last one actually does something different for strings vs. arrays.It would be good if @andreas-h-sie (or anyone) could give a clear context where this is problematic and
[()]specifically would help, then we may have a basis to for discussing such a change.andreas-h-sie commentedon May 18, 2026
I have a library which does IO with numpy arrays and more complex datatypes composed of numpy arrays. It cannot save Python types only numpy arrays. So the code has to handle cornercases where the numpy arrays are scalars (shape == ()). However sometimes these scalar numpy arrays have to be converted back to a Python type especially when they are used to construct more complex types from them. One example is a pandas.Categorical where the categories have to be Python strings and pandas does not like 0-d numpy arrays.
This is why I reference all objects which have a shape == (), to remove the "scalar wrapper".
This was working for numpy 1, but for numpy 2 it fails because now there is a
numpy.str_type returned by the subscript operators and this numpy.str_ has ashapeattribute.I understand why
numpy.str_was introduced, but adding ashape == ()without allowing()subscript seems inconsistent.MATLAB for instance, after introducing the true string type (with the double quotes), does not offer a subscript operator for the string at all. (Note in matlab everything is an "ndarray" by default so applying a subscript on a string references the n-d array not the characters). To get characters one uses special functions such as
extractBeforeseberg commentedon May 18, 2026
I agree this is all unfortunate, but we never changed it? NumPy strings always had a shape and always only supported string indexing.
Do you know what the actual change is that suddenly gives you a scalar rather than a 0-D array? We would like to do something about NumPy converting 0-D arrays to scalars, but it is a bit tricky (if you have earlier ufunc calls, new NumPy allows you to pass
out=...to avoid this, but this may be more helpful for NumPy itself than for downstream probably).FWIW, using
.item()makes a lot of sense (although it has it's problems with datetime/timedelta :())I would prefer that if our scalars just didn't do subscription, but there isn't any easy solution so long (a) we don't transition away from converting 0-D arrays to scalars and (b) a possibly painful deprecation for
.shapeon at least our strings...(Of course others would get rid of scalars entirely, but I think that is even less plausible.)
andreas-h-sie commentedon May 18, 2026
Now that I think about it, I guess the issue is that now the array subscript returns
numpy.str_and no longerstr.You see my function, special cases
strandbytes. I think this is the code path that was taken in numpy 1 (and it did not arrive at theif len(shape) == 0: ...). Maybe this is the root cause.I would need to check if pandas likes
numpy.str_instead of Python str though. Changing to.item()might be a better soluton, though - for the reason you mention.seberg commentedon May 18, 2026
Hmmmm, this is also weird :/?
np.str_andnp.bytes_still (and always have) subclassedstrandbytes, so just like before you should be hitting that code path (sorry, I didn't notice that before).Haris-bin-shakeel commentedon May 26, 2026
Reproduced on 2.4.x and traced on current main.
Root cause:
np.str_/np.bytes_are dual-inheritance scalars (tp_base = PyUnicode_Type / PyBytes_Type) with NumPy scalar behavior layered viacharacter → generic. As a result,shape == ()is provided bynumpy.generic, but__getitem__dispatch for these types is resolved at the Pythonstr/byteslevel before NumPy scalar indexing is reached.Path split:
np.int64(5)[()] → gen_arrtype_subscript → PyArray_FromScalar → array_subscript, where()is treated as a valid 0-d index.np.str_('x')[()] →Pythonstr/bytesmapping (tp_as_mapping) → tuple index rejected, raisingTypeErrorbefore NumPy dispatch.This is longstanding behavior and not a 2.x regression; it follows from inheritance and dispatch precedence.
A minimal safe change would be an
()-only special case viatp_as_mappingfor string/bytes scalar types, returningself, while delegating all other keys unchanged to Pythonstr/bytes.Mohataseem89 commentedon Jun 28, 2026
i wouldd like to work on a fix for this. the root cause is that
numpy.str_andnumpy.bytes_inherit Python's str.__getitem__/bytes.__getitem__,which intercepts [()] before NumPy's 0-d scalar indexing protocol. the fix is to define a custommp_subscriptinscalartypes.c.srcthat catches the empty-tuple case and delegates to.item(). This approach was outlined in #7260 years ago but never implemented as a proper fix. happy to open a PRBUG: fix str_ and bytes_ scalars not supporting [()] indexing Fixes: n…
BUG: fix str_ and bytes_ scalars not supporting [()] indexing
BUG: fix str_ and bytes_ scalars not supporting [()] indexing