Skip to content

Commit c6f250b

Browse files
author
Cipher
committed
feat: Implement Array.__len__ (fixes zarr-developers#3740)
Add __len__ method to both AsyncArray and Array classes to restore numpy compatibility. - AsyncArray.__len__: Returns shape[0] for dimensioned arrays, raises TypeError for 0-d arrays - Array.__len__: Delegates to async_array.__len__() - Matches numpy behavior exactly with error message 'len() of unsized object' - Added comprehensive tests covering: - 1-D, 2-D, 3-D, 4-D arrays returning shape[0] - 0-dimensional arrays raising TypeError - Both synchronous and asynchronous versions This restores the zarr v2 behavior that was removed in the v3 rewrite, essential for ecosystem compatibility with code that uses hasattr(obj, '__len__') to distinguish arrays from scalars.
1 parent 420f11c commit c6f250b

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

src/zarr/core/array.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,38 @@ def shape(self) -> tuple[int, ...]:
10391039
"""
10401040
return self.metadata.shape
10411041

1042+
def __len__(self) -> int:
1043+
"""Return the length of the first dimension.
1044+
1045+
Matches numpy behavior: returns shape[0] for dimensioned arrays,
1046+
raises TypeError for 0-dimensional arrays.
1047+
1048+
Returns
1049+
-------
1050+
int
1051+
The size of the first dimension.
1052+
1053+
Raises
1054+
------
1055+
TypeError
1056+
If the array is 0-dimensional (empty shape).
1057+
1058+
Examples
1059+
--------
1060+
>>> import zarr
1061+
>>> a = zarr.zeros((5, 10))
1062+
>>> len(a)
1063+
5
1064+
>>> b = zarr.zeros(())
1065+
>>> len(b) # doctest: +SKIP
1066+
Traceback (most recent call last):
1067+
...
1068+
TypeError: len() of unsized object
1069+
"""
1070+
if self.ndim == 0:
1071+
raise TypeError("len() of unsized object")
1072+
return self.shape[0]
1073+
10421074
@property
10431075
def chunks(self) -> tuple[int, ...]:
10441076
"""Returns the chunk shape of the Array.
@@ -2263,6 +2295,36 @@ def shape(self, value: tuple[int, ...]) -> None:
22632295
"""Sets the shape of the array by calling resize."""
22642296
self.resize(value)
22652297

2298+
def __len__(self) -> int:
2299+
"""Return the length of the first dimension.
2300+
2301+
Matches numpy behavior: returns shape[0] for dimensioned arrays,
2302+
raises TypeError for 0-dimensional arrays.
2303+
2304+
Returns
2305+
-------
2306+
int
2307+
The size of the first dimension.
2308+
2309+
Raises
2310+
------
2311+
TypeError
2312+
If the array is 0-dimensional (empty shape).
2313+
2314+
Examples
2315+
--------
2316+
>>> import zarr
2317+
>>> a = zarr.zeros((5, 10))
2318+
>>> len(a)
2319+
5
2320+
>>> b = zarr.zeros(())
2321+
>>> len(b) # doctest: +SKIP
2322+
Traceback (most recent call last):
2323+
...
2324+
TypeError: len() of unsized object
2325+
"""
2326+
return self.async_array.__len__()
2327+
22662328
@property
22672329
def chunks(self) -> tuple[int, ...]:
22682330
"""Returns a tuple of integers describing the length of each dimension of a chunk of the array.

tests/test_array.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2299,3 +2299,31 @@ def test_with_config_polymorphism() -> None:
22992299
arr_source_config_dict = arr.with_config(source_config_dict)
23002300

23012301
assert arr_source_config.config == arr_source_config_dict.config
2302+
2303+
2304+
@pytest.mark.parametrize("shape", [(10,), (5, 10), (3, 4, 5), (2, 3, 4, 5)])
2305+
def test_array_len_dimensioned(shape: tuple[int, ...]) -> None:
2306+
"""Test __len__ for dimensioned arrays returns shape[0]."""
2307+
arr = zarr.create_array({}, shape=shape, dtype="uint8")
2308+
assert len(arr) == shape[0]
2309+
2310+
2311+
@pytest.mark.parametrize("shape", [(10,), (5, 10), (3, 4, 5)])
2312+
async def test_array_len_dimensioned_async(shape: tuple[int, ...]) -> None:
2313+
"""Test __len__ for async dimensioned arrays returns shape[0]."""
2314+
arr = await AsyncArray.create({}, shape=shape, dtype="uint8")
2315+
assert len(arr) == shape[0]
2316+
2317+
2318+
def test_array_len_0d_raises() -> None:
2319+
"""Test __len__ raises TypeError for 0-dimensional arrays."""
2320+
arr = zarr.create_array({}, shape=(), dtype="uint8")
2321+
with pytest.raises(TypeError, match="len\\(\\) of unsized object"):
2322+
len(arr)
2323+
2324+
2325+
async def test_array_len_0d_raises_async() -> None:
2326+
"""Test __len__ raises TypeError for async 0-dimensional arrays."""
2327+
arr = await AsyncArray.create({}, shape=(), dtype="uint8")
2328+
with pytest.raises(TypeError, match="len\\(\\) of unsized object"):
2329+
len(arr)

0 commit comments

Comments
 (0)