diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index b82c77fa9c..330c62aab1 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -1039,6 +1039,38 @@ def shape(self) -> tuple[int, ...]: """ return self.metadata.shape + def __len__(self) -> int: + """Return the length of the first dimension. + + Matches numpy behavior: returns shape[0] for dimensioned arrays, + raises TypeError for 0-dimensional arrays. + + Returns + ------- + int + The size of the first dimension. + + Raises + ------ + TypeError + If the array is 0-dimensional (empty shape). + + Examples + -------- + >>> import zarr + >>> a = zarr.zeros((5, 10)) + >>> len(a) + 5 + >>> b = zarr.zeros(()) + >>> len(b) # doctest: +SKIP + Traceback (most recent call last): + ... + TypeError: len() of unsized object + """ + if self.ndim == 0: + raise TypeError("len() of unsized object") + return self.shape[0] + @property def chunks(self) -> tuple[int, ...]: """Returns the chunk shape of the Array. @@ -2263,6 +2295,36 @@ def shape(self, value: tuple[int, ...]) -> None: """Sets the shape of the array by calling resize.""" self.resize(value) + def __len__(self) -> int: + """Return the length of the first dimension. + + Matches numpy behavior: returns shape[0] for dimensioned arrays, + raises TypeError for 0-dimensional arrays. + + Returns + ------- + int + The size of the first dimension. + + Raises + ------ + TypeError + If the array is 0-dimensional (empty shape). + + Examples + -------- + >>> import zarr + >>> a = zarr.zeros((5, 10)) + >>> len(a) + 5 + >>> b = zarr.zeros(()) + >>> len(b) # doctest: +SKIP + Traceback (most recent call last): + ... + TypeError: len() of unsized object + """ + return self.async_array.__len__() + @property def chunks(self) -> tuple[int, ...]: """Returns a tuple of integers describing the length of each dimension of a chunk of the array. diff --git a/tests/test_array.py b/tests/test_array.py index 5b85c6ba1d..298cba6107 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -2299,3 +2299,31 @@ def test_with_config_polymorphism() -> None: arr_source_config_dict = arr.with_config(source_config_dict) assert arr_source_config.config == arr_source_config_dict.config + + +@pytest.mark.parametrize("shape", [(10,), (5, 10), (3, 4, 5), (2, 3, 4, 5)]) +def test_array_len_dimensioned(shape: tuple[int, ...]) -> None: + """Test __len__ for dimensioned arrays returns shape[0].""" + arr = zarr.create_array({}, shape=shape, dtype="uint8") + assert len(arr) == shape[0] + + +@pytest.mark.parametrize("shape", [(10,), (5, 10), (3, 4, 5)]) +async def test_array_len_dimensioned_async(shape: tuple[int, ...]) -> None: + """Test __len__ for async dimensioned arrays returns shape[0].""" + arr = await zarr.api.asynchronous.create_array({}, shape=shape, dtype="uint8") + assert len(arr) == shape[0] + + +def test_array_len_0d_raises() -> None: + """Test __len__ raises TypeError for 0-dimensional arrays.""" + arr = zarr.create_array({}, shape=(), dtype="uint8") + with pytest.raises(TypeError, match="len\\(\\) of unsized object"): + len(arr) + + +async def test_array_len_0d_raises_async() -> None: + """Test __len__ raises TypeError for async 0-dimensional arrays.""" + arr = await zarr.api.asynchronous.create_array({}, shape=(), dtype="uint8") + with pytest.raises(TypeError, match="len\\(\\) of unsized object"): + len(arr)