From cbe4a107741c6cba1e0455201d4695ea879e99f9 Mon Sep 17 00:00:00 2001 From: Johnson K C Date: Sun, 26 Jul 2026 20:28:34 -0700 Subject: [PATCH 1/4] fix: allow `require_array` to accept a `ZDType` AsyncGroup.require_array normalised its dtype with np.dtype(), which cannot consume a ZDType, so requiring an existing array with one raised a TypeError. Every sibling creation method already accepts ZDTypeLike. Widen the annotation and normalise via parse_data_type().to_native_dtype(). parse_data_type(None) resolves to float64 just as np.dtype(None) did, so the default is unchanged. This leaves numpy.typing unused, so drop it. --- changes/4188.bugfix.md | 1 + src/zarr/core/group.py | 11 ++++++----- tests/test_group.py | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 changes/4188.bugfix.md diff --git a/changes/4188.bugfix.md b/changes/4188.bugfix.md new file mode 100644 index 0000000000..76ef7e7a5e --- /dev/null +++ b/changes/4188.bugfix.md @@ -0,0 +1 @@ +Allow `Group.require_array` to accept a `ZDType` for `dtype`, matching the other array creation methods. Previously an existing array could only be required with a string or NumPy dtype. diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index 0aaf89234e..76fcc704b0 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, Literal, assert_never, cast, overload import numpy as np -import numpy.typing as npt import zarr.api.asynchronous as async_api from zarr.abc.metadata import Metadata @@ -47,6 +46,7 @@ parse_shapelike, ) from zarr.core.config import config +from zarr.core.dtype import parse_data_type from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata from zarr.core.metadata.io import save_metadata from zarr.core.sync import SyncMixin, sync @@ -1159,7 +1159,7 @@ async def require_array( name: str, *, shape: ShapeLike, - dtype: npt.DTypeLike | None = None, + dtype: ZDTypeLike | None = None, exact: bool = False, **kwargs: Any, ) -> AnyAsyncArray: @@ -1173,8 +1173,9 @@ async def require_array( Array name. shape : int or tuple of ints Array shape. - dtype : str or dtype, optional - NumPy dtype. + dtype : ZDTypeLike, optional + The data type of the array, given as a string, a NumPy dtype, or a + Zarr data type. exact : bool, optional If True, require `dtype` to match exactly. If false, require `dtype` can be cast from array dtype. @@ -1192,7 +1193,7 @@ async def require_array( if shape != ds.shape: raise TypeError(f"Incompatible shape ({ds.shape} vs {shape})") - dtype = np.dtype(dtype) + dtype = parse_data_type(dtype, zarr_format=self.metadata.zarr_format).to_native_dtype() if exact: if ds.dtype != dtype: raise TypeError(f"Incompatible dtype ({ds.dtype} vs {dtype})") diff --git a/tests/test_group.py b/tests/test_group.py index 1acd5551ca..2c88a6524c 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -24,6 +24,7 @@ from zarr.core._info import GroupInfo from zarr.core.buffer import default_buffer_prototype from zarr.core.config import config as zarr_config +from zarr.core.dtype import parse_data_type from zarr.core.dtype.common import unpack_dtype_json from zarr.core.dtype.npy.int import UInt8 from zarr.core.group import ( @@ -1366,6 +1367,19 @@ async def test_require_array(store: Store, zarr_format: ZarrFormat) -> None: await root.require_array("bar", shape=(10,), dtype="int8") +async def test_require_array_zdtype(store: Store, zarr_format: ZarrFormat) -> None: + """An existing array can be required with a ZDType, as well as a string or + a NumPy dtype. See https://github.com/zarr-developers/zarr-python/issues/3377 + """ + root = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) + await root.require_array("foo", shape=(10,), dtype="int32") + + zdtype = parse_data_type("int32", zarr_format=zarr_format) + for dtype in (zdtype, np.dtype("int32"), "int32"): + foo = await root.require_array("foo", shape=(10,), dtype=dtype, exact=True) + assert foo.dtype == np.dtype("int32") + + @pytest.mark.parametrize("consolidate", [True, False]) async def test_members_name(store: Store, consolidate: bool, zarr_format: ZarrFormat): group = Group.from_store(store=store, zarr_format=zarr_format) From 6a6e8b1bbc6017622a34128fa1bb4d55e606288a Mon Sep 17 00:00:00 2001 From: Johnson K C Date: Sun, 26 Jul 2026 20:28:51 -0700 Subject: [PATCH 2/4] chore: rename changelog fragment to the PR number --- changes/{4188.bugfix.md => 4189.bugfix.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changes/{4188.bugfix.md => 4189.bugfix.md} (100%) diff --git a/changes/4188.bugfix.md b/changes/4189.bugfix.md similarity index 100% rename from changes/4188.bugfix.md rename to changes/4189.bugfix.md From 899201190e754f5ebd638533cbd80c281eb7dde2 Mon Sep 17 00:00:00 2001 From: Johnson K C Date: Sun, 26 Jul 2026 20:39:54 -0700 Subject: [PATCH 3/4] fix: keep the float64 default explicit for mypy parse_data_type does not accept None, so pass "float64" directly, which is what np.dtype(None) resolved to before. --- src/zarr/core/group.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index 76fcc704b0..63a0fbe812 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -1193,7 +1193,11 @@ async def require_array( if shape != ds.shape: raise TypeError(f"Incompatible shape ({ds.shape} vs {shape})") - dtype = parse_data_type(dtype, zarr_format=self.metadata.zarr_format).to_native_dtype() + # `np.dtype(None)` used to resolve to float64 here; keep that default. + dtype = parse_data_type( + "float64" if dtype is None else dtype, + zarr_format=self.metadata.zarr_format, + ).to_native_dtype() if exact: if ds.dtype != dtype: raise TypeError(f"Incompatible dtype ({ds.dtype} vs {dtype})") From 98376b1936d44c2c60e36021f81669f10ebccebf Mon Sep 17 00:00:00 2001 From: Johnson K C Date: Tue, 11 Aug 2026 09:41:50 -0700 Subject: [PATCH 4/4] test: parametrize require_array dtype cases over (input, expected) pairs Covers the `dtype=None` path, which resolves to float64 and was previously untested, and asserts on the resulting ZDType rather than the native dtype. --- tests/test_group.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/test_group.py b/tests/test_group.py index 0cb1245a34..f7f2333ef5 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -24,7 +24,7 @@ from zarr.core._info import GroupInfo from zarr.core.buffer import default_buffer_prototype from zarr.core.config import config as zarr_config -from zarr.core.dtype import parse_data_type +from zarr.core.dtype import Float64, Int32 from zarr.core.dtype.common import unpack_dtype_json from zarr.core.dtype.npy.int import UInt8 from zarr.core.group import ( @@ -62,6 +62,7 @@ from zarr.core.buffer.core import Buffer from zarr.core.common import JSON, ZarrFormat + from zarr.core.dtype import ZDType, ZDTypeLike @pytest.fixture(params=["local", "memory", "zip"]) @@ -1440,17 +1441,27 @@ async def test_require_array(store: Store, zarr_format: ZarrFormat) -> None: await root.require_array("bar", shape=(10,), dtype="int8") -async def test_require_array_zdtype(store: Store, zarr_format: ZarrFormat) -> None: - """An existing array can be required with a ZDType, as well as a string or - a NumPy dtype. See https://github.com/zarr-developers/zarr-python/issues/3377 +@pytest.mark.parametrize( + ("dtype", "expected"), + [ + (Int32(), Int32()), + (np.dtype("int32"), Int32()), + ("int32", Int32()), + (None, Float64()), + ], + ids=["zdtype", "numpy", "str", "none"], +) +async def test_require_array_zdtype( + store: Store, zarr_format: ZarrFormat, dtype: ZDTypeLike | None, expected: ZDType[Any, Any] +) -> None: + """An existing array can be required with a ZDType, as well as a string, a NumPy dtype, + or None. See https://github.com/zarr-developers/zarr-python/issues/3377 """ root = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) - await root.require_array("foo", shape=(10,), dtype="int32") + await root.create_array("foo", shape=(10,), dtype=expected) - zdtype = parse_data_type("int32", zarr_format=zarr_format) - for dtype in (zdtype, np.dtype("int32"), "int32"): - foo = await root.require_array("foo", shape=(10,), dtype=dtype, exact=True) - assert foo.dtype == np.dtype("int32") + foo = await root.require_array("foo", shape=(10,), dtype=dtype, exact=True) + assert foo._zdtype == expected @pytest.mark.parametrize("consolidate", [True, False])