From 4828760a48270f8fcf2a8b0bc69fd5ba6c427e1b Mon Sep 17 00:00:00 2001 From: Karan Dhaodiyal <256503836+karandhaodiyal28-hash@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:52:51 +0530 Subject: [PATCH] BUG: return scalar types from dtypes() and default_dtypes() __array_namespace_info__().dtypes() and .default_dtypes() returned np.dtype objects instead of the scalar types. That causes two problems: - np.dtype(None) is float64 in NumPy, so dtypes()["float64"] == None is True and a caller cannot use None to mean "this dtype is unsupported". - np.dtype objects have no __name__, so the returned values cannot be introspected at all. This affects every dtype, not just float64. Both methods already document the scalar types in their docstrings, so this makes the code match its own documentation. The cupy and dask backends have the same pattern and already return a bare `bool` for the bool entry, so the scalar type is the intended convention here too. This only changes the numpy backend; cupy and dask are left alone since they cannot be exercised from the numpy-only test environment. Fixes gh-359 --- src/array_api_compat/numpy/_info.py | 101 ++++++++++++++-------------- tests/test_numpy.py | 23 +++++++ 2 files changed, 73 insertions(+), 51 deletions(-) diff --git a/src/array_api_compat/numpy/_info.py b/src/array_api_compat/numpy/_info.py index 9ba004da..d5fd5d7c 100644 --- a/src/array_api_compat/numpy/_info.py +++ b/src/array_api_compat/numpy/_info.py @@ -13,7 +13,6 @@ from numpy import ( complex64, complex128, - dtype, float32, float64, int8, @@ -186,10 +185,10 @@ def default_dtypes( f' {device}' ) return { - "real floating": dtype(float64), - "complex floating": dtype(complex128), - "integral": dtype(intp), - "indexing": dtype(intp), + "real floating": float64, + "complex floating": complex128, + "integral": intp, + "indexing": intp, } def dtypes( @@ -259,71 +258,71 @@ def dtypes( ) if kind is None: return { - "bool": dtype(bool), - "int8": dtype(int8), - "int16": dtype(int16), - "int32": dtype(int32), - "int64": dtype(int64), - "uint8": dtype(uint8), - "uint16": dtype(uint16), - "uint32": dtype(uint32), - "uint64": dtype(uint64), - "float32": dtype(float32), - "float64": dtype(float64), - "complex64": dtype(complex64), - "complex128": dtype(complex128), + "bool": bool, + "int8": int8, + "int16": int16, + "int32": int32, + "int64": int64, + "uint8": uint8, + "uint16": uint16, + "uint32": uint32, + "uint64": uint64, + "float32": float32, + "float64": float64, + "complex64": complex64, + "complex128": complex128, } if kind == "bool": - return {"bool": dtype(bool)} + return {"bool": bool} if kind == "signed integer": return { - "int8": dtype(int8), - "int16": dtype(int16), - "int32": dtype(int32), - "int64": dtype(int64), + "int8": int8, + "int16": int16, + "int32": int32, + "int64": int64, } if kind == "unsigned integer": return { - "uint8": dtype(uint8), - "uint16": dtype(uint16), - "uint32": dtype(uint32), - "uint64": dtype(uint64), + "uint8": uint8, + "uint16": uint16, + "uint32": uint32, + "uint64": uint64, } if kind == "integral": return { - "int8": dtype(int8), - "int16": dtype(int16), - "int32": dtype(int32), - "int64": dtype(int64), - "uint8": dtype(uint8), - "uint16": dtype(uint16), - "uint32": dtype(uint32), - "uint64": dtype(uint64), + "int8": int8, + "int16": int16, + "int32": int32, + "int64": int64, + "uint8": uint8, + "uint16": uint16, + "uint32": uint32, + "uint64": uint64, } if kind == "real floating": return { - "float32": dtype(float32), - "float64": dtype(float64), + "float32": float32, + "float64": float64, } if kind == "complex floating": return { - "complex64": dtype(complex64), - "complex128": dtype(complex128), + "complex64": complex64, + "complex128": complex128, } if kind == "numeric": return { - "int8": dtype(int8), - "int16": dtype(int16), - "int32": dtype(int32), - "int64": dtype(int64), - "uint8": dtype(uint8), - "uint16": dtype(uint16), - "uint32": dtype(uint32), - "uint64": dtype(uint64), - "float32": dtype(float32), - "float64": dtype(float64), - "complex64": dtype(complex64), - "complex128": dtype(complex128), + "int8": int8, + "int16": int16, + "int32": int32, + "int64": int64, + "uint8": uint8, + "uint16": uint16, + "uint32": uint32, + "uint64": uint64, + "float32": float32, + "float64": float64, + "complex64": complex64, + "complex128": complex128, } if isinstance(kind, tuple): res: dict[str, DType] = {} diff --git a/tests/test_numpy.py b/tests/test_numpy.py index f8d45e2d..b020d3e3 100644 --- a/tests/test_numpy.py +++ b/tests/test_numpy.py @@ -154,3 +154,26 @@ def test_reshape_copy_false_returns_a_view_without_deprecation(): def test_reshape_copy_false_rejects_an_input_that_needs_a_copy(): with pytest.raises((ValueError, AttributeError)): xp.reshape(np.arange(12).reshape(3, 4).T, (2, 6), copy=False) + + +def test_inspection_dtypes_do_not_compare_equal_to_none(): + """`dtypes()`/`default_dtypes()` must return the scalar types, not + `np.dtype` objects. gh-359. + + `np.dtype(None)` is `float64` in NumPy, so `dtypes()["float64"] == None` + is True and a caller cannot tell "unsupported" from "supported". And + `np.dtype` objects have no `__name__`, so they are unusable for the + ordinary introspection callers do on the returned values. + """ + info = xp.__array_namespace_info__() + + assert info.dtypes()["float64"] is np.float64 + assert info.default_dtypes()["real floating"] is np.float64 + + for dt in info.dtypes().values(): + assert not (dt == None) # noqa: E711 + assert isinstance(dt.__name__, str) + + for dt in info.default_dtypes().values(): + assert not (dt == None) # noqa: E711 + assert isinstance(dt.__name__, str)