Proposed new feature or change:
For a Python int or float operand, casting="no" is accepted where casting="equiv" is rejected, in ufuncs and in np.copyto:
>>> np.copyto(np.zeros(3, dtype="uint8"), 3, casting="no")
>>> np.copyto(np.zeros(3, dtype="uint8"), 3, casting="equiv")
TypeError: cannot cast Python int to uint8 under the casting rule 'equiv'
>>> np.add(np.zeros(3, dtype="float32"), 3.0, casting="no")
array([3., 3., 3.], dtype=float32)
>>> np.add(np.zeros(3, dtype="float32"), 3.0, casting="equiv")
TypeError: cannot cast Python float to float32 under the casting rule 'equiv'
"no" is stricter than "equiv" for arrays, so the second call should not fail when the first succeeds. np.concatenate((np.zeros(1, dtype="uint8"), 3), axis=None, casting=...) accepts all five rules, so the functions also disagree with each other.
The cause is npy_update_operand_for_scalar, added in #27091:
|
if (PyArray_EquivTypes(PyArray_DESCR(*operand), descr)) { |
|
/* |
|
* TODO: This is an unfortunate work-around for legacy type resolvers |
|
* (see `convert_ufunc_arguments` in `ufunc_object.c`), that |
|
* currently forces us to replace the array. |
|
*/ |
|
if (!(PyArray_FLAGS(*operand) & NPY_ARRAY_WAS_PYTHON_INT)) { |
|
return 0; |
|
} |
|
} |
|
else if (NPY_UNLIKELY(casting == NPY_EQUIV_CASTING) && |
|
descr->type_num != NPY_OBJECT) { |
|
/* |
|
* incredibly niche, but users could pass equiv casting and we |
|
* actually need to cast. Let object pass (technically correct) but |
|
* in all other cases, we don't technically consider equivalent. |
|
* NOTE(seberg): I don't think we should be beholden to this logic. |
|
*/ |
|
PyErr_Format(PyExc_TypeError, |
|
"cannot cast Python %s to %S under the casting rule 'equiv'", |
|
Py_TYPE(scalar)->tp_name, descr); |
|
return -1; |
|
} |
When the target descriptor differs from the one discovered for the scalar, only NPY_EQUIV_CASTING is rejected. NPY_NO_CASTING falls through, the operand is recreated with the target descriptor, and the later cast check compares identical dtypes. test_copyto_cast_safety asserts the "equiv" error, and the comment in the code already questions the rule. This behavior was added in #27091 and NO_CASTING never came up in the review there.
A Python str converted to StringDType or object (#32040, #32356) goes through the same helper and has the same asymmetry. That behavior is new in the next release, so we're free to change it if we want, but I left things consistent with numeric types for now.
Possible fixes
- Reject "no" whenever "equiv" is rejected. Consistent, but code passing
casting="no" with a Python scalar whose dtype changes starts raising. We may need a deprecation?
- Accept "equiv". Under NEP 50 the scalar takes the other operand's dtype, so there is no cast for either rule to reject. This is what concatenate already does. It changes behavior asserted in
test_copyto_cast_safety.
@seberg I'd appreciate your opinion, especially if you can remember where you ultimately wanted to end up after #27091.
Proposed new feature or change:
For a Python int or float operand,
casting="no"is accepted wherecasting="equiv"is rejected, in ufuncs and innp.copyto:"no" is stricter than "equiv" for arrays, so the second call should not fail when the first succeeds.
np.concatenate((np.zeros(1, dtype="uint8"), 3), axis=None, casting=...)accepts all five rules, so the functions also disagree with each other.The cause is
npy_update_operand_for_scalar, added in #27091:numpy/numpy/_core/src/multiarray/abstractdtypes.c
Lines 366 to 388 in b265412
When the target descriptor differs from the one discovered for the scalar, only
NPY_EQUIV_CASTINGis rejected.NPY_NO_CASTINGfalls through, the operand is recreated with the target descriptor, and the later cast check compares identical dtypes.test_copyto_cast_safetyasserts the "equiv" error, and the comment in the code already questions the rule. This behavior was added in #27091 and NO_CASTING never came up in the review there.A Python str converted to
StringDTypeorobject(#32040, #32356) goes through the same helper and has the same asymmetry. That behavior is new in the next release, so we're free to change it if we want, but I left things consistent with numeric types for now.Possible fixes
casting="no"with a Python scalar whose dtype changes starts raising. We may need a deprecation?test_copyto_cast_safety.@seberg I'd appreciate your opinion, especially if you can remember where you ultimately wanted to end up after #27091.