Repository navigation
Description
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.6Error 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 dtypePython 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
seberg commentedon Jun 26, 2026
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()moble commentedon Jul 9, 2026
Can you point me to some docs for this, please?
seberg commentedon Jul 9, 2026
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_VERSIONfor 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 thePyArray_RUNTIME_VERSIONis 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
NUMERICflag, 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).