Repository: zarr-developers/zarr-python

Issue:
Array.nbytes logic is wrong
### Zarr version

3.0.0.b2

### Numcodecs version

!

### Python Version

!

### Operating System

!

### Installation

!

### Description

This logic is obviously and embarrassingly wrong:

https://github.com/zarr-developers/zarr-python/blob/f035d453894ec83234f69f7627b540390ee2b6eb/src/zarr/core/array.py#L977-L982

### Steps to reproduce

Read the code

### Additional output

_No response_

Discussion:

Patch:
diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py
index e5fc707f..2849907f 100644
--- a/src/zarr/core/array.py
+++ b/src/zarr/core/array.py
@@ -977,9 +977,17 @@ class AsyncArray(Generic[T_ArrayMetadata]):
     @property
     def nbytes(self) -> int:
         """
-        The number of bytes that can be stored in this array.
+        The total number of bytes that can be stored in the chunks of this array.
+
+        Notes
+        -----
+        This value is calculated by multiplying the number of elements in the array and the size
+        of each element, the latter of which is determined by the dtype of the array.
+        For this reason, ``nbytes`` will likely be inaccurate for arrays with variable-length
+        dtypes. It is not possible to determine the size of an array with variable-length elements
+        from the shape and dtype alone.
         """
-        return self.nchunks * self.dtype.itemsize
+        return self.size * self.dtype.itemsize
 
     async def _get_selection(
         self,
@@ -1429,7 +1437,7 @@ class AsyncArray(Generic[T_ArrayMetadata]):
             _order=self.order,
             _read_only=self.read_only,
             _store_type=type(self.store_path.store).__name__,
-            _count_bytes=self.dtype.itemsize * self.size,
+            _count_bytes=self.nbytes,
             _count_bytes_stored=count_bytes_stored,
             _count_chunks_initialized=count_chunks_initialized,
             **kwargs,
@@ -1740,7 +1748,15 @@ class Array:
     @property
     def nbytes(self) -> int:
         """
-        The number of bytes that can be stored in this array.
+        The total number of bytes that can be stored in the chunks of this array.
+
+        Notes
+        -----
+        This value is calculated by multiplying the number of elements in the array and the size
+        of each element, the latter of which is determined by the dtype of the array.
+        For this reason, ``nbytes`` will likely be inaccurate for arrays with variable-length
+        dtypes. It is not possible to determine the size of an array with variable-length elements
+        from the shape and dtype alone.
         """
         return self._async_array.nbytes
 


Test patch:
diff --git a/tests/test_array.py b/tests/test_array.py
index 3eb317e5..cf722c73 100644
--- a/tests/test_array.py
+++ b/tests/test_array.py
@@ -776,3 +776,21 @@ async def test_special_complex_fill_values_roundtrip(fill_value: Any, expected:
     assert content is not None
     actual = json.loads(content.to_bytes())
     assert actual["fill_value"] == expected
+
+
+@pytest.mark.parametrize("shape", [(1,), (2, 3), (4, 5, 6)])
+@pytest.mark.parametrize("dtype", ["uint8", "float32"])
+@pytest.mark.parametrize("array_type", ["async", "sync"])
+async def test_nbytes(
+    shape: tuple[int, ...], dtype: str, array_type: Literal["async", "sync"]
+) -> None:
+    """
+    Test that the ``nbytes`` attribute of an Array or AsyncArray correctly reports the capacity of
+    the chunks of that array.
+    """
+    store = MemoryStore()
+    arr = Array.create(store=store, shape=shape, dtype=dtype, fill_value=0)
+    if array_type == "async":
+        assert arr._async_array.nbytes == np.prod(arr.shape) * arr.dtype.itemsize
+    else:
+        assert arr.nbytes == np.prod(arr.shape) * arr.dtype.itemsize