From aa351ae1c5c512d16733b8ef1e703e17d407cd82 Mon Sep 17 00:00:00 2001 From: Fabian Isensee Date: Tue, 3 Nov 2026 13:49:19 +0100 Subject: [PATCH 1/5] Give lazy expressions NumPy's result dtype for division and float functions check_dtype() gave integer or bool result dtypes to hypot, copysign and nextafter of integer subexpressions, to true division involving bools ((a < b) / b, bool / bool) and to the bool //, % and ** that NumPy computes in int8, so their results were truncated on every evaluation path: hypot(a + 1, b) of int8 arrays gave [2 3 4 5] for NumPy's [2.83 3.61 4.47 5.39], (a < b) / b gave [0 0 0 0] for [0.5 0 0 0]. It now asks NumPy for these operators and functions: the operator or ufunc on empty operands, with a scalar next to an array kept as it is, so that it promotes as a Python scalar; int32 / int32 keeps its float32. Array operands' dtypes are read directly instead of through blosc2.result_type(), which more than pays for the probe: building (a + b) * 2 - f takes 62 instead of 70 us. The new test checks the dtype on every path and the values where the typing follows NumPy (compute() and partial slices). expr[:] and reductions of integer true division and two-argument float functions of two integer arrays still compute in integer arithmetic inside the bundled miniexpr, which is for miniexpr to fix. Taken from #744 (55660b4), without its miniexpr guard. Co-Authored-By: Claude Opus 5.5 --- src/blosc2/lazyexpr.py | 49 +++++++++++++++++++++++------ tests/ndarray/test_lazyexpr.py | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/src/blosc2/lazyexpr.py b/src/blosc2/lazyexpr.py index 2229cf7f2..9846148b8 100644 --- a/src/blosc2/lazyexpr.py +++ b/src/blosc2/lazyexpr.py @@ -358,6 +358,23 @@ def _find_constructor_call(expression: str, constructor: str) -> re.Match | None "//": operator.floordiv, "%": operator.mod, } +# NumPy operations of the numeric operators and functions whose result dtype is not just the +# promoted operand dtype: true division and the float functions turn integers (and bools) into +# floats, and //, %, ** and the shifts, which NumPy has no bool loops for, turn bools into int8 +_result_dtype_ufuncs = { + "/": operator.truediv, + "//": operator.floordiv, + "%": operator.mod, + "**": operator.pow, # not np.power: NumPy squares for x ** 2, which keeps bools int8 + "<<": operator.lshift, + ">>": operator.rshift, + "arctan2": np.arctan2, + "copysign": np.copysign, + "hypot": np.hypot, + "nextafter": np.nextafter, + "pow": np.power, + "power": np.power, +} logical_ops = ["&", "|", "^", "~"] not_complex_ops = ["maximum", "minimum", "<", "<=", ">", ">="] funcs_2args = ( @@ -3624,12 +3641,18 @@ def fuse_expressions(expr, new_base, dup_op): return new_expr +def _operand_dtype(value): + """blosc2.result_type(value), without building a NumPy array for an array operand.""" + dtype = None if np.isscalar(value) else getattr(value, "dtype", None) + return blosc2.result_type(value) if dtype is None else convert_dtype(dtype) + + def check_dtype(op, value1, value2): if op in ("contains", "startswith", "endswith"): return np.dtype(np.bool_) - v1_dtype = blosc2.result_type(value1) - v2_dtype = v1_dtype if value2 is None else blosc2.result_type(value2) + v1_dtype = _operand_dtype(value1) + v2_dtype = v1_dtype if value2 is None else _operand_dtype(value2) if op in not_complex_ops and (v1_dtype == np.complex128 or v2_dtype == np.complex128): # Ensure that throw exception for functions which don't support complex args raise ValueError(f"Invalid operand type for {op}: {v1_dtype, v2_dtype}") @@ -3642,14 +3665,20 @@ def check_dtype(op, value1, value2): if v2_dtype not in (np.bool_, np.int32, np.int64): raise ValueError(f"Invalid operand type for {op}: {v2_dtype}") - if op == "/": - if v1_dtype == np.int32 and v2_dtype == np.int32: - return blosc2.float32 - if np.issubdtype(v1_dtype, np.integer) and np.issubdtype(v2_dtype, np.integer): - return blosc2.float64 - - if op == "arctan2": - return np.arctan2(np.empty(0, v1_dtype), np.empty(0, v2_dtype)).dtype + if op == "/" and v1_dtype == np.int32 and v2_dtype == np.int32: + return blosc2.float32 + + ufunc = _result_dtype_ufuncs.get(op) + if ufunc is not None and value2 is not None and v1_dtype.kind in "biufc" and v2_dtype.kind in "biufc": + # Ask NumPy on empty operands, but with a scalar next to an array as it is, so that NumPy + # promotes it as usual; what it rejects is left to the evaluation + scalars = np.isscalar(value1) and np.isscalar(value2) + probes = [ + v if np.isscalar(v) and not scalars else np.empty(0, dt) + for v, dt in ((value1, v1_dtype), (value2, v2_dtype)) + ] + with contextlib.suppress(TypeError, ValueError, OverflowError): + return ufunc(*probes).dtype if (v1_dtype.kind in "Mm" or v2_dtype.kind in "Mm") and op in _datetime_binops: # result_type describes promotion, not operator semantics, and the two diff --git a/tests/ndarray/test_lazyexpr.py b/tests/ndarray/test_lazyexpr.py index 4be24af5d..0dc1facd6 100644 --- a/tests/ndarray/test_lazyexpr.py +++ b/tests/ndarray/test_lazyexpr.py @@ -45,6 +45,62 @@ def test_arctan2_preserves_fractional_results(dtype, fused): np.testing.assert_allclose(blosc2.lazyexpr("arctan2(a, b)")[:], expected, rtol=1e-6) +_RESULT_DTYPE_EXPRESSIONS = [ + "hypot({n}, b)", + "copysign({n}, b)", + "nextafter({n}, b)", + "arctan2({n}, b)", + "hypot(a, b * 1)", + "hypot({n}, 2)", + "arctan2({n}, 2.5)", + "(a < b) / b", + "a / 2", + "a / b", + "{n} / b", + "{n} // b", + "{n} % b", + "a ** b", + "{n} ** 2", + "sin({n})", + "sqrt(a)", +] + + +@pytest.mark.parametrize("dtype", [np.bool_, np.int8, np.uint16, np.int32, np.int64]) +@pytest.mark.parametrize("expression", _RESULT_DTYPE_EXPRESSIONS) +@pytest.mark.parametrize("build", ["functions", "string"]) +def test_lazyexpr_result_dtype_matches_numpy(dtype, expression, build): + # Float functions and true division of integer (or bool) subexpressions were given an integer + # (or bool) result dtype, and bool //, % and ** a bool one, so their results were truncated + if dtype is np.int32 and expression in ("a / b", "{n} / b"): + pytest.skip("int32 / int32 gives float32 on purpose") + if "nextafter" in expression and dtype not in (np.int32, np.int64): + pytest.skip("a float16 or float32 nextafter() is computed in float64 on every path") + # A subexpression of the operand dtype (bool + bool is an or in NumPy) + expression = expression.format(n="(a & b)" if dtype is np.bool_ else "(a + b)") + na = (np.arange(1200).reshape(30, 40) % 5).astype(dtype) + nb = (np.arange(1200).reshape(30, 40) % 3 + 1).astype(dtype) + a = blosc2.asarray(na, chunks=(10, 40), blocks=(5, 40)) + b = blosc2.asarray(nb, chunks=(10, 40), blocks=(5, 40)) + namespace = {"hypot": np.hypot, "copysign": np.copysign, "nextafter": np.nextafter} + namespace.update(arctan2=np.arctan2, sin=np.sin, sqrt=np.sqrt) + with np.errstate(all="ignore"): + expected = eval(expression, namespace, {"a": na, "b": nb}) + if build == "string": + expr = blosc2.lazyexpr(expression, {"a": a, "b": b}) + else: + expr = eval(expression, {name: getattr(blosc2, name) for name in namespace}, {"a": a, "b": b}) + assert expr.dtype == expected.dtype + for res in (expr[:], expr[...], expr.compute()[:], expr[:7]): + assert res.dtype == expected.dtype + # Values on the paths whose typing follows NumPy here; expr[:] and reductions of integer + # true division and two-argument float functions still compute in integer arithmetic in + # the bundled miniexpr + rtol = 1e-3 if expected.dtype == np.float16 else 1e-6 + np.testing.assert_allclose(expr.compute()[:], expected, rtol=rtol) + np.testing.assert_allclose(expr[:7], expected[:7], rtol=rtol) + + _UNARY_FUNCTIONS = [ "sin", "cos", From f1fa158c7b39affcf56ff6474df31b5ad86bc928 Mon Sep 17 00:00:00 2001 From: Fabian Isensee Date: Tue, 3 Nov 2026 14:37:04 +0100 Subject: [PATCH 2/5] Keep check_dtype()'s byte order and arctan2 behavior from before the probe _operand_dtype() returned an array operand's dtype as it is, but blosc2.result_type(), which it replaced, gave the native byte order, and check_dtype() compares against native dtypes. A big-endian operand therefore failed the bitwise operators (a & b with a >i4 NumPy array raised "Invalid operand type for &: >i4") and lost the float32 of int32 / int32 (a / a of a >i4 array gave float64). _operand_dtype() now returns np.result_type(dtype), which is what blosc2.result_type() computed, without building the array. Structured and void dtypes go through blosc2.result_type() as before, which rejects some of them. The probe also let arctan2 of complex, datetime or string operands build and fail only on evaluation; arctan2 raises NumPy's error when the expression is built again. Co-Authored-By: Claude Opus 5.5 --- src/blosc2/lazyexpr.py | 12 ++++++++++- tests/ndarray/test_lazyexpr.py | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/blosc2/lazyexpr.py b/src/blosc2/lazyexpr.py index 9846148b8..5e44ed362 100644 --- a/src/blosc2/lazyexpr.py +++ b/src/blosc2/lazyexpr.py @@ -3644,7 +3644,13 @@ def fuse_expressions(expr, new_base, dup_op): def _operand_dtype(value): """blosc2.result_type(value), without building a NumPy array for an array operand.""" dtype = None if np.isscalar(value) else getattr(value, "dtype", None) - return blosc2.result_type(value) if dtype is None else convert_dtype(dtype) + if dtype is not None: + dtype = convert_dtype(dtype) + if dtype.kind != "V": + # np.result_type() gives the native byte order, which the dtype checks below compare against + return np.result_type(dtype) + # Structured and void dtypes too: blosc2.result_type() rejects some of them + return blosc2.result_type(value) def check_dtype(op, value1, value2): @@ -3680,6 +3686,10 @@ def check_dtype(op, value1, value2): with contextlib.suppress(TypeError, ValueError, OverflowError): return ufunc(*probes).dtype + if op == "arctan2": + # Except for arctan2, which rejects what NumPy rejects (complex, datetimes, strings) here + return np.arctan2(np.empty(0, v1_dtype), np.empty(0, v2_dtype)).dtype + if (v1_dtype.kind in "Mm" or v2_dtype.kind in "Mm") and op in _datetime_binops: # result_type describes promotion, not operator semantics, and the two # part ways for datetimes: subtracting two datetime64s gives a diff --git a/tests/ndarray/test_lazyexpr.py b/tests/ndarray/test_lazyexpr.py index 0dc1facd6..f562b84c7 100644 --- a/tests/ndarray/test_lazyexpr.py +++ b/tests/ndarray/test_lazyexpr.py @@ -101,6 +101,43 @@ def test_lazyexpr_result_dtype_matches_numpy(dtype, expression, build): np.testing.assert_allclose(expr[:7], expected[:7], rtol=rtol) +@pytest.mark.parametrize("dtype", [np.int32, np.int64, np.float64]) +@pytest.mark.parametrize("op", ["&", "|", "^", "<<", ">>", "+", "*", "/", "//", "%", "**", "<", "hypot"]) +def test_lazyexpr_non_native_byte_order(dtype, op): + # The operand dtype checks compare against native dtypes: a non-native operand must give the + # same result dtype as a native one (no "Invalid operand type for &: >i4", and >i4 / >i4 + # keeps the float32 of int32 / int32) + if dtype is np.float64 and op in ("&", "|", "^", "<<", ">>"): + pytest.skip("no float bitwise operators") + native = np.dtype(dtype) + swapped = native.newbyteorder() + na = np.arange(1, 7, dtype=native) + nb = np.array([1, 2, 1, 3, 1, 2], dtype=native) + + def build(a, b): + return blosc2.hypot(a, b) if op == "hypot" else eval(f"a {op} b") + + reference = build(blosc2.asarray(na), nb) + if op == "/" and dtype is np.int32: + assert np.dtype(reference.dtype) == np.float32 + expr = build(blosc2.asarray(na), nb.astype(swapped)) + assert expr.dtype == reference.dtype + np.testing.assert_array_equal(expr[:], reference[:]) + np.testing.assert_array_equal(expr.compute()[:], reference.compute()[:]) + # Only the dtype for non-native NDArray operands: their values are a separate problem + sa, sb = na.astype(swapped), nb.astype(swapped) + for a, b in ((na, sb), (sa, nb), (sa, sb)): + assert build(blosc2.asarray(a), blosc2.asarray(b)).dtype == reference.dtype + + +@pytest.mark.parametrize("other", [np.ones(4, dtype=np.int32), 2, 2.5]) +def test_arctan2_rejects_complex_when_built(other): + c = blosc2.asarray(np.ones(4, dtype=np.complex128)) + for x in (c, c + c): + with pytest.raises(TypeError): + blosc2.arctan2(x, other) + + _UNARY_FUNCTIONS = [ "sin", "cos", From 014a7b1a5927b9ad303738ff2d4713ac1fa47349 Mon Sep 17 00:00:00 2001 From: Fabian Isensee Date: Tue, 3 Nov 2026 14:52:05 +0100 Subject: [PATCH 3/5] Check only the result dtypes on WebAssembly in the NumPy dtype test On WebAssembly, compute() and slices also run the bundled miniexpr, which computes integer true division and two-argument float functions of integer arrays in integer arithmetic (0.0 for NumPy's 0.5), so the test checks only the dtypes there. A Python int is int32 on wasm32, so int32 / 2 gives the intended float32 of int32 / int32 there; skip it. Co-Authored-By: Claude Opus 5.5 --- tests/ndarray/test_lazyexpr.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/ndarray/test_lazyexpr.py b/tests/ndarray/test_lazyexpr.py index f562b84c7..671605aaf 100644 --- a/tests/ndarray/test_lazyexpr.py +++ b/tests/ndarray/test_lazyexpr.py @@ -74,6 +74,8 @@ def test_lazyexpr_result_dtype_matches_numpy(dtype, expression, build): # (or bool) result dtype, and bool //, % and ** a bool one, so their results were truncated if dtype is np.int32 and expression in ("a / b", "{n} / b"): pytest.skip("int32 / int32 gives float32 on purpose") + if dtype is np.int32 and expression == "a / 2" and blosc2.result_type(2) == np.int32: + pytest.skip("a Python int is int32 here (wasm32), and int32 / int32 gives float32 on purpose") if "nextafter" in expression and dtype not in (np.int32, np.int64): pytest.skip("a float16 or float32 nextafter() is computed in float64 on every path") # A subexpression of the operand dtype (bool + bool is an or in NumPy) @@ -95,7 +97,9 @@ def test_lazyexpr_result_dtype_matches_numpy(dtype, expression, build): assert res.dtype == expected.dtype # Values on the paths whose typing follows NumPy here; expr[:] and reductions of integer # true division and two-argument float functions still compute in integer arithmetic in - # the bundled miniexpr + # the bundled miniexpr, and on WebAssembly compute() and slices do too + if blosc2.IS_WASM: + return rtol = 1e-3 if expected.dtype == np.float16 else 1e-6 np.testing.assert_allclose(expr.compute()[:], expected, rtol=rtol) np.testing.assert_allclose(expr[:7], expected[:7], rtol=rtol) From b62727a8df746c78309deedab1c263e0ef488123 Mon Sep 17 00:00:00 2001 From: Fabian Isensee Date: Tue, 3 Nov 2026 15:28:19 +0100 Subject: [PATCH 4/5] Mark the known WebAssembly failures of the dtype test as strict xfail The previous commit checked only the dtypes on WebAssembly and skipped int32 / 2 there, which also dropped the value checks that pass. Keep the test as it was and mark exactly the 32 cases that fail on WebAssembly as xfail(strict=True) instead: integer true division and hypot/nextafter of integer arrays, which the bundled miniexpr computes in integer arithmetic there on compute() and slices too, and int32 / 2, which gets the float32 of int32 / int32 because a Python int is int32 on wasm32. With strict, a fix shows up as XPASS. Co-Authored-By: Claude Opus 5.5 --- tests/ndarray/test_lazyexpr.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/ndarray/test_lazyexpr.py b/tests/ndarray/test_lazyexpr.py index 671605aaf..f78175c4f 100644 --- a/tests/ndarray/test_lazyexpr.py +++ b/tests/ndarray/test_lazyexpr.py @@ -66,18 +66,31 @@ def test_arctan2_preserves_fractional_results(dtype, fused): ] +# Known failures on WebAssembly: there compute() and slices also run the bundled miniexpr, which +# computes these in integer arithmetic (0.0 for NumPy's 0.5), and a Python int is int32 on wasm32, +# so int32 / 2 gets the float32 of int32 / int32. strict, so that a fix shows up as XPASS +_WASM_KNOWN_FAILURES = { + "hypot({n}, b)": (np.uint16, np.int32, np.int64), + "nextafter({n}, b)": (np.int32, np.int64), + "(a < b) / b": (np.int8, np.uint16, np.int32, np.int64), + "a / b": (np.int8, np.uint16, np.int64), + "{n} / b": (np.int8, np.uint16, np.int64), + "a / 2": (np.int32,), +} + + @pytest.mark.parametrize("dtype", [np.bool_, np.int8, np.uint16, np.int32, np.int64]) @pytest.mark.parametrize("expression", _RESULT_DTYPE_EXPRESSIONS) @pytest.mark.parametrize("build", ["functions", "string"]) -def test_lazyexpr_result_dtype_matches_numpy(dtype, expression, build): +def test_lazyexpr_result_dtype_matches_numpy(dtype, expression, build, request): # Float functions and true division of integer (or bool) subexpressions were given an integer # (or bool) result dtype, and bool //, % and ** a bool one, so their results were truncated if dtype is np.int32 and expression in ("a / b", "{n} / b"): pytest.skip("int32 / int32 gives float32 on purpose") - if dtype is np.int32 and expression == "a / 2" and blosc2.result_type(2) == np.int32: - pytest.skip("a Python int is int32 here (wasm32), and int32 / int32 gives float32 on purpose") if "nextafter" in expression and dtype not in (np.int32, np.int64): pytest.skip("a float16 or float32 nextafter() is computed in float64 on every path") + if blosc2.IS_WASM and dtype in _WASM_KNOWN_FAILURES.get(expression, ()): + request.applymarker(pytest.mark.xfail(reason="see _WASM_KNOWN_FAILURES", strict=True)) # A subexpression of the operand dtype (bool + bool is an or in NumPy) expression = expression.format(n="(a & b)" if dtype is np.bool_ else "(a + b)") na = (np.arange(1200).reshape(30, 40) % 5).astype(dtype) @@ -97,9 +110,7 @@ def test_lazyexpr_result_dtype_matches_numpy(dtype, expression, build): assert res.dtype == expected.dtype # Values on the paths whose typing follows NumPy here; expr[:] and reductions of integer # true division and two-argument float functions still compute in integer arithmetic in - # the bundled miniexpr, and on WebAssembly compute() and slices do too - if blosc2.IS_WASM: - return + # the bundled miniexpr rtol = 1e-3 if expected.dtype == np.float16 else 1e-6 np.testing.assert_allclose(expr.compute()[:], expected, rtol=rtol) np.testing.assert_allclose(expr[:7], expected[:7], rtol=rtol) From bf840e59eb8e8d6a45592d7236b60f3f34cae8ce Mon Sep 17 00:00:00 2001 From: Fabian Isensee Date: Tue, 3 Nov 2026 17:14:04 +0100 Subject: [PATCH 5/5] Expect float32 for int32 / 2 where a Python int is int32 in the dtype test Where a Python int is int32, as on wasm32, int32_array / 2 is int32 / int32, which gives float32 on purpose. The test now expects that, values included, instead of marking these two cases xfail. The other 30 WebAssembly xfails stay strict, each with its reason and miniexpr issue (Blosc/miniexpr#26, Blosc/miniexpr#27). Co-Authored-By: Claude Opus 5.5 --- tests/ndarray/test_lazyexpr.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/ndarray/test_lazyexpr.py b/tests/ndarray/test_lazyexpr.py index f78175c4f..324cd71b4 100644 --- a/tests/ndarray/test_lazyexpr.py +++ b/tests/ndarray/test_lazyexpr.py @@ -66,16 +66,22 @@ def test_arctan2_preserves_fractional_results(dtype, fused): ] -# Known failures on WebAssembly: there compute() and slices also run the bundled miniexpr, which -# computes these in integer arithmetic (0.0 for NumPy's 0.5), and a Python int is int32 on wasm32, -# so int32 / 2 gets the float32 of int32 / int32. strict, so that a fix shows up as XPASS +# Known wrong values on WebAssembly: there compute() and slices also run the bundled miniexpr, which +# computes these in integer arithmetic (0.0 for NumPy's 0.5). strict, so that a fix shows up as XPASS +_MINIEXPR_TRUE_DIVISION = ( + "miniexpr computes integer true division in integer arithmetic: " + "https://github.com/Blosc/miniexpr/issues/26" +) +_MINIEXPR_FLOAT_FUNCTIONS = ( + "miniexpr computes hypot() and nextafter() of integers in integer arithmetic: " + "https://github.com/Blosc/miniexpr/issues/27" +) _WASM_KNOWN_FAILURES = { - "hypot({n}, b)": (np.uint16, np.int32, np.int64), - "nextafter({n}, b)": (np.int32, np.int64), - "(a < b) / b": (np.int8, np.uint16, np.int32, np.int64), - "a / b": (np.int8, np.uint16, np.int64), - "{n} / b": (np.int8, np.uint16, np.int64), - "a / 2": (np.int32,), + "hypot({n}, b)": ((np.uint16, np.int32, np.int64), _MINIEXPR_FLOAT_FUNCTIONS), + "nextafter({n}, b)": ((np.int32, np.int64), _MINIEXPR_FLOAT_FUNCTIONS), + "(a < b) / b": ((np.int8, np.uint16, np.int32, np.int64), _MINIEXPR_TRUE_DIVISION), + "a / b": ((np.int8, np.uint16, np.int64), _MINIEXPR_TRUE_DIVISION), + "{n} / b": ((np.int8, np.uint16, np.int64), _MINIEXPR_TRUE_DIVISION), } @@ -89,8 +95,9 @@ def test_lazyexpr_result_dtype_matches_numpy(dtype, expression, build, request): pytest.skip("int32 / int32 gives float32 on purpose") if "nextafter" in expression and dtype not in (np.int32, np.int64): pytest.skip("a float16 or float32 nextafter() is computed in float64 on every path") - if blosc2.IS_WASM and dtype in _WASM_KNOWN_FAILURES.get(expression, ()): - request.applymarker(pytest.mark.xfail(reason="see _WASM_KNOWN_FAILURES", strict=True)) + known_dtypes, reason = _WASM_KNOWN_FAILURES.get(expression, ((), None)) + if blosc2.IS_WASM and dtype in known_dtypes: + request.applymarker(pytest.mark.xfail(reason=reason, strict=True)) # A subexpression of the operand dtype (bool + bool is an or in NumPy) expression = expression.format(n="(a & b)" if dtype is np.bool_ else "(a + b)") na = (np.arange(1200).reshape(30, 40) % 5).astype(dtype) @@ -101,6 +108,10 @@ def test_lazyexpr_result_dtype_matches_numpy(dtype, expression, build, request): namespace.update(arctan2=np.arctan2, sin=np.sin, sqrt=np.sqrt) with np.errstate(all="ignore"): expected = eval(expression, namespace, {"a": na, "b": nb}) + if dtype is np.int32 and expression == "a / 2" and blosc2.result_type(2) == np.int32: + # A Python int is int32 here (as on wasm32), so this is int32 / int32, which gives float32 + # on purpose; elsewhere it is int32 / int64, float64 as in NumPy + expected = expected.astype(np.float32) if build == "string": expr = blosc2.lazyexpr(expression, {"a": a, "b": b}) else: