Skip to content

BUG: numpy.str_ and numpy.bytes_ scalars do not support [()] indexing despite having shape == () #31435

Open

Description

@andreas-h-sie

Describe the issue:

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.

Workaround

Use .item() instead of [()]:

np.str_('hello').item()   # => 'hello'  (works)
np.bytes_(b'hello').item()  # => b'hello'  (works)

.item() works consistently across all numpy scalar types.

Reproduce the code example:

import numpy as np

# All other numpy scalar types support [()] indexing:
assert np.int64(5)[()] == 5
assert np.float64(3.14)[()] == 3.14
assert np.bool_(True)[()] == True
assert np.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=0

s[()]  # 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=0

b[()]  # 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

Python and NumPy Versions:

numpy version: 2.2.6
Python 3.11.2 (main, Aug 26 2024, 07:20:54) [GCC 12.2.0]

Runtime Environment:

No response

How does this issue affect you or how did you find it:

No response

Activity

  1. jorenham commented on May 14, 2026

    @jorenham
    Member

    I can confirm that this also occurs with 2.4.4

  2. seberg commented on May 17, 2026

    @seberg
    Member

    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.

  3. AriGood commented on May 17, 2026

    @AriGood

    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.

  4. seberg commented on May 18, 2026

    @seberg
    Member

    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.

  5. andreas-h-sie commented on May 18, 2026

    @andreas-h-sie
    Author

    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

  6. seberg commented on May 18, 2026

    @seberg
    Member

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

  7. andreas-h-sie commented on May 18, 2026

    @andreas-h-sie
    Author

    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.

  8. seberg commented on May 18, 2026

    @seberg
    Member

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

  9. Haris-bin-shakeel commented on May 26, 2026

    @Haris-bin-shakeel

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

  10. Mohataseem89 commented on Jun 28, 2026

    @Mohataseem89
    Contributor

    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

  11. added 3 commits that reference this issue on Jun 29, 2026
    3239fd3
    20ba59e
    5488b77
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions