From 5845809d82d9f4895954a871e27c9b2e39acd79e Mon Sep 17 00:00:00 2001 From: Bryn Pickering <17178478+brynpickering@users.noreply.github.com> Date: Tue, 20 May 2025 15:50:48 +0100 Subject: [PATCH 01/12] Coerce unknown types to O dtype --- xarray/compat/array_api_compat.py | 13 +++++++++---- xarray/tests/test_dtypes.py | 4 ++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index e1e5d5c5bdc..1b6c440cc7a 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -38,10 +38,15 @@ def _future_array_api_result_type(*arrays_and_dtypes, xp): def result_type(*arrays_and_dtypes, xp) -> np.dtype: - if xp is np or any( - isinstance(getattr(t, "dtype", t), np.dtype) for t in arrays_and_dtypes - ): - return xp.result_type(*arrays_and_dtypes) + is_np_dtype = [ + hasattr(t, "dtype") and isinstance(t.dtype, np.dtype) for t in arrays_and_dtypes + ] + if xp is np or any(is_np_dtype): + all_valid_arrays_and_dtypes = [ + t if is_np_dtype[i] or t.__class__.__module__ == "builtins" else np.object_ + for i, t in enumerate(arrays_and_dtypes) + ] + return xp.result_type(*all_valid_arrays_and_dtypes) else: return _future_array_api_result_type(*arrays_and_dtypes, xp=xp) diff --git a/xarray/tests/test_dtypes.py b/xarray/tests/test_dtypes.py index 0ccda1d8074..9b76e7d431a 100644 --- a/xarray/tests/test_dtypes.py +++ b/xarray/tests/test_dtypes.py @@ -32,6 +32,10 @@ class DummyArrayAPINamespace: ([np.dtype(" None: From ef3df17dbf4c7e209a820cc3d9a54918a513d2a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 14:57:50 +0000 Subject: [PATCH 02/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/tests/test_dtypes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xarray/tests/test_dtypes.py b/xarray/tests/test_dtypes.py index 9b76e7d431a..3b9c60517f9 100644 --- a/xarray/tests/test_dtypes.py +++ b/xarray/tests/test_dtypes.py @@ -32,10 +32,10 @@ class DummyArrayAPINamespace: ([np.dtype(" None: From 1756bc4ba9bb4a05f85b66de3ca07b2a3d7c129b Mon Sep 17 00:00:00 2001 From: Bryn Pickering <17178478+brynpickering@users.noreply.github.com> Date: Fri, 23 May 2025 14:34:39 +0100 Subject: [PATCH 03/12] Add comment on setting obj dtype --- xarray/compat/array_api_compat.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index 1b6c440cc7a..166a23ba2f8 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -42,6 +42,9 @@ def result_type(*arrays_and_dtypes, xp) -> np.dtype: hasattr(t, "dtype") and isinstance(t.dtype, np.dtype) for t in arrays_and_dtypes ] if xp is np or any(is_np_dtype): + # Numpy can only apply type promotion rules on non-builtin & numpy-compatible Python objects. + # So we convert any incompatible objects (e.g. user-defined class instances) stored in the array to numpy Object dtype. + # Then, numpy's type promotion will fall back to Object dtype rather than raising an exception. all_valid_arrays_and_dtypes = [ t if is_np_dtype[i] or t.__class__.__module__ == "builtins" else np.object_ for i, t in enumerate(arrays_and_dtypes) From 8d15d2fddcc13315532a7f98ffd7a3d1e9eb5c20 Mon Sep 17 00:00:00 2001 From: Bryn Pickering <17178478+brynpickering@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:54:32 +0000 Subject: [PATCH 04/12] Update xarray/compat/array_api_compat.py Co-authored-by: Justus Magin --- xarray/compat/array_api_compat.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index 166a23ba2f8..6ba4e6ca060 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -42,14 +42,10 @@ def result_type(*arrays_and_dtypes, xp) -> np.dtype: hasattr(t, "dtype") and isinstance(t.dtype, np.dtype) for t in arrays_and_dtypes ] if xp is np or any(is_np_dtype): - # Numpy can only apply type promotion rules on non-builtin & numpy-compatible Python objects. - # So we convert any incompatible objects (e.g. user-defined class instances) stored in the array to numpy Object dtype. - # Then, numpy's type promotion will fall back to Object dtype rather than raising an exception. - all_valid_arrays_and_dtypes = [ - t if is_np_dtype[i] or t.__class__.__module__ == "builtins" else np.object_ - for i, t in enumerate(arrays_and_dtypes) - ] - return xp.result_type(*all_valid_arrays_and_dtypes) + builtin_types = (bool, int, float, complex, str, bytes, dt.datetime, dt.timedelta) + if any(not is_numpy and not isinstance(t, builtin_types) for is_numpy, t in zip(is_np_dtype, arrays_and_types)): + return np.object_ + return xp.result_type(*arrays_and_dtypes) else: return _future_array_api_result_type(*arrays_and_dtypes, xp=xp) From 5fc7ccc5586af85707d0234ec2d31afdc43f056a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:55:54 +0000 Subject: [PATCH 05/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/compat/array_api_compat.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index 6ba4e6ca060..0428d59536b 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -42,8 +42,20 @@ def result_type(*arrays_and_dtypes, xp) -> np.dtype: hasattr(t, "dtype") and isinstance(t.dtype, np.dtype) for t in arrays_and_dtypes ] if xp is np or any(is_np_dtype): - builtin_types = (bool, int, float, complex, str, bytes, dt.datetime, dt.timedelta) - if any(not is_numpy and not isinstance(t, builtin_types) for is_numpy, t in zip(is_np_dtype, arrays_and_types)): + builtin_types = ( + bool, + int, + float, + complex, + str, + bytes, + dt.datetime, + dt.timedelta, + ) + if any( + not is_numpy and not isinstance(t, builtin_types) + for is_numpy, t in zip(is_np_dtype, arrays_and_types) + ): return np.object_ return xp.result_type(*arrays_and_dtypes) else: From 22ab662ffccec312569fc1a1f330fdfbbd1356ac Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 12 Nov 2025 13:22:26 +0100 Subject: [PATCH 06/12] import datetime --- xarray/compat/array_api_compat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index 0428d59536b..3b89e27cbf6 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -1,3 +1,5 @@ +import datetime as dt + import numpy as np from xarray.namedarray.pycompat import array_type From 4cea3fa98dbf910e1a665cf5120b23f34011d346 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 12 Nov 2025 13:22:44 +0100 Subject: [PATCH 07/12] typo --- xarray/compat/array_api_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index 3b89e27cbf6..cfe6161afad 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -56,7 +56,7 @@ def result_type(*arrays_and_dtypes, xp) -> np.dtype: ) if any( not is_numpy and not isinstance(t, builtin_types) - for is_numpy, t in zip(is_np_dtype, arrays_and_types) + for is_numpy, t in zip(is_np_dtype, arrays_and_dtypes) ): return np.object_ return xp.result_type(*arrays_and_dtypes) From 8f267686e3737de532343c81c3cbab2b5f4d3094 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 12 Nov 2025 13:22:55 +0100 Subject: [PATCH 08/12] use `zip` with `strict=True` --- xarray/compat/array_api_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index cfe6161afad..069b26cf48d 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -56,7 +56,7 @@ def result_type(*arrays_and_dtypes, xp) -> np.dtype: ) if any( not is_numpy and not isinstance(t, builtin_types) - for is_numpy, t in zip(is_np_dtype, arrays_and_dtypes) + for is_numpy, t in zip(is_np_dtype, arrays_and_dtypes, strict=True) ): return np.object_ return xp.result_type(*arrays_and_dtypes) From 46ae1132a39bfa074d234049ecb62693a6da3857 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 12 Nov 2025 14:25:30 +0100 Subject: [PATCH 09/12] refactor the dtype checks into functions --- xarray/compat/array_api_compat.py | 41 ++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index 069b26cf48d..6a4a0948a69 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -4,6 +4,17 @@ from xarray.namedarray.pycompat import array_type +builtin_types = ( + bool, + int, + float, + complex, + str, + bytes, + dt.datetime, + dt.timedelta, +) + def is_weak_scalar_type(t): return isinstance(t, bool | int | float | complex | str | bytes) @@ -39,23 +50,29 @@ def _future_array_api_result_type(*arrays_and_dtypes, xp): return xp.result_type(dtype, *dtypes) +def is_builtin_type(value): + if isinstance(value, type): + return issubclass(value, builtin_types) + else: + return isinstance(value, builtin_types) + + +def is_dtype(value): + if isinstance(value, type): + return issubclass(value, np.generic) + else: + return isinstance(value, np.dtype) + + def result_type(*arrays_and_dtypes, xp) -> np.dtype: is_np_dtype = [ - hasattr(t, "dtype") and isinstance(t.dtype, np.dtype) for t in arrays_and_dtypes + is_dtype(getattr(t, "dtype", t) if not isinstance(t, type) else t) + for t in arrays_and_dtypes ] + if xp is np or any(is_np_dtype): - builtin_types = ( - bool, - int, - float, - complex, - str, - bytes, - dt.datetime, - dt.timedelta, - ) if any( - not is_numpy and not isinstance(t, builtin_types) + not is_numpy and not is_builtin_type(t) for is_numpy, t in zip(is_np_dtype, arrays_and_dtypes, strict=True) ): return np.object_ From 788eedbabeb47c274272153cd3f8f93c5c0332cd Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 12 Nov 2025 14:38:53 +0100 Subject: [PATCH 10/12] return a dtype object instead of the dtype type --- xarray/compat/array_api_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index 6a4a0948a69..67507419bfa 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -75,7 +75,7 @@ def result_type(*arrays_and_dtypes, xp) -> np.dtype: not is_numpy and not is_builtin_type(t) for is_numpy, t in zip(is_np_dtype, arrays_and_dtypes, strict=True) ): - return np.object_ + return np.dtype("object") return xp.result_type(*arrays_and_dtypes) else: return _future_array_api_result_type(*arrays_and_dtypes, xp=xp) From e51a8c415fb72beaa860f16c7a5f6ce29d5b0a14 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 11 Feb 2026 20:08:04 +0100 Subject: [PATCH 11/12] revert the changes to `result_type` --- xarray/compat/array_api_compat.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index 67507419bfa..43bfe3153d9 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -65,20 +65,15 @@ def is_dtype(value): def result_type(*arrays_and_dtypes, xp) -> np.dtype: - is_np_dtype = [ - is_dtype(getattr(t, "dtype", t) if not isinstance(t, type) else t) - for t in arrays_and_dtypes - ] - - if xp is np or any(is_np_dtype): - if any( - not is_numpy and not is_builtin_type(t) - for is_numpy, t in zip(is_np_dtype, arrays_and_dtypes, strict=True) + try: + if xp is np or any( + isinstance(getattr(t, "dtype", t), np.dtype) for t in arrays_and_dtypes ): - return np.dtype("object") - return xp.result_type(*arrays_and_dtypes) - else: - return _future_array_api_result_type(*arrays_and_dtypes, xp=xp) + return xp.result_type(*arrays_and_dtypes) + else: + return _future_array_api_result_type(*arrays_and_dtypes, xp=xp) + except TypeError: + return np.dtype(object) def get_array_namespace(*values): From 602adc5dc66d1a82386c605db90c6647f9ddf8f5 Mon Sep 17 00:00:00 2001 From: Bryn Pickering <17178478+brynpickering@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:04:55 +0000 Subject: [PATCH 12/12] Update given TypeError fallback --- xarray/compat/array_api_compat.py | 14 -------------- xarray/core/dtypes.py | 17 ++++++----------- xarray/core/utils.py | 10 +++------- 3 files changed, 9 insertions(+), 32 deletions(-) diff --git a/xarray/compat/array_api_compat.py b/xarray/compat/array_api_compat.py index 43bfe3153d9..63a7cd8ac44 100644 --- a/xarray/compat/array_api_compat.py +++ b/xarray/compat/array_api_compat.py @@ -50,20 +50,6 @@ def _future_array_api_result_type(*arrays_and_dtypes, xp): return xp.result_type(dtype, *dtypes) -def is_builtin_type(value): - if isinstance(value, type): - return issubclass(value, builtin_types) - else: - return isinstance(value, builtin_types) - - -def is_dtype(value): - if isinstance(value, type): - return issubclass(value, np.generic) - else: - return isinstance(value, np.dtype) - - def result_type(*arrays_and_dtypes, xp) -> np.dtype: try: if xp is np or any( diff --git a/xarray/core/dtypes.py b/xarray/core/dtypes.py index 40334d58e68..4f29df2bb70 100644 --- a/xarray/core/dtypes.py +++ b/xarray/core/dtypes.py @@ -272,17 +272,11 @@ def should_promote_to_object( """ np_result_types = set() for arr_or_dtype in arrays_and_dtypes: - try: - result_type = array_api_compat.result_type( - maybe_promote_to_variable_width(arr_or_dtype), xp=xp - ) - if isinstance(result_type, np.dtype): - np_result_types.add(result_type) - except TypeError: - # passing individual objects to xp.result_type (i.e., what `array_api_compat.result_type` calls) means NEP-18 implementations won't have - # a chance to intercept special values (such as NA) that numpy core cannot handle. - # Thus they are considered as types that don't need promotion i.e., the `arr_or_dtype` that rose the `TypeError` will not contribute to `np_result_types`. - pass + result_type = array_api_compat.result_type( + maybe_promote_to_variable_width(arr_or_dtype), xp=xp + ) + if isinstance(result_type, np.dtype): + np_result_types.add(result_type) if np_result_types: for left, right in PROMOTE_TO_OBJECT: @@ -322,6 +316,7 @@ def result_type( if should_promote_to_object(arrays_and_dtypes, xp): return np.dtype(object) + maybe_promote = functools.partial( maybe_promote_to_variable_width, # let extension arrays handle their own str/bytes diff --git a/xarray/core/utils.py b/xarray/core/utils.py index c6828f0a363..dc49d41a21b 100644 --- a/xarray/core/utils.py +++ b/xarray/core/utils.py @@ -211,13 +211,9 @@ def maybe_coerce_to_str(index, original_coords): """ from xarray.core import dtypes - try: - result_type = dtypes.result_type(*original_coords) - except TypeError: - pass - else: - if result_type.kind in "SU": - index = np.asarray(index, dtype=result_type.type) + result_type = dtypes.result_type(*original_coords) + if result_type.kind in "SU": + index = np.asarray(index, dtype=result_type.type) return index