From c85adf39531bf340c5af43c8d6c32ede00019425 Mon Sep 17 00:00:00 2001 From: barlowa124 Date: Thu, 8 Oct 2026 15:30:41 -0400 Subject: [PATCH 1/7] fix: raise a validation error for out-of-bounds integer fill values from_json_scalar cast the parsed integer into the native dtype without a range check, so fill_value 300 for int8 surfaced a numpy OverflowError instead of a validation error. Bounds-check against np.iinfo before casting; int, intish float, and intish string forms are covered. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/zarr/core/dtype/npy/int.py | 24 +++++++++++++--------- tests/test_dtype/test_npy/test_int.py | 29 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/zarr/core/dtype/npy/int.py b/src/zarr/core/dtype/npy/int.py index b7198fcb0e..7b7763cbec 100644 --- a/src/zarr/core/dtype/npy/int.py +++ b/src/zarr/core/dtype/npy/int.py @@ -203,15 +203,21 @@ def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> Scalar: TypeError If the input is not a valid integer type. """ - if check_json_int(data): - return self._cast_scalar_unchecked(data) - if check_json_intish_float(data): - return self._cast_scalar_unchecked(int(data)) - - if check_json_intish_str(data): - return self._cast_scalar_unchecked(int(data)) - - raise TypeError(f"Invalid type: {data}. Expected an integer.") + if ( + check_json_int(data) + or check_json_intish_float(data) + or check_json_intish_str(data) + ): + value = int(data) + else: + raise TypeError(f"Invalid type: {data}. Expected an integer.") + + info = np.iinfo(self.to_native_dtype()) + if value < info.min or value > info.max: + raise TypeError( + f"Invalid type: {data}. Integer is out of bounds for {self.to_native_dtype()}." + ) + return self._cast_scalar_unchecked(value) def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> int: """ diff --git a/tests/test_dtype/test_npy/test_int.py b/tests/test_dtype/test_npy/test_int.py index f25fa1a564..169fd9c13c 100644 --- a/tests/test_dtype/test_npy/test_int.py +++ b/tests/test_dtype/test_npy/test_int.py @@ -1,6 +1,7 @@ from __future__ import annotations import numpy as np +import pytest from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype.npy.int import Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 @@ -337,3 +338,31 @@ def test_string_integer_from_json_scalar() -> None: # Test that it works for v2 format too result = dtype_instance.from_json_scalar("123", zarr_format=2) assert result == np.int32(123) + + +def test_out_of_bounds_integer_from_json_scalar() -> None: + """An integer outside the dtype's range raises a validation error, not + numpy's OverflowError. Regression test for + https://github.com/zarr-developers/zarr-python/issues/4453 item 4.""" + for dtype_instance, bad in ( + (Int8(), 300), + (Int8(), -129), + (Int8(), "300"), + (Int8(), 300.0), + (UInt8(), -1), + (UInt64(), 2**64), + (Int64(), -(2**63) - 1), + ): + with pytest.raises(TypeError, match="out of bounds"): + dtype_instance.from_json_scalar(bad, zarr_format=3) + with pytest.raises(TypeError, match="out of bounds"): + dtype_instance.from_json_scalar(bad, zarr_format=2) + + for dtype_instance, good in ( + (Int8(), 127), + (Int8(), -128), + (UInt8(), 255), + (UInt64(), 2**64 - 1), + (Int64(), -(2**63)), + ): + assert dtype_instance.from_json_scalar(good, zarr_format=3) == dtype_instance.to_native_dtype().type(good) From 8eff6623bd0c2d8287fa67c380d28e67b0a7ee4c Mon Sep 17 00:00:00 2001 From: barlowa124 Date: Thu, 8 Oct 2026 15:31:42 -0400 Subject: [PATCH 2/7] docs: changelog entry for #4492 --- changes/4492.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/4492.bugfix.md diff --git a/changes/4492.bugfix.md b/changes/4492.bugfix.md new file mode 100644 index 0000000000..222b277c8c --- /dev/null +++ b/changes/4492.bugfix.md @@ -0,0 +1 @@ +Raise a validation error for out-of-bounds integer fill values instead of leaking NumPy's ``OverflowError``. From 53e384f74d1090bb27c07f2305ac7b8b468e94df Mon Sep 17 00:00:00 2001 From: barlowa124 Date: Thu, 8 Oct 2026 16:18:37 -0400 Subject: [PATCH 3/7] style: collapse from_json_scalar condition per ruff format --- src/zarr/core/dtype/npy/int.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/zarr/core/dtype/npy/int.py b/src/zarr/core/dtype/npy/int.py index 7b7763cbec..2048a2c4b2 100644 --- a/src/zarr/core/dtype/npy/int.py +++ b/src/zarr/core/dtype/npy/int.py @@ -203,11 +203,7 @@ def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> Scalar: TypeError If the input is not a valid integer type. """ - if ( - check_json_int(data) - or check_json_intish_float(data) - or check_json_intish_str(data) - ): + if check_json_int(data) or check_json_intish_float(data) or check_json_intish_str(data): value = int(data) else: raise TypeError(f"Invalid type: {data}. Expected an integer.") From 5382837033709c0249988d1b5de2f379f4d69a88 Mon Sep 17 00:00:00 2001 From: barlowa124 Date: Thu, 8 Oct 2026 16:33:16 -0400 Subject: [PATCH 4/7] style: ruff format the new test --- tests/test_dtype/test_npy/test_int.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_dtype/test_npy/test_int.py b/tests/test_dtype/test_npy/test_int.py index 169fd9c13c..fb8b9902b4 100644 --- a/tests/test_dtype/test_npy/test_int.py +++ b/tests/test_dtype/test_npy/test_int.py @@ -365,4 +365,6 @@ def test_out_of_bounds_integer_from_json_scalar() -> None: (UInt64(), 2**64 - 1), (Int64(), -(2**63)), ): - assert dtype_instance.from_json_scalar(good, zarr_format=3) == dtype_instance.to_native_dtype().type(good) + assert dtype_instance.from_json_scalar( + good, zarr_format=3 + ) == dtype_instance.to_native_dtype().type(good) From e360200c21c8ad60f3aeccbebc917f0a9731c80f Mon Sep 17 00:00:00 2001 From: barlowa124 Date: Fri, 9 Oct 2026 15:07:03 -0400 Subject: [PATCH 5/7] Address review on #4492: ValueError for out-of-range, shared bounds check - Raise ValueError (not TypeError) for out-of-range values and drop "Invalid type" from the message; format input with {data!r} - Document the ValueError in the Raises sections of from_json_scalar and cast_scalar - Split the regression test into parametrized in-bounds and out-of-bounds tests - Share the bounds check with cast_scalar so create_array with an out-of-range fill_value raises the same error (optional item) - Changelog: single backticks, cover both read and write paths --- changes/4492.bugfix.md | 2 +- src/zarr/core/dtype/npy/int.py | 31 +++++++++++++--- tests/test_dtype/test_npy/test_int.py | 51 +++++++++++++++++++-------- 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/changes/4492.bugfix.md b/changes/4492.bugfix.md index 222b277c8c..83500c2deb 100644 --- a/changes/4492.bugfix.md +++ b/changes/4492.bugfix.md @@ -1 +1 @@ -Raise a validation error for out-of-bounds integer fill values instead of leaking NumPy's ``OverflowError``. +Raise a `ValueError` for out-of-bounds integer fill values instead of leaking NumPy's `OverflowError`, both when reading stored metadata and when casting values at array creation. diff --git a/src/zarr/core/dtype/npy/int.py b/src/zarr/core/dtype/npy/int.py index 2048a2c4b2..ed79815a5a 100644 --- a/src/zarr/core/dtype/npy/int.py +++ b/src/zarr/core/dtype/npy/int.py @@ -143,6 +143,26 @@ def _cast_scalar_unchecked(self, data: IntLike) -> Scalar: return self.to_native_dtype().type(data) # type: ignore[return-value] + def _check_int_bounds(self, data: object, value: int) -> None: + """ + Check that an integer value fits in this data type's range. + + Parameters + ---------- + data : object + The original input, used in the error message. + value : int + The integer value to bounds-check. + + Raises + ------ + ValueError + If ``value`` is outside the range of the native dtype. + """ + info = np.iinfo(self.to_native_dtype()) + if value < info.min or value > info.max: + raise ValueError(f"{data!r} is out of bounds for {self.to_native_dtype()}.") + def cast_scalar(self, data: object) -> Scalar: """ Attempt to cast a given object to a NumPy integer scalar. @@ -161,9 +181,12 @@ def cast_scalar(self, data: object) -> Scalar: ------ TypeError If the data cannot be converted to a NumPy integer scalar. + ValueError + If the data is an integer outside the range of the dtype. """ if self._check_scalar(data): + self._check_int_bounds(data, int(data)) return self._cast_scalar_unchecked(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " @@ -202,17 +225,15 @@ def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> Scalar: ------ TypeError If the input is not a valid integer type. + ValueError + If the input is an integer outside the range of the dtype. """ if check_json_int(data) or check_json_intish_float(data) or check_json_intish_str(data): value = int(data) else: raise TypeError(f"Invalid type: {data}. Expected an integer.") - info = np.iinfo(self.to_native_dtype()) - if value < info.min or value > info.max: - raise TypeError( - f"Invalid type: {data}. Integer is out of bounds for {self.to_native_dtype()}." - ) + self._check_int_bounds(data, value) return self._cast_scalar_unchecked(value) def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> int: diff --git a/tests/test_dtype/test_npy/test_int.py b/tests/test_dtype/test_npy/test_int.py index fb8b9902b4..d92e6b3ce4 100644 --- a/tests/test_dtype/test_npy/test_int.py +++ b/tests/test_dtype/test_npy/test_int.py @@ -340,11 +340,9 @@ def test_string_integer_from_json_scalar() -> None: assert result == np.int32(123) -def test_out_of_bounds_integer_from_json_scalar() -> None: - """An integer outside the dtype's range raises a validation error, not - numpy's OverflowError. Regression test for - https://github.com/zarr-developers/zarr-python/issues/4453 item 4.""" - for dtype_instance, bad in ( +@pytest.mark.parametrize( + ("dtype_instance", "value"), + [ (Int8(), 300), (Int8(), -129), (Int8(), "300"), @@ -352,19 +350,44 @@ def test_out_of_bounds_integer_from_json_scalar() -> None: (UInt8(), -1), (UInt64(), 2**64), (Int64(), -(2**63) - 1), - ): - with pytest.raises(TypeError, match="out of bounds"): - dtype_instance.from_json_scalar(bad, zarr_format=3) - with pytest.raises(TypeError, match="out of bounds"): - dtype_instance.from_json_scalar(bad, zarr_format=2) + ], +) +def test_out_of_bounds_integer_from_json_scalar( + dtype_instance: Int8 | UInt8 | UInt64 | Int64, value: object +) -> None: + """An integer outside the dtype's range raises a validation error, not + numpy's OverflowError. Regression test for + https://github.com/zarr-developers/zarr-python/issues/4453 item 4.""" + for zarr_format in (2, 3): + with pytest.raises(ValueError, match="out of bounds"): + dtype_instance.from_json_scalar(value, zarr_format=zarr_format) + - for dtype_instance, good in ( +@pytest.mark.parametrize( + ("dtype_instance", "value"), + [ (Int8(), 127), (Int8(), -128), (UInt8(), 255), (UInt64(), 2**64 - 1), (Int64(), -(2**63)), - ): + ], +) +def test_in_bounds_integer_from_json_scalar( + dtype_instance: Int8 | UInt8 | UInt64 | Int64, value: int +) -> None: + """In-bounds integers round-trip for both zarr formats.""" + for zarr_format in (2, 3): assert dtype_instance.from_json_scalar( - good, zarr_format=3 - ) == dtype_instance.to_native_dtype().type(good) + value, zarr_format=zarr_format + ) == dtype_instance.to_native_dtype().type(value) + + +def test_out_of_bounds_integer_cast_scalar() -> None: + """cast_scalar raises ValueError for out-of-bounds integers, so + create_array(dtype="i1", fill_value=300) fails the same way reading + stored metadata does.""" + with pytest.raises(ValueError, match="out of bounds"): + Int8().cast_scalar(300) + with pytest.raises(ValueError, match="out of bounds"): + UInt64().cast_scalar(2**64) From 9ea1426c1946728cc1ad9915d22021ab1b802f42 Mon Sep 17 00:00:00 2001 From: barlowa124 Date: Fri, 9 Oct 2026 16:43:59 -0400 Subject: [PATCH 6/7] test: type out-of-bounds param as JSON for mypy --- tests/test_dtype/test_npy/test_int.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_dtype/test_npy/test_int.py b/tests/test_dtype/test_npy/test_int.py index d92e6b3ce4..8653305ed3 100644 --- a/tests/test_dtype/test_npy/test_int.py +++ b/tests/test_dtype/test_npy/test_int.py @@ -4,6 +4,7 @@ import pytest from tests.test_dtype.test_wrapper import BaseTestZDType +from zarr.core.common import JSON from zarr.core.dtype.npy.int import Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 @@ -353,7 +354,7 @@ def test_string_integer_from_json_scalar() -> None: ], ) def test_out_of_bounds_integer_from_json_scalar( - dtype_instance: Int8 | UInt8 | UInt64 | Int64, value: object + dtype_instance: Int8 | UInt8 | UInt64 | Int64, value: JSON ) -> None: """An integer outside the dtype's range raises a validation error, not numpy's OverflowError. Regression test for From 1c3bb48f186d8bd73069dc0154248972893c2a31 Mon Sep 17 00:00:00 2001 From: barlowa124 Date: Fri, 9 Oct 2026 17:45:10 -0400 Subject: [PATCH 7/7] Move JSON import into TYPE_CHECKING block --- tests/test_dtype/test_npy/test_int.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_dtype/test_npy/test_int.py b/tests/test_dtype/test_npy/test_int.py index 8653305ed3..2b498223ae 100644 --- a/tests/test_dtype/test_npy/test_int.py +++ b/tests/test_dtype/test_npy/test_int.py @@ -1,12 +1,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np import pytest from tests.test_dtype.test_wrapper import BaseTestZDType -from zarr.core.common import JSON from zarr.core.dtype.npy.int import Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 +if TYPE_CHECKING: + from zarr.core.common import JSON + class TestInt8(BaseTestZDType): test_cls = Int8