Skip to content

BUG: MaskedArray arithmetic with Python scalars ignores NEP 50 weak-type#31871

Open
shipitdev wants to merge 1 commit into
numpy:mainfrom
shipitdev:fix-masked-array-nep50
Open

BUG: MaskedArray arithmetic with Python scalars ignores NEP 50 weak-type#31871
shipitdev wants to merge 1 commit into
numpy:mainfrom
shipitdev:fix-masked-array-nep50

Conversation

@shipitdev

@shipitdev shipitdev commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Closes #31868

Problem

MaskedArray arithmetic with Python scalars produces a different dtype
than the same operation on a plain ndarray:

import numpy as np
import numpy.ma as ma

arr = np.array([100], dtype=np.int8)
masked = ma.masked_array([100], dtype=np.int8)

print((arr + 50).dtype)     # int8  (Expected, respects NEP 50)
print((masked + 50).dtype)  # int64 (Bug)

This was due to:
_MaskedBinaryOperation.__call__ calls getdata() on both operands before calling the ufunc. For Python scalars, getdata() has no
._data to grab, so it falls back to np.array(50) which creates a 0-d int64 array. Under NEP 50 that's a "strong" type, so the ufunc
promotes int8 to int64. The standalone power() function has the same issue.

Fix

Skip getdata() for Python scalars (int, float, complex) in the two call sites, passing them straight to the ufunc where NEP 50
treats them as weak types.

Test & Enviornment

Added test_scalar_arithmetic_preserves_dtype_nep50 covering int8,float32, four operators, reversed operand order, and numpy-scalar promotion.

  • OS: Darwin 25.5.0 (arm64)
  • Python: 3.14.0
  • NumPy: 2.6.0.dev0+git20260706.7e381e7
$ spin test numpy/ma/tests/test_core.py -- -x -q
...
=========================== short test summary info ===========================
SKIPPED [1] numpy/testing/_private/utils.py:2692: Could not determine available memory...
XFAIL numpy/ma/tests/test_core.py::TestMaskedConstant::test_coercion_unicode - See gh-9750
XFAIL numpy/ma/tests/test_core.py::TestMaskedConstant::test_coercion_bytes - See gh-9750
4170 passed, 1 skipped, 2 xfailed in 2.37s

AI Disclosure

AI was used only to strictly format this PR for maintainer readability and ease I also Used an AI assistant to help trace the call chain from MaskedArray.__add__ through _MaskedBinaryOperation.__call__ to
getdata, and to confirm the root cause.

The diagnosis, fix, and tests were written and verified manually.

When a Python scalar is passed to MaskedArray arithmetic, getdata()
converts it to a 0-d numpy array before the ufunc sees it. This
turns it from a NEP 50 'weak' type into a 'strong' type, causing
unnecessary dtype promotion (e.g. int8 + int -> int64 instead of int8).
Skip getdata() for Python scalars in _MaskedBinaryOperation.__call__
and in power(), so the ufunc receives them directly.
Closes numpygh-31868
@shipitdev

Copy link
Copy Markdown
Contributor Author

I first tried fixing this inside getdata() itself (returning Python scalars unchanged). That worked for normal arithmetic but when i ran the whole test_core i noticed it broke all in-place operators (+=, -=, etc.) . The call-site approach avoids this by only changing the path where scalars go to a ufunc.

@shipitdev

Copy link
Copy Markdown
Contributor Author

Relation to prior work

This issue was previously reported in #27029 and #27679. A more comprehensive fix covering both regular and in-place operations exists in #28567. This PR addresses the non-in-place case only, using a call-site approach (bypassing getdata() for Python scalars in_MaskedBinaryOperation.__call__ and power()) rather than modifying getdata() itself.

If a maintainer feels this is not relevant or conflicting with #28567. I have no problem abandoning this, However if the comprehensive pr stall for any longer, i feel that this narrower fix covers the most common user-facing case.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: numpy.ma.MaskedArray arithmetic with Python scalars differs from ndarray

2 participants