Skip to content

BUG: StringDType(coerce=...) is not preserved by binary ufuncs when operands differ #31861

Closed

Description

@Ethan0456

Describe the issue:

Brief description of bug

Binary operations on StringDType arrays do not correctly combine the coerce setting from both operands.

When two arrays with different coerce settings are used in an operation such as np.add, the result dtype effectively inherits coerce from the first operand only.

The result should enable coercion only when both input dtypes have coerce=True (same rule as np.promote_types / common_instance).

  • Current output
StringDType()
True
  • Expected output
StringDType(coerce=False)
False

Why its incorrect

The result reports coerce=True even though one input has coerce=False. That allows assignments like c[0] = object() to succeed by stringifying the value. With coerce=False preserved, that assignment should raise:

ValueError: StringDType only allows string data when string coercion is disabled.

Note that behavior is order-dependent for ufuncs: np.add(coerce=False, coerce=True) happens to look correct, while np.add(coerce=True, coerce=False) does not which is consistent with only the first descriptor being used.

Debugging hints

np.promote_types already does the right thing both ways, because common_instance in numpy/_core/src/multiarray/stringdtype/dtype.c was fixed recently in #31846 (backport #31856) to use:

dtype1->coerce && dtype2->coerce

The remaining bug is in the binary ufunc path: binary_resolve_descriptors in numpy/_core/src/umath/stringdtype_ufuncs.cpp, which still combines coerce incorrectly:

@@ -248,7 +248,7 @@ binary_resolve_descriptors(...)
 {
     PyArray_StringDTypeObject *descr1 = (PyArray_StringDTypeObject *)given_descrs[0];
     PyArray_StringDTypeObject *descr2 = (PyArray_StringDTypeObject *)given_descrs[1];
-    int out_coerce = descr1->coerce && descr1->coerce;
+    int out_coerce = descr1->coerce && descr2->coerce;
     PyObject *out_na_object = NULL;

So the result dtype depends only on the first input’s coerce flag (the descr1 && descr1 typo), which is why np.add still misbehaves even though type promotion via promote_types is correct.

Applying that one-line change produces the expected result:

StringDType(coerce=False)
False

and assigning a non-string value correctly raises

ValueError: StringDType only allows string data when string coercion is disabled.

If this is confirmed to be a bug, I'd be happy to contribute a fix

Reproduce the code example:

import numpy as np
from numpy.dtypes import StringDType

a = np.array(["a"], dtype=StringDType(coerce=True))
b = np.array(["b"], dtype=StringDType(coerce=False))

c = np.add(a, b)

print(c.dtype)        # currently: StringDType()
print(c.dtype.coerce) # currently: True; expected: False

c[0] = object()       # currently succeeds (stringifies); should raise ValueError

Error message:

No exception is raised.

If the resulting dtype preserved coerce=False, assigning a non-string value would raise:

ValueError: StringDType only allows string data when string coercion is disabled.

Python and NumPy Versions:

2.6.0.dev0+git20260625.67f37e4
3.14.3 (main, Apr  8 2026, 11:37:06) [Clang 14.0.3 (clang-1403.0.22.14.1)]

Runtime Environment:

[{'numpy_version': '2.6.0.dev0+git20260625.67f37e4',
  'python': '3.14.3 (main, Apr  8 2026, 11:37:06) [Clang 14.0.3 '
            '(clang-1403.0.22.14.1)]',
  'uname': uname_result(system='Darwin', node='192.168.0.100', release='22.6.0', version='Darwin Kernel Version 22.6.0: Wed Jul  5 22:22:52 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T8103', 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:

No response

Activity

  1. Ethan0456 commented on Jul 4, 2026

    @Ethan0456
    ContributorAuthor

    Since this is a small fix, I've opened a minimal PR with a regression test here: #31862. Happy to tweak if needed.

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