Skip to content

BUG: ndarray.conjugate() regression for legacy user-defined dtypes in 2.5.0 #31754

Closed

Description

@moble

Describe the issue:

ndarray.conjugate() (and .conj()) raises TypeError: cannot conjugate non-numeric dtype for legacy (pre-NEP-42) user-defined dtypes in NumPy 2.5.0. It worked in 2.4.x and earlier. This affects any dtype registered via PyArray_RegisterDataType, including ones that register their own conjugate ufunc loop. The np.conjugate(arr) ufunc is unaffected — only the ndarray.conjugate()/.conj() method regressed.

The example below uses NumPy's own in-tree rational test dtype, so it needs no third-party packages:

  • 2.4.6: a.conjugate() → [1 2 3] (no-op, correct for this real-valued dtype)
  • 2.5.0: a.conjugate() → TypeError: cannot conjugate non-numeric dtype

A real-world case is numpy-quaternion, whose dtype registers a genuine conjugate ufunc loop; arr.conjugate() used to compute the quaternion conjugate via that loop and now errors (moble/quaternion#270).

Root cause: #31264 / commit b6bc683 ("ENH: Allow user-dtypes return views on .conjugate()") rewrote the dispatch in
PyArray_Conjugate (numpy/_core/src/multiarray/calculation.c):

-    if (PyArray_ISCOMPLEX(self) || PyArray_ISOBJECT(self) ||
-            PyArray_ISUSERDEF(self) || !NPY_DT_is_legacy(PyArray_DESCR(self))) {
+    if (NPY_DT_SLOTS(NPY_DTYPE(PyArray_DTYPE(self)))->imag_meth != NULL) {

The old condition routed user-defined dtypes (PyArray_ISUSERDEF) to the conjugate ufunc. The new one routes only dtypes whose DType defines an imag_meth slot. Legacy user dtypes have no imag_meth (a NEP-42/43 slot) and are never flagged NPY_DT_NUMERIC (for legacy descriptors that flag is set only when PyTypeNum_ISNUMBER(descr->type_num), always false for type_num >= NPY_USERDEF), so they fall through to the new !NPY_DT_is_numeric(...) branch and error — even when a conjugate loop is registered.

(For clarity: the "cannot conjugate non-numeric dtype" message predates this and came from the expired no-op-fallthrough deprecation; that expiry was fine, since in 2.4.x the PyArray_ISUSERDEF carve-out kept user dtypes off that path. The regression is specifically the removal of that carve-out in #31264, which doesn't appear to have been intended to change legacy-dtype behavior.)

Expected behavior: as in 2.4.x — dispatch to the registered conjugate ufunc loop if one exists, otherwise return a view (real-valued no-op). Legacy user dtypes should not unconditionally error.

Reproduce the code example:

import numpy as np
from numpy._core._rational_tests import rational

a = np.array([1, 2, 3], dtype=rational)
print("dtype:", a.dtype, "type_num:", a.dtype.num)  # rational 256
print("np.conjugate (ufunc) works:", np.conjugate(a))  # [1 2 3]
print(a.conjugate())  # TypeError on 2.5.0, was [1 2 3] on 2.4.6

Error message:

Traceback (most recent call last):
  File "<python-input-0>", line 7, in <module>
    print(a.conjugate())  # TypeError on 2.5.0, was [1 2 3] on 2.4.6
          ~~~~~~~~~~~^^
TypeError: cannot conjugate non-numeric dtype

Python and NumPy Versions:

2.5.0
3.14.6 | packaged by conda-forge | (main, Jun 12 2026, 08:47:56) [Clang 20.1.8 ]

Runtime Environment:

[{'numpy_version': '2.4.6',
'python': '3.14.6 | packaged by conda-forge | (main, Jun 12 2026, 08:47:56) '
'[Clang 20.1.8 ]',
'uname': uname_result(system='Darwin', node='Beasty', release='23.6.0', version='Darwin Kernel Version 23.6.0: Mon Jul 29 21:13:04 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T6020', machine='arm64')},
{'simd_extensions': {'baseline': ['NEON', 'NEON_FP16', 'NEON_VFPV4', 'ASIMD'],
'found': ['ASIMDHP', 'ASIMDDP'],
'not_found': ['ASIMDFHM']}},
{'ignore_floating_point_errors_in_matmul': True},
{'architecture': 'VORTEX',
'filepath': '/Users/boyle/.continuum/mambaforge/envs/science/lib/libopenblas.0.dylib',
'internal_api': 'openblas',
'num_threads': 12,
'prefix': 'libopenblas',
'threading_layer': 'openmp',
'user_api': 'blas',
'version': '0.3.33'},
{'filepath': '/Users/boyle/.continuum/mambaforge/envs/science/lib/libomp.dylib',
'internal_api': 'openmp',
'num_threads': 12,
'prefix': 'libomp',
'user_api': 'openmp',
'version': None}]

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

Found via a downstream bug report against the quaternion package (moble/quaternion#270): a user's array-conjugation code that worked on numpy <= 2.4.6 began raising TypeError on 2.5.0. The same regression reproduces on NumPy's own in-tree rational dtype, so it affects any legacy user-defined dtype, not just quaternion.

Activity

  1. seberg commented on Jun 26, 2026

    @seberg
    Member

    Legacy user dtypes have no imag_meth (a NEP-42/43 slot) and are never flagged

    You actually can. This one is registered more like a ufunc so if you guard by NumPy 2.5+ you can do it. Admittedly, a bit annoying, because you have to manually define the one helper function if you compile for NumPy <2.4 (since the NumPy mechanism makes things safe, but that means it just doens't define "future" API for the most part).

    If you compile with NumPy 2.5 (or backport) and support only NumPy 2.0+. you can even migrate the new API. Sure, adds a bunch of verbosity unfortunately, but it allows you to define things like result_type()

  2. moble commented on Jul 9, 2026

    @moble
    ContributorAuthor

    Legacy user dtypes have no imag_meth (a NEP-42/43 slot) and are never flagged

    You actually can. This one is registered more like a ufunc so if you guard by NumPy 2.5+ you can do it.

    Can you point me to some docs for this, please?

  3. seberg commented on Jul 9, 2026

    @seberg
    Member

    https://numpy.org/doc/stable/reference/c-api/array.html#c.PyUFunc_AddLoopsFromSpecs but unless you put it into it's own compilation unit with NPY_TARGET_VERSION for NumPy 2.4+ you'll have to manually "backport" it. That's a bit raw/ugly, but not hard. Just make sure to guard it in some form (e.g. #ifndef PyUFunc_AddLoopsFromSpecs) and don't execute unless the PyArray_RUNTIME_VERSION is 2.5+.
    (Because you know it's correct now, but it might not be indefinitely.)

    Now, that isn't actually enough since you probably need to set the NUMERIC flag, I guess. The only official way to do that is to make it a new API + backcompat, maybe easier to look at an example: jax-ml/ml_dtypes#374 (that also goes to the trouble to backport so compiling against NumPy <2.5 works).

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