Repository: numpy/numpy
Pull request: ENH: Add array API standard v2022.12 support to numpy.array_api
<!--         ----------------------------------------------------------------
                MAKE SURE YOUR PR GETS THE ATTENTION IT DESERVES!
                ----------------------------------------------------------------

*  FORMAT IT RIGHT:
      https://www.numpy.org/devdocs/dev/development_workflow.html#writing-the-commit-message

*  IF IT'S A NEW FEATURE OR API CHANGE, TEST THE WATERS:
      https://www.numpy.org/devdocs/dev/development_workflow.html#get-the-mailing-list-s-opinion

*  HIT ALL THE GUIDELINES:
      https://numpy.org/devdocs/dev/index.html#guidelines

*  WHAT TO DO IF WE HAVEN'T GOTTEN BACK TO YOU:
      https://www.numpy.org/devdocs/dev/development_workflow.html#getting-your-pr-reviewed
-->
Fixes #23880

Problem statement:
ENH: Add 2022.12 support to numpy.array_api
### Proposed new feature or change:

This is a tracking issue for adding v2022.12 support to numpy.array_api. If possible this should happen before the 1.25 release (I hope I'm not too late for that), because that will make things much easier for people adopting the array API. 

The work should be straightforward. It mainly consists of making sure functions accept complex dtypes, although there are also a few new functions that need to be added (like `real` and `imag`).

Hints from issue discussion:
It's getting late for the 1.25.0 release, I'd like to get it out before the end of the month. However, as long as it only affects `array_api` I feel pretty free to backport it at any time, that is a work in progress and having it up to date for people working with it isn't a problem. Changes to the mainline api will need to wait.
Actually, there's a bit of an open question here. Do we want to support the `api_version='2021.12'` argument to [`__array_namespace__`](https://data-apis.org/array-api/latest/API_specification/generated/array_api.array.__array_namespace__.html#array_api.array.__array_namespace__)? That is, make it somehow return a version of the namespace corresponding to NumPy 1.24? I'm not sure if there's a simple way to do this. My initial thinking is we shouldn't attempt this, at least for now. If people want to test against an older version of the spec they can just use an older numpy.
This is *only* `numpy.array_api`. I'll work to get something ASAP (is there a branch I should be working against rather than `main`?)
> is there a branch I should be working against rather than main

No, just make it against main. After it goes in I can just copy the files into a PR against 1.25 at any time.
OK, I've implemented everything at https://github.com/numpy/numpy/pull/23881. There's just one open question about whether we want to try to add the fft extension, but other than that, everything is ready. We should also try to include https://github.com/numpy/numpy/pull/23789 in 1.25 too. 
NumPy's FFT is only 64bit, there was potentially something else with the way the custom length is handled (I don't quite remember).

I don't mind just pushing this through, the namespace has a warning and I wouldn't even worry about updates in a bug-fix release.

After that, maybe we should discuss removing the namespace, though?  The main reason for it to be here was that the NEP 47 draft hoped for it to be useful beyond testing, I think (and e.g. review also, yes).   As it has not proven to be used for more than testing right now, there is no user-facing reason for it to remain, I think.
Moving it out, would remove the burden of coordinating with NumPy releases.

We don't need to do it on a schedule, but to me it seems like this should happen eventually, I simply see no great reason for the opposite.
> NumPy's FFT is only 64bit, there was potentially something else with the way the custom length is handled (I don't quite remember).

OK, so that sounds like there is actually going to be some work involved then, so I'm not going to do it here. I don't want to stall this, and I would need to implement test suite support to feel confident about it. 

I wonder if I should add a simple `fft.py` with a `NotImplementedError` or something for now. 

> We don't need to do it on a schedule, but to me it seems like this should happen eventually, I simply see no great reason for the opposite.

I'm also open to that, although wouldn't it make the most sense to do it in NumPy 2.0 if we are going to do it?

Commit message: Add a note that we are skipping numpy.array_api.fft for now

Patch to review:
diff --git a/doc/release/upcoming_changes/23789.new_feature.rst b/doc/release/upcoming_changes/23789.new_feature.rst
new file mode 100644
index 000000000000..58158486c9f1
--- /dev/null
+++ b/doc/release/upcoming_changes/23789.new_feature.rst
@@ -0,0 +1,7 @@
+Array API v2022.12 support in ``numpy.array_api``
+-------------------------------------------------
+
+- ``numpy.array_api`` now full supports the `v2022.12 version
+  <https://data-apis.org/array-api/2022.12>`__ of the array API standard. Note
+  that this does not yet include the optional ``fft`` extension in the
+  standard.
diff --git a/numpy/array_api/__init__.py b/numpy/array_api/__init__.py
index e154b995298c..964873faab20 100644
--- a/numpy/array_api/__init__.py
+++ b/numpy/array_api/__init__.py
@@ -121,7 +121,7 @@
     "The numpy.array_api submodule is still experimental. See NEP 47.", stacklevel=2
 )
 
-__array_api_version__ = "2021.12"
+__array_api_version__ = "2022.12"
 
 __all__ = ["__array_api_version__"]
 
@@ -173,6 +173,7 @@
     broadcast_to,
     can_cast,
     finfo,
+    isdtype,
     iinfo,
     result_type,
 )
@@ -198,6 +199,8 @@
     uint64,
     float32,
     float64,
+    complex64,
+    complex128,
     bool,
 )
 
@@ -232,6 +235,7 @@
     bitwise_right_shift,
     bitwise_xor,
     ceil,
+    conj,
     cos,
     cosh,
     divide,
@@ -242,6 +246,7 @@
     floor_divide,
     greater,
     greater_equal,
+    imag,
     isfinite,
     isinf,
     isnan,
@@ -261,6 +266,7 @@
     not_equal,
     positive,
     pow,
+    real,
     remainder,
     round,
     sign,
diff --git a/numpy/array_api/_array_object.py b/numpy/array_api/_array_object.py
index a949b5977c25..c7eb2a0a361f 100644
--- a/numpy/array_api/_array_object.py
+++ b/numpy/array_api/_array_object.py
@@ -24,6 +24,7 @@
     _integer_dtypes,
     _integer_or_boolean_dtypes,
     _floating_dtypes,
+    _complex_floating_dtypes,
     _numeric_dtypes,
     _result_type,
     _dtype_categories,
@@ -139,7 +140,7 @@ def _check_allowed_dtypes(self, other: bool | int | float | Array, dtype_categor
 
         if self.dtype not in _dtype_categories[dtype_category]:
             raise TypeError(f"Only {dtype_category} dtypes are allowed in {op}")
-        if isinstance(other, (int, float, bool)):
+        if isinstance(other, (int, complex, float, bool)):
             other = self._promote_scalar(other)
         elif isinstance(other, Array):
             if other.dtype not in _dtype_categories[dtype_category]:
@@ -189,11 +190,23 @@ def _promote_scalar(self, scalar):
                 raise TypeError(
                     "Python int scalars cannot be promoted with bool arrays"
                 )
+            if self.dtype in _integer_dtypes:
+                info = np.iinfo(self.dtype)
+                if not (info.min <= scalar <= info.max):
+                    raise OverflowError(
+                        "Python int scalars must be within the bounds of the dtype for integer arrays"
+                    )
+            # int + array(floating) is allowed
         elif isinstance(scalar, float):
             if self.dtype not in _floating_dtypes:
                 raise TypeError(
                     "Python float scalars can only be promoted with floating-point arrays."
                 )
+        elif isinstance(scalar, complex):
+            if self.dtype not in _complex_floating_dtypes:
+                raise TypeError(
+                    "Python complex scalars can only be promoted with complex floating-point arrays."
+                )
         else:
             raise TypeError("'scalar' must be a Python scalar")
 
@@ -454,11 +467,19 @@ def __bool__(self: Array, /) -> bool:
         # Note: This is an error here.
         if self._array.ndim != 0:
             raise TypeError("bool is only allowed on arrays with 0 dimensions")
-        if self.dtype not in _boolean_dtypes:
-            raise ValueError("bool is only allowed on boolean arrays")
         res = self._array.__bool__()
         return res
 
+    def __complex__(self: Array, /) -> float:
+        """
+        Performs the operation __complex__.
+        """
+        # Note: This is an error here.
+        if self._array.ndim != 0:
+            raise TypeError("complex is only allowed on arrays with 0 dimensions")
+        res = self._array.__complex__()
+        return res
+
     def __dlpack__(self: Array, /, *, stream: None = None) -> PyCapsule:
         """
         Performs the operation __dlpack__.
@@ -492,8 +513,8 @@ def __float__(self: Array, /) -> float:
         # Note: This is an error here.
         if self._array.ndim != 0:
             raise TypeError("float is only allowed on arrays with 0 dimensions")
-        if self.dtype not in _floating_dtypes:
-            raise ValueError("float is only allowed on floating-point arrays")
+        if self.dtype in _complex_floating_dtypes:
+            raise TypeError("float is not allowed on complex floating-point arrays")
         res = self._array.__float__()
         return res
 
@@ -501,7 +522,7 @@ def __floordiv__(self: Array, other: Union[int, float, Array], /) -> Array:
         """
         Performs the operation __floordiv__.
         """
-        other = self._check_allowed_dtypes(other, "numeric", "__floordiv__")
+        other = self._check_allowed_dtypes(other, "real numeric", "__floordiv__")
         if other is NotImplemented:
             return other
         self, other = self._normalize_two_args(self, other)
@@ -512,7 +533,7 @@ def __ge__(self: Array, other: Union[int, float, Array], /) -> Array:
         """
         Performs the operation __ge__.
         """
-        other = self._check_allowed_dtypes(other, "numeric", "__ge__")
+        other = self._check_allowed_dtypes(other, "real numeric", "__ge__")
         if other is NotImplemented:
             return other
         self, other = self._normalize_two_args(self, other)
@@ -542,7 +563,7 @@ def __gt__(self: Array, other: Union[int, float, Array], /) -> Array:
         """
         Performs the operation __gt__.
         """
-        other = self._check_allowed_dtypes(other, "numeric", "__gt__")
+        other = self._check_allowed_dtypes(other, "real numeric", "__gt__")
         if other is NotImplemented:
             return other
         self, other = self._normalize_two_args(self, other)
@@ -556,8 +577,8 @@ def __int__(self: Array, /) -> int:
         # Note: This is an error here.
         if self._array.ndim != 0:
             raise TypeError("int is only allowed on arrays with 0 dimensions")
-        if self.dtype not in _integer_dtypes:
-            raise ValueError("int is only allowed on integer arrays")
+        if self.dtype in _complex_floating_dtypes:
+            raise TypeError("int is not allowed on complex floating-point arrays")
         res = self._array.__int__()
         return res
 
@@ -581,7 +602,7 @@ def __le__(self: Array, other: Union[int, float, Array], /) -> Array:
         """
         Performs the operation __le__.
         """
-        other = self._check_allowed_dtypes(other, "numeric", "__le__")
+        other = self._check_allowed_dtypes(other, "real numeric", "__le__")
         if other is NotImplemented:
             return other
         self, other = self._normalize_two_args(self, other)
@@ -603,7 +624,7 @@ def __lt__(self: Array, other: Union[int, float, Array], /) -> Array:
         """
         Performs the operation __lt__.
         """
-        other = self._check_allowed_dtypes(other, "numeric", "__lt__")
+        other = self._check_allowed_dtypes(other, "real numeric", "__lt__")
         if other is NotImplemented:
             return other
         self, other = self._normalize_two_args(self, other)
@@ -626,7 +647,7 @@ def __mod__(self: Array, other: Union[int, float, Array], /) -> Array:
         """
         Performs the operation __mod__.
         """
-        other = self._check_allowed_dtypes(other, "numeric", "__mod__")
+        other = self._check_allowed_dtypes(other, "real numeric", "__mod__")
         if other is NotImplemented:
             return other
         self, other = self._normalize_two_args(self, other)
@@ -808,7 +829,7 @@ def __ifloordiv__(self: Array, other: Union[int, float, Array], /) -> Array:
         """
         Performs the operation __ifloordiv__.
         """
-        other = self._check_allowed_dtypes(other, "numeric", "__ifloordiv__")
+        other = self._check_allowed_dtypes(other, "real numeric", "__ifloordiv__")
         if other is NotImplemented:
             return other
         self._array.__ifloordiv__(other._array)
@@ -818,7 +839,7 @@ def __rfloordiv__(self: Array, other: Union[int, float, Array], /) -> Array:
         """
         Performs the operation __rfloordiv__.
         """
-        other = self._check_allowed_dtypes(other, "numeric", "__rfloordiv__")
+        other = self._check_allowed_dtypes(other, "real numeric", "__rfloordiv__")
         if other is NotImplemented:
             return other
         self, other = self._normalize_two_args(self, other)
@@ -874,7 +895,7 @@ def __imod__(self: Array, other: Union[int, float, Array], /) -> Array:
         """
         Performs the operation __imod__.
         """
-        other = self._check_allowed_dtypes(other, "numeric", "__imod__")
+        other = self._check_allowed_dtypes(other, "real numeric", "__imod__")
         if other is NotImplemented:
             return other
         self._array.__imod__(other._array)
@@ -884,7 +905,7 @@ def __rmod__(self: Array, other: Union[int, float, Array], /) -> Array:
         """
         Performs the operation __rmod__.
         """
-        other = self._check_allowed_dtypes(other, "numeric", "__rmod__")
+        other = self._check_allowed_dtypes(other, "real numeric", "__rmod__")
         if other is NotImplemented:
             return other
         self, other = self._normalize_two_args(self, other)
diff --git a/numpy/array_api/_data_type_functions.py b/numpy/array_api/_data_type_functions.py
index 7026bd489563..6f972c3b5424 100644
--- a/numpy/array_api/_data_type_functions.py
+++ b/numpy/array_api/_data_type_functions.py
@@ -1,7 +1,17 @@
 from __future__ import annotations
 
 from ._array_object import Array
-from ._dtypes import _all_dtypes, _result_type
+from ._dtypes import (
+    _all_dtypes,
+    _boolean_dtypes,
+    _signed_integer_dtypes,
+    _unsigned_integer_dtypes,
+    _integer_dtypes,
+    _real_floating_dtypes,
+    _complex_floating_dtypes,
+    _numeric_dtypes,
+    _result_type,
+)
 
 from dataclasses import dataclass
 from typing import TYPE_CHECKING, List, Tuple, Union
@@ -80,6 +90,7 @@ class finfo_object:
     max: float
     min: float
     smallest_normal: float
+    dtype: Dtype
 
 
 @dataclass
@@ -87,6 +98,7 @@ class iinfo_object:
     bits: int
     max: int
     min: int
+    dtype: Dtype
 
 
 def finfo(type: Union[Dtype, Array], /) -> finfo_object:
@@ -104,6 +116,7 @@ def finfo(type: Union[Dtype, Array], /) -> finfo_object:
         float(fi.max),
         float(fi.min),
         float(fi.smallest_normal),
+        fi.dtype,
     )
 
 
@@ -114,9 +127,47 @@ def iinfo(type: Union[Dtype, Array], /) -> iinfo_object:
     See its docstring for more information.
     """
     ii = np.iinfo(type)
-    return iinfo_object(ii.bits, ii.max, ii.min)
+    return iinfo_object(ii.bits, ii.max, ii.min, ii.dtype)
 
 
+# Note: isdtype is a new function from the 2022.12 array API specification.
+def isdtype(
+    dtype: Dtype, kind: Union[Dtype, str, Tuple[Union[Dtype, str], ...]]
+) -> bool:
+    """
+    Returns a boolean indicating whether a provided dtype is of a specified data type ``kind``.
+
+    See
+    https://data-apis.org/array-api/latest/API_specification/generated/array_api.isdtype.html
+    for more details
+    """
+    if isinstance(kind, tuple):
+        # Disallow nested tuples
+        if any(isinstance(k, tuple) for k in kind):
+            raise TypeError("'kind' must be a dtype, str, or tuple of dtypes and strs")
+        return any(isdtype(dtype, k) for k in kind)
+    elif isinstance(kind, str):
+        if kind == 'bool':
+            return dtype in _boolean_dtypes
+        elif kind == 'signed integer':
+            return dtype in _signed_integer_dtypes
+        elif kind == 'unsigned integer':
+            return dtype in _unsigned_integer_dtypes
+        elif kind == 'integral':
+            return dtype in _integer_dtypes
+        elif kind == 'real floating':
+            return dtype in _real_floating_dtypes
+        elif kind == 'complex floating':
+            return dtype in _complex_floating_dtypes
+        elif kind == 'numeric':
+            return dtype in _numeric_dtypes
+        else:
+            raise ValueError(f"Unrecognized data type kind: {kind!r}")
+    elif kind in _all_dtypes:
+        return dtype == kind
+    else:
+        raise TypeError(f"'kind' must be a dtype, str, or tuple of dtypes and strs, not {type(kind).__name__}")
+
 def result_type(*arrays_and_dtypes: Union[Array, Dtype]) -> Dtype:
     """
     Array API compatible wrapper for :py:func:`np.result_type <numpy.result_type>`.
diff --git a/numpy/array_api/_dtypes.py b/numpy/array_api/_dtypes.py
index 476d619fee63..0e8f666eeedd 100644
--- a/numpy/array_api/_dtypes.py
+++ b/numpy/array_api/_dtypes.py
@@ -12,6 +12,8 @@
 uint64 = np.dtype("uint64")
 float32 = np.dtype("float32")
 float64 = np.dtype("float64")
+complex64 = np.dtype("complex64")
+complex128 = np.dtype("complex128")
 # Note: This name is changed
 bool = np.dtype("bool")
 
@@ -26,11 +28,17 @@
     uint64,
     float32,
     float64,
+    complex64,
+    complex128,
     bool,
 )
 _boolean_dtypes = (bool,)
-_floating_dtypes = (float32, float64)
+_real_floating_dtypes = (float32, float64)
+_floating_dtypes = (float32, float64, complex64, complex128)
+_complex_floating_dtypes = (complex64, complex128)
 _integer_dtypes = (int8, int16, int32, int64, uint8, uint16, uint32, uint64)
+_signed_integer_dtypes = (int8, int16, int32, int64)
+_unsigned_integer_dtypes = (uint8, uint16, uint32, uint64)
 _integer_or_boolean_dtypes = (
     bool,
     int8,
@@ -42,9 +50,23 @@
     uint32,
     uint64,
 )
+_real_numeric_dtypes = (
+    float32,
+    float64,
+    int8,
+    int16,
+    int32,
+    int64,
+    uint8,
+    uint16,
+    uint32,
+    uint64,
+)
 _numeric_dtypes = (
     float32,
     float64,
+    complex64,
+    complex128,
     int8,
     int16,
     int32,
@@ -57,10 +79,13 @@
 
 _dtype_categories = {
     "all": _all_dtypes,
+    "real numeric": _real_numeric_dtypes,
     "numeric": _numeric_dtypes,
     "integer": _integer_dtypes,
     "integer or boolean": _integer_or_boolean_dtypes,
     "boolean": _boolean_dtypes,
+    "real floating-point": _floating_dtypes,
+    "complex floating-point": _complex_floating_dtypes,
     "floating-point": _floating_dtypes,
 }
 
@@ -133,6 +158,18 @@
     (float32, float64): float64,
     (float64, float32): float64,
     (float64, float64): float64,
+    (complex64, complex64): complex64,
+    (complex64, complex128): complex128,
+    (complex128, complex64): complex128,
+    (complex128, complex128): complex128,
+    (float32, complex64): complex64,
+    (float32, complex128): complex128,
+    (float64, complex64): complex128,
+    (float64, complex128): complex128,
+    (complex64, float32): complex64,
+    (complex64, float64): complex128,
+    (complex128, float32): complex128,
+    (complex128, float64): complex128,
     (bool, bool): bool,
 }
 
diff --git a/numpy/array_api/_elementwise_functions.py b/numpy/array_api/_elementwise_functions.py
index c758a09447a0..8b696772f6dd 100644
--- a/numpy/array_api/_elementwise_functions.py
+++ b/numpy/array_api/_elementwise_functions.py
@@ -3,8 +3,11 @@
 from ._dtypes import (
     _boolean_dtypes,
     _floating_dtypes,
+    _real_floating_dtypes,
+    _complex_floating_dtypes,
     _integer_dtypes,
     _integer_or_boolean_dtypes,
+    _real_numeric_dtypes,
     _numeric_dtypes,
     _result_type,
 )
@@ -105,8 +108,8 @@ def atan2(x1: Array, x2: Array, /) -> Array:
 
     See its docstring for more information.
     """
-    if x1.dtype not in _floating_dtypes or x2.dtype not in _floating_dtypes:
-        raise TypeError("Only floating-point dtypes are allowed in atan2")
+    if x1.dtype not in _real_floating_dtypes or x2.dtype not in _real_floating_dtypes:
+        raise TypeError("Only real floating-point dtypes are allowed in atan2")
     # Call result type here just to raise on disallowed type combinations
     _result_type(x1.dtype, x2.dtype)
     x1, x2 = Array._normalize_two_args(x1, x2)
@@ -230,14 +233,25 @@ def ceil(x: Array, /) -> Array:
 
     See its docstring for more information.
     """
-    if x.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in ceil")
+    if x.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in ceil")
     if x.dtype in _integer_dtypes:
         # Note: The return dtype of ceil is the same as the input
         return x
     return Array._new(np.ceil(x._array))
 
 
+def conj(x: Array, /) -> Array:
+    """
+    Array API compatible wrapper for :py:func:`np.conj <numpy.conj>`.
+
+    See its docstring for more information.
+    """
+    if x.dtype not in _complex_floating_dtypes:
+        raise TypeError("Only complex floating-point dtypes are allowed in conj")
+    return Array._new(np.conj(x))
+
+
 def cos(x: Array, /) -> Array:
     """
     Array API compatible wrapper for :py:func:`np.cos <numpy.cos>`.
@@ -314,8 +328,8 @@ def floor(x: Array, /) -> Array:
 
     See its docstring for more information.
     """
-    if x.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in floor")
+    if x.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in floor")
     if x.dtype in _integer_dtypes:
         # Note: The return dtype of floor is the same as the input
         return x
@@ -328,8 +342,8 @@ def floor_divide(x1: Array, x2: Array, /) -> Array:
 
     See its docstring for more information.
     """
-    if x1.dtype not in _numeric_dtypes or x2.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in floor_divide")
+    if x1.dtype not in _real_numeric_dtypes or x2.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in floor_divide")
     # Call result type here just to raise on disallowed type combinations
     _result_type(x1.dtype, x2.dtype)
     x1, x2 = Array._normalize_two_args(x1, x2)
@@ -342,8 +356,8 @@ def greater(x1: Array, x2: Array, /) -> Array:
 
     See its docstring for more information.
     """
-    if x1.dtype not in _numeric_dtypes or x2.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in greater")
+    if x1.dtype not in _real_numeric_dtypes or x2.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in greater")
     # Call result type here just to raise on disallowed type combinations
     _result_type(x1.dtype, x2.dtype)
     x1, x2 = Array._normalize_two_args(x1, x2)
@@ -356,14 +370,25 @@ def greater_equal(x1: Array, x2: Array, /) -> Array:
 
     See its docstring for more information.
     """
-    if x1.dtype not in _numeric_dtypes or x2.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in greater_equal")
+    if x1.dtype not in _real_numeric_dtypes or x2.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in greater_equal")
     # Call result type here just to raise on disallowed type combinations
     _result_type(x1.dtype, x2.dtype)
     x1, x2 = Array._normalize_two_args(x1, x2)
     return Array._new(np.greater_equal(x1._array, x2._array))
 
 
+def imag(x: Array, /) -> Array:
+    """
+    Array API compatible wrapper for :py:func:`np.imag <numpy.imag>`.
+
+    See its docstring for more information.
+    """
+    if x.dtype not in _complex_floating_dtypes:
+        raise TypeError("Only complex floating-point dtypes are allowed in imag")
+    return Array._new(np.imag(x))
+
+
 def isfinite(x: Array, /) -> Array:
     """
     Array API compatible wrapper for :py:func:`np.isfinite <numpy.isfinite>`.
@@ -403,8 +428,8 @@ def less(x1: Array, x2: Array, /) -> Array:
 
     See its docstring for more information.
     """
-    if x1.dtype not in _numeric_dtypes or x2.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in less")
+    if x1.dtype not in _real_numeric_dtypes or x2.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in less")
     # Call result type here just to raise on disallowed type combinations
     _result_type(x1.dtype, x2.dtype)
     x1, x2 = Array._normalize_two_args(x1, x2)
@@ -417,8 +442,8 @@ def less_equal(x1: Array, x2: Array, /) -> Array:
 
     See its docstring for more information.
     """
-    if x1.dtype not in _numeric_dtypes or x2.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in less_equal")
+    if x1.dtype not in _real_numeric_dtypes or x2.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in less_equal")
     # Call result type here just to raise on disallowed type combinations
     _result_type(x1.dtype, x2.dtype)
     x1, x2 = Array._normalize_two_args(x1, x2)
@@ -475,8 +500,8 @@ def logaddexp(x1: Array, x2: Array) -> Array:
 
     See its docstring for more information.
     """
-    if x1.dtype not in _floating_dtypes or x2.dtype not in _floating_dtypes:
-        raise TypeError("Only floating-point dtypes are allowed in logaddexp")
+    if x1.dtype not in _real_floating_dtypes or x2.dtype not in _real_floating_dtypes:
+        raise TypeError("Only real floating-point dtypes are allowed in logaddexp")
     # Call result type here just to raise on disallowed type combinations
     _result_type(x1.dtype, x2.dtype)
     x1, x2 = Array._normalize_two_args(x1, x2)
@@ -599,14 +624,25 @@ def pow(x1: Array, x2: Array, /) -> Array:
     return Array._new(np.power(x1._array, x2._array))
 
 
+def real(x: Array, /) -> Array:
+    """
+    Array API compatible wrapper for :py:func:`np.real <numpy.real>`.
+
+    See its docstring for more information.
+    """
+    if x.dtype not in _complex_floating_dtypes:
+        raise TypeError("Only complex floating-point dtypes are allowed in real")
+    return Array._new(np.real(x))
+
+
 def remainder(x1: Array, x2: Array, /) -> Array:
     """
     Array API compatible wrapper for :py:func:`np.remainder <numpy.remainder>`.
 
     See its docstring for more information.
     """
-    if x1.dtype not in _numeric_dtypes or x2.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in remainder")
+    if x1.dtype not in _real_numeric_dtypes or x2.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in remainder")
     # Call result type here just to raise on disallowed type combinations
     _result_type(x1.dtype, x2.dtype)
     x1, x2 = Array._normalize_two_args(x1, x2)
@@ -721,8 +757,8 @@ def trunc(x: Array, /) -> Array:
 
     See its docstring for more information.
     """
-    if x.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in trunc")
+    if x.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in trunc")
     if x.dtype in _integer_dtypes:
         # Note: The return dtype of trunc is the same as the input
         return x
diff --git a/numpy/array_api/_searching_functions.py b/numpy/array_api/_searching_functions.py
index 40f5a4d2e8fc..a1f4b0c904c1 100644
--- a/numpy/array_api/_searching_functions.py
+++ b/numpy/array_api/_searching_functions.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 from ._array_object import Array
-from ._dtypes import _result_type
+from ._dtypes import _result_type, _real_numeric_dtypes
 
 from typing import Optional, Tuple
 
@@ -14,6 +14,8 @@ def argmax(x: Array, /, *, axis: Optional[int] = None, keepdims: bool = False) -
 
     See its docstring for more information.
     """
+    if x.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in argmax")
     return Array._new(np.asarray(np.argmax(x._array, axis=axis, keepdims=keepdims)))
 
 
@@ -23,6 +25,8 @@ def argmin(x: Array, /, *, axis: Optional[int] = None, keepdims: bool = False) -
 
     See its docstring for more information.
     """
+    if x.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in argmin")
     return Array._new(np.asarray(np.argmin(x._array, axis=axis, keepdims=keepdims)))
 
 
diff --git a/numpy/array_api/_sorting_functions.py b/numpy/array_api/_sorting_functions.py
index afbb412f7f5e..9b8cb044d88a 100644
--- a/numpy/array_api/_sorting_functions.py
+++ b/numpy/array_api/_sorting_functions.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 from ._array_object import Array
+from ._dtypes import _real_numeric_dtypes
 
 import numpy as np
 
@@ -14,6 +15,8 @@ def argsort(
 
     See its docstring for more information.
     """
+    if x.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in argsort")
     # Note: this keyword argument is different, and the default is different.
     kind = "stable" if stable else "quicksort"
     if not descending:
@@ -41,6 +44,8 @@ def sort(
 
     See its docstring for more information.
     """
+    if x.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in sort")
     # Note: this keyword argument is different, and the default is different.
     kind = "stable" if stable else "quicksort"
     res = np.sort(x._array, axis=axis, kind=kind)
diff --git a/numpy/array_api/_statistical_functions.py b/numpy/array_api/_statistical_functions.py
index 5bc831ac2965..98e31b51c115 100644
--- a/numpy/array_api/_statistical_functions.py
+++ b/numpy/array_api/_statistical_functions.py
@@ -1,12 +1,12 @@
 from __future__ import annotations
 
 from ._dtypes import (
-    _floating_dtypes,
+    _real_floating_dtypes,
+    _real_numeric_dtypes,
     _numeric_dtypes,
 )
 from ._array_object import Array
-from ._creation_functions import asarray
-from ._dtypes import float32, float64
+from ._dtypes import float32, float64, complex64, complex128
 
 from typing import TYPE_CHECKING, Optional, Tuple, Union
 
@@ -23,8 +23,8 @@ def max(
     axis: Optional[Union[int, Tuple[int, ...]]] = None,
     keepdims: bool = False,
 ) -> Array:
-    if x.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in max")
+    if x.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in max")
     return Array._new(np.max(x._array, axis=axis, keepdims=keepdims))
 
 
@@ -35,8 +35,8 @@ def mean(
     axis: Optional[Union[int, Tuple[int, ...]]] = None,
     keepdims: bool = False,
 ) -> Array:
-    if x.dtype not in _floating_dtypes:
-        raise TypeError("Only floating-point dtypes are allowed in mean")
+    if x.dtype not in _real_floating_dtypes:
+        raise TypeError("Only real floating-point dtypes are allowed in mean")
     return Array._new(np.mean(x._array, axis=axis, keepdims=keepdims))
 
 
@@ -47,8 +47,8 @@ def min(
     axis: Optional[Union[int, Tuple[int, ...]]] = None,
     keepdims: bool = False,
 ) -> Array:
-    if x.dtype not in _numeric_dtypes:
-        raise TypeError("Only numeric dtypes are allowed in min")
+    if x.dtype not in _real_numeric_dtypes:
+        raise TypeError("Only real numeric dtypes are allowed in min")
     return Array._new(np.min(x._array, axis=axis, keepdims=keepdims))
 
 
@@ -62,10 +62,14 @@ def prod(
 ) -> Array:
     if x.dtype not in _numeric_dtypes:
         raise TypeError("Only numeric dtypes are allowed in prod")
-    # Note: sum() and prod() always upcast float32 to float64 for dtype=None
-    # We need to do so here before computing the product to avoid overflow
-    if dtype is None and x.dtype == float32:
-        dtype = float64
+    # Note: sum() and prod() always upcast for dtype=None. `np.prod` does that
+    # for integers, but not for float32 or complex64, so we need to
+    # special-case it here
+    if dtype is None:
+        if x.dtype == float32:
+            dtype = float64
+        elif x.dtype == complex64:
+            dtype = complex128
     return Array._new(np.prod(x._array, dtype=dtype, axis=axis, keepdims=keepdims))
 
 
@@ -78,8 +82,8 @@ def std(
     keepdims: bool = False,
 ) -> Array:
     # Note: the keyword argument correction is different here
-    if x.dtype not in _floating_dtypes:
-        raise TypeError("Only floating-point dtypes are allowed in std")
+    if x.dtype not in _real_floating_dtypes:
+        raise TypeError("Only real floating-point dtypes are allowed in std")
     return Array._new(np.std(x._array, axis=axis, ddof=correction, keepdims=keepdims))
 
 
@@ -93,11 +97,14 @@ def sum(
 ) -> Array:
     if x.dtype not in _numeric_dtypes:
         raise TypeError("Only numeric dtypes are allowed in sum")
-    # Note: sum() and prod() always upcast integers to (u)int64 and float32 to
-    # float64 for dtype=None. `np.sum` does that too for integers, but not for
-    # float32, so we need to special-case it here
-    if dtype is None and x.dtype == float32:
-        dtype = float64
+    # Note: sum() and prod() always upcast for dtype=None. `np.sum` does that
+    # for integers, but not for float32 or complex64, so we need to
+    # special-case it here
+    if dtype is None:
+        if x.dtype == float32:
+            dtype = float64
+        elif x.dtype == complex64:
+            dtype = complex128
     return Array._new(np.sum(x._array, axis=axis, dtype=dtype, keepdims=keepdims))
 
 
@@ -110,6 +117,6 @@ def var(
     keepdims: bool = False,
 ) -> Array:
     # Note: the keyword argument correction is different here
-    if x.dtype not in _floating_dtypes:
-        raise TypeError("Only floating-point dtypes are allowed in var")
+    if x.dtype not in _real_floating_dtypes:
+        raise TypeError("Only real floating-point dtypes are allowed in var")
     return Array._new(np.var(x._array, axis=axis, ddof=correction, keepdims=keepdims))
diff --git a/numpy/array_api/linalg.py b/numpy/array_api/linalg.py
index d214046effd3..58320db55ceb 100644
--- a/numpy/array_api/linalg.py
+++ b/numpy/array_api/linalg.py
@@ -1,6 +1,13 @@
 from __future__ import annotations
 
-from ._dtypes import _floating_dtypes, _numeric_dtypes
+from ._dtypes import (
+    _floating_dtypes,
+    _numeric_dtypes,
+    float32,
+    float64,
+    complex64,
+    complex128
+)
 from ._manipulation_functions import reshape
 from ._array_object import Array
 
@@ -8,7 +15,7 @@
 
 from typing import TYPE_CHECKING
 if TYPE_CHECKING:
-    from ._typing import Literal, Optional, Sequence, Tuple, Union
+    from ._typing import Literal, Optional, Sequence, Tuple, Union, Dtype
 
 from typing import NamedTuple
 
@@ -363,7 +370,7 @@ def tensordot(x1: Array, x2: Array, /, *, axes: Union[int, Tuple[Sequence[int],
     return Array._new(np.tensordot(x1._array, x2._array, axes=axes))
 
 # Note: trace is the numpy top-level namespace, not np.linalg
-def trace(x: Array, /, *, offset: int = 0) -> Array:
+def trace(x: Array, /, *, offset: int = 0, dtype: Optional[Dtype] = None) -> Array:
     """
     Array API compatible wrapper for :py:func:`np.trace <numpy.trace>`.
 
@@ -371,9 +378,17 @@ def trace(x: Array, /, *, offset: int = 0) -> Array:
     """
     if x.dtype not in _numeric_dtypes:
         raise TypeError('Only numeric dtypes are allowed in trace')
+
+    # Note: trace() works the same as sum() and prod() (see
+    # _statistical_functions.py)
+    if dtype is None:
+        if x.dtype == float32:
+            dtype = float64
+        elif x.dtype == complex64:
+            dtype = complex128
     # Note: trace always operates on the last two axes, whereas np.trace
     # operates on the first two axes by default
-    return Array._new(np.asarray(np.trace(x._array, offset=offset, axis1=-2, axis2=-1)))
+    return Array._new(np.asarray(np.trace(x._array, offset=offset, axis1=-2, axis2=-1, dtype=dtype)))
 
 # Note: vecdot is not in NumPy
 def vecdot(x1: Array, x2: Array, /, *, axis: int = -1) -> Array:
diff --git a/numpy/array_api/tests/test_array_object.py b/numpy/array_api/tests/test_array_object.py
index f6efacefaee1..0feb72c4ea33 100644
--- a/numpy/array_api/tests/test_array_object.py
+++ b/numpy/array_api/tests/test_array_object.py
@@ -1,6 +1,6 @@
 import operator
 
-from numpy.testing import assert_raises
+from numpy.testing import assert_raises, suppress_warnings
 import numpy as np
 import pytest
 
@@ -9,9 +9,12 @@
 from .._dtypes import (
     _all_dtypes,
     _boolean_dtypes,
+    _real_floating_dtypes,
     _floating_dtypes,
+    _complex_floating_dtypes,
     _integer_dtypes,
     _integer_or_boolean_dtypes,
+    _real_numeric_dtypes,
     _numeric_dtypes,
     int8,
     int16,
@@ -85,13 +88,13 @@ def test_operators():
         "__add__": "numeric",
         "__and__": "integer_or_boolean",
         "__eq__": "all",
-        "__floordiv__": "numeric",
-        "__ge__": "numeric",
-        "__gt__": "numeric",
-        "__le__": "numeric",
+        "__floordiv__": "real numeric",
+        "__ge__": "real numeric",
+        "__gt__": "real numeric",
+        "__le__": "real numeric",
         "__lshift__": "integer",
-        "__lt__": "numeric",
-        "__mod__": "numeric",
+        "__lt__": "real numeric",
+        "__mod__": "real numeric",
         "__mul__": "numeric",
         "__ne__": "all",
         "__or__": "integer_or_boolean",
@@ -101,7 +104,6 @@ def test_operators():
         "__truediv__": "floating",
         "__xor__": "integer_or_boolean",
     }
-
     # Recompute each time because of in-place ops
     def _array_vals():
         for d in _integer_dtypes:
@@ -111,13 +113,15 @@ def _array_vals():
         for d in _floating_dtypes:
             yield asarray(1.0, dtype=d)
 
+
+    BIG_INT = int(1e30)
     for op, dtypes in binary_op_dtypes.items():
         ops = [op]
         if op not in ["__eq__", "__ne__", "__le__", "__ge__", "__lt__", "__gt__"]:
             rop = "__r" + op[2:]
             iop = "__i" + op[2:]
             ops += [rop, iop]
-        for s in [1, 1.0, False]:
+        for s in [1, 1.0, 1j, BIG_INT, False]:
             for _op in ops:
                 for a in _array_vals():
                     # Test array op scalar. From the spec, the following combinations
@@ -125,13 +129,12 @@ def _array_vals():
 
                     # - Python bool for a bool array dtype,
                     # - a Python int within the bounds of the given dtype for integer array dtypes,
-                    # - a Python int or float for floating-point array dtypes
-
-                    # We do not do bounds checking for int scalars, but rather use the default
-                    # NumPy behavior for casting in that case.
+                    # - a Python int or float for real floating-point array dtypes
+                    # - a Python int, float, or complex for complex floating-point array dtypes
 
                     if ((dtypes == "all"
                          or dtypes == "numeric" and a.dtype in _numeric_dtypes
+                         or dtypes == "real numeric" and a.dtype in _real_numeric_dtypes
                          or dtypes == "integer" and a.dtype in _integer_dtypes
                          or dtypes == "integer_or_boolean" and a.dtype in _integer_or_boolean_dtypes
                          or dtypes == "boolean" and a.dtype in _boolean_dtypes
@@ -141,10 +144,18 @@ def _array_vals():
                         # isinstance here.
                         and (a.dtype in _boolean_dtypes and type(s) == bool
                              or a.dtype in _integer_dtypes and type(s) == int
-                             or a.dtype in _floating_dtypes and type(s) in [float, int]
+                             or a.dtype in _real_floating_dtypes and type(s) in [float, int]
+                             or a.dtype in _complex_floating_dtypes and type(s) in [complex, float, int]
                         )):
-                        # Only test for no error
-                        getattr(a, _op)(s)
+                        if a.dtype in _integer_dtypes and s == BIG_INT:
+                            assert_raises(OverflowError, lambda: getattr(a, _op)(s))
+                        else:
+                            # Only test for no error
+                            with suppress_warnings() as sup:
+                                # ignore warnings from pow(BIG_INT)
+                                sup.filter(RuntimeWarning,
+                                           "invalid value encountered in power")
+                                getattr(a, _op)(s)
                     else:
                         assert_raises(TypeError, lambda: getattr(a, _op)(s))
 
@@ -174,8 +185,9 @@ def _array_vals():
                             # Ensure only those dtypes that are required for every operator are allowed.
                             elif (dtypes == "all" and (x.dtype in _boolean_dtypes and y.dtype in _boolean_dtypes
                                                       or x.dtype in _numeric_dtypes and y.dtype in _numeric_dtypes)
+                                or (dtypes == "real numeric" and x.dtype in _real_numeric_dtypes and y.dtype in _real_numeric_dtypes)
                                 or (dtypes == "numeric" and x.dtype in _numeric_dtypes and y.dtype in _numeric_dtypes)
-                                or dtypes == "integer" and x.dtype in _integer_dtypes and y.dtype in _numeric_dtypes
+                                or dtypes == "integer" and x.dtype in _integer_dtypes and y.dtype in _integer_dtypes
                                 or dtypes == "integer_or_boolean" and (x.dtype in _integer_dtypes and y.dtype in _integer_dtypes
                                                                        or x.dtype in _boolean_dtypes and y.dtype in _boolean_dtypes)
                                 or dtypes == "boolean" and x.dtype in _boolean_dtypes and y.dtype in _boolean_dtypes
@@ -263,31 +275,39 @@ def test_python_scalar_construtors():
     b = asarray(False)
     i = asarray(0)
     f = asarray(0.0)
+    c = asarray(0j)
 
     assert bool(b) == False
     assert int(i) == 0
     assert float(f) == 0.0
     assert operator.index(i) == 0
 
-    # bool/int/float should only be allowed on 0-D arrays.
+    # bool/int/float/complex should only be allowed on 0-D arrays.
     assert_raises(TypeError, lambda: bool(asarray([False])))
     assert_raises(TypeError, lambda: int(asarray([0])))
     assert_raises(TypeError, lambda: float(asarray([0.0])))
+    assert_raises(TypeError, lambda: complex(asarray([0j])))
     assert_raises(TypeError, lambda: operator.index(asarray([0])))
 
-    # bool/int/float should only be allowed on arrays of the corresponding
-    # dtype
-    assert_raises(ValueError, lambda: bool(i))
-    assert_raises(ValueError, lambda: bool(f))
+    # bool should work on all types of arrays
+    assert bool(b) is bool(i) is bool(f) is bool(c) is False
+
+    # int should fail on complex arrays
+    assert int(b) == int(i) == int(f) == 0
+    assert_raises(TypeError, lambda: int(c))
 
-    assert_raises(ValueError, lambda: int(b))
-    assert_raises(ValueError, lambda: int(f))
+    # float should fail on complex arrays
+    assert float(b) == float(i) == float(f) == 0.0
+    assert_raises(TypeError, lambda: float(c))
 
-    assert_raises(ValueError, lambda: float(b))
-    assert_raises(ValueError, lambda: float(i))
+    # complex should work on all types of arrays
+    assert complex(b) == complex(i) == complex(f) == complex(c) == 0j
 
+    # index should only work on integer arrays
+    assert operator.index(i) == 0
     assert_raises(TypeError, lambda: operator.index(b))
     assert_raises(TypeError, lambda: operator.index(f))
+    assert_raises(TypeError, lambda: operator.index(c))
 
 
 def test_device_property():
diff --git a/numpy/array_api/tests/test_data_type_functions.py b/numpy/array_api/tests/test_data_type_functions.py
index efe3d0abd4be..61d56ca45b1e 100644
--- a/numpy/array_api/tests/test_data_type_functions.py
+++ b/numpy/array_api/tests/test_data_type_functions.py
@@ -1,7 +1,8 @@
 import pytest
 
+from numpy.testing import assert_raises
 from numpy import array_api as xp
-
+import numpy as np
 
 @pytest.mark.parametrize(
     "from_, to, expected",
@@ -17,3 +18,14 @@ def test_can_cast(from_, to, expected):
     can_cast() returns correct result
     """
     assert xp.can_cast(from_, to) == expected
+
+def test_isdtype_strictness():
+    assert_raises(TypeError, lambda: xp.isdtype(xp.float64, 64))
+    assert_raises(ValueError, lambda: xp.isdtype(xp.float64, 'f8'))
+
+    assert_raises(TypeError, lambda: xp.isdtype(xp.float64, (('integral',),)))
+    assert_raises(TypeError, lambda: xp.isdtype(xp.float64, np.object_))
+
+    # TODO: These will require https://github.com/numpy/numpy/issues/23883
+    # assert_raises(TypeError, lambda: xp.isdtype(xp.float64, None))
+    # assert_raises(TypeError, lambda: xp.isdtype(xp.float64, np.float64))
diff --git a/numpy/array_api/tests/test_elementwise_functions.py b/numpy/array_api/tests/test_elementwise_functions.py
index b2fb44e766f8..1228d0af2e6a 100644
--- a/numpy/array_api/tests/test_elementwise_functions.py
+++ b/numpy/array_api/tests/test_elementwise_functions.py
@@ -29,7 +29,7 @@ def test_function_types():
         "asin": "floating-point",
         "asinh": "floating-point",
         "atan": "floating-point",
-        "atan2": "floating-point",
+        "atan2": "real floating-point",
         "atanh": "floating-point",
         "bitwise_and": "integer or boolean",
         "bitwise_invert": "integer or boolean",
@@ -37,24 +37,26 @@ def test_function_types():
         "bitwise_or": "integer or boolean",
         "bitwise_right_shift": "integer",
         "bitwise_xor": "integer or boolean",
-        "ceil": "numeric",
+        "ceil": "real numeric",
+        "conj": "complex floating-point",
         "cos": "floating-point",
         "cosh": "floating-point",
         "divide": "floating-point",
         "equal": "all",
         "exp": "floating-point",
         "expm1": "floating-point",
-        "floor": "numeric",
-        "floor_divide": "numeric",
-        "greater": "numeric",
-        "greater_equal": "numeric",
+        "floor": "real numeric",
+        "floor_divide": "real numeric",
+        "greater": "real numeric",
+        "greater_equal": "real numeric",
+        "imag": "complex floating-point",
         "isfinite": "numeric",
         "isinf": "numeric",
         "isnan": "numeric",
-        "less": "numeric",
-        "less_equal": "numeric",
+        "less": "real numeric",
+        "less_equal": "real numeric",
         "log": "floating-point",
-        "logaddexp": "floating-point",
+        "logaddexp": "real floating-point",
         "log10": "floating-point",
         "log1p": "floating-point",
         "log2": "floating-point",
@@ -67,7 +69,8 @@ def test_function_types():
         "not_equal": "all",
         "positive": "numeric",
         "pow": "numeric",
-        "remainder": "numeric",
+        "real": "complex floating-point",
+        "remainder": "real numeric",
         "round": "numeric",
         "sign": "numeric",
         "sin": "floating-point",
@@ -77,7 +80,7 @@ def test_function_types():
         "subtract": "numeric",
         "tan": "floating-point",
         "tanh": "floating-point",
-        "trunc": "numeric",
+        "trunc": "real numeric",
     }
 
     def _array_vals():


Code review comments:
diff_hunk: @@ -454,11 +467,19 @@ def __bool__(self: Array, /) -> bool:
         # Note: This is an error here.
         if self._array.ndim != 0:
             raise TypeError("bool is only allowed on arrays with 0 dimensions")
-        if self.dtype not in _boolean_dtypes:
-            raise ValueError("bool is only allowed on boolean arrays")
         res = self._array.__bool__()
         return res
 
+    def __complex__(self: Array, /) -> float:
path: numpy/array_api/_array_object.py
text: @user1:
```suggestion
    def __complex__(self: Array, /) -> complex:
```