Skip to content

BUG: MaskedArray.__array_wrap__ propagates a stale, dtype-invalid fill_value across dtype-changing ufuncs #32401

Closed

Description

@jdavies-st

Describe the issue:

When a ufunc changes the dtype of a MaskedArray's output relative to its input (e.g. np.strings.find applied to a string masked array, which returns an integer array of match positions), the output MaskedArray silently inherits the raw _fill_value from the input array, without checking that it is still valid for the new dtype.

This happens in MaskedArray.__array_wrap__, which calls result._update_from(self) to copy attributes (including _fill_value) from the input onto the ufunc output. _update_from does not validate _fill_value against result.dtype.

The stale fill_value does not raise an error immediately -- printing the result, indexing it, etc. all appear to work fine. It only surfaces later, the next time something calls .view() on the array, because __array_finalize__ does validate _fill_value against self.dtype via _check_fill_value, and that validation now fails.

This has been broken since numpy 2.0. Before that, this np.char.find would just return a standard ndarray, no a MaskedArray, so the fill_value was not an issue.

Reproduce the code example:

import numpy as np

col = np.ma.MaskedArray(['foo', 'bar', 'baz'], mask=False, fill_value='N/A')
result = np.strings.find(col, 'foo')   # dtype changes from string to int64
print(type(result), result.dtype)

# result silently carries over col's *string* fill_value even though its
# own dtype is now int64 -- not validated yet, so this all looks fine:
print(result._fill_value)     # 'N/A'
print(result)                 # repr works fine
print(result[0])              # indexing works fine

# But an explicit .view() re-runs numpy's internal fill_value/dtype
# consistency check, and that raises an exception:
result.view(np.ma.MaskedArray)

Error message:

Traceback (most recent call last):
  File ".../numpy/ma/core.py", line 510, in _check_fill_value
    fill_value = np.asarray(fill_value, dtype=ndtype)
ValueError: invalid literal for int() with base 10: np.str_('N/A')

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<string>", line 16, in <module>
    result.view(np.ma.MaskedArray)
  File ".../numpy/ma/core.py", line 3264, in view
    output = super().view(*[a for a in (dtype, type) if a is not None])
  File ".../numpy/ma/core.py", line 3138, in __array_finalize__
    self._fill_value = _check_fill_value(self._fill_value, self.dtype)
  File ".../numpy/ma/core.py", line 516, in _check_fill_value
    raise TypeError(err_msg % (fill_value, ndtype)) from e
TypeError: Cannot convert fill_value N/A to dtype int64

Python and NumPy Versions:

NumPy 2.5.2
Python 3.14.7

Runtime Environment:

[{'numpy_version': '2.5.2',
  'python': '3.14.7 | packaged by conda-forge | (main, Aug 19 2026, 15:29:14) '
            '[Clang 20.1.8 ]',
  'uname': uname_result(system='Darwin', node='mac-n-211', release='25.6.0', version='Darwin Kernel Version 25.6.0: Sat Jul 11 15:25:26 PDT 2026; root:xnu-12377.161.13~4/RELEASE_ARM64_T6030', machine='arm64')},
 {'simd_extensions': {'baseline': ['NEON', 'NEON_FP16', 'NEON_VFPV4', 'ASIMD'],
                      'found': ['ASIMDHP', 'ASIMDDP'],
                      'not_found': ['ASIMDFHM']}},
 {'ignore_floating_point_errors_in_matmul': True}]

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

This was originally noticed as a downstream bug in astropy's MaskedColumn (a MaskedArray subclass), whose .data property calls self.view(np.ma.MaskedArray) internally on every __getitem__, so it hits this landmine on essentially every access after a dtype-changing ufunc like np.strings.find. See astropy/astropy#20257

Plain np.ma.MaskedArray mostly avoids tripping over it in everyday use only because printing/indexing happen not to call .view() internally -- but the underlying inconsistent state (an unvalidated, dtype-mismatched _fill_value) is present regardless of subclassing, as the snippet above shows with plain MaskedArray.

Expected behavior: either __array_wrap__/_update_from should reset _fill_value to the dtype-appropriate default when the output dtype no longer matches the fill_value's type, or _update_from should validate the copied fill_value the same way __array_finalize__ does, so the inconsistency is caught (or avoided) at the point it is introduced rather than surfacing later as an unrelated-looking TypeError.

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