Repository navigation
Description
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 int64Python 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.
Activity