Skip to content

pytest.approx with a list expected value reports overflowing differences for numpy integer arrays #15314

Open

Description

@ege-arhan

Comparing a NumPy integer array against pytest.approx(<python list>) uses 8-bit arithmetic in the failure report, so the reported differences are wrong and, with -Werror, the report degrades to representation of details failed. The approx(<numpy array>) path unboxes the NumPy scalars (ApproxNumpy._yield_comparisons / _repr_compare), the sequence path (ApproxSequenceLike) does not.

Reproducer (pytest 10.0.0.dev10+g0212785e3, numpy 2.3.3):

import numpy as np
import pytest


def test_approx_list_expected_numpy_actual():
    actual = np.array([255], dtype=np.uint8)
    assert actual == pytest.approx([0])

python -m pytest -Werror test_approx_list_expected_numpy_actual.py:

E   RuntimeWarning: overflow encountered in scalar subtract
    result: bool = abs(self.expected - actual) <= self.tolerance

Without -Werror the test fails, but the message reports the wrong numbers:

E       AssertionError:
E       assert array([255], dtype=uint8) == approx([0 ± 1.0e-12])
E         comparison failed. Mismatched elements: 1 / 1:
E         Max absolute difference: 1
E         Max relative difference: 0.00392156862745098

The differences come from ApproxSequenceLike._repr_compare subtracting a numpy.uint8 scalar from a Python float, which wraps around (255 - 0 → -1), so the absolute difference is 1 instead of 255. int8 is worse: pytest.approx([127]) against np.array([-128], dtype=np.int8) reports Max relative difference: -0.0078125.

pytest.approx(np.array([0], dtype=np.uint8)) against the same array reports 255 and 1.0 correctly, so the two approx flavours disagree about the same comparison.

Unboxing the value in the sequence loop as well fixes it:

        for i, (approx_value, other_value) in enumerate(
            zip(approx_side_as_map, other_side, strict=True)
        ):
            other_value = _unbox_numpy_scalar(other_value)
            if approx_value != other_value:

It is a no-op for plain Python values, since _unbox_numpy_scalar only calls .item() when the attribute exists. With that applied both reproducers report 255 / 1.0 and testing/python/approx.py still passes.

Related: #15312 fixes the array-expected path only.

Activity

  1. doubledare704 commented on Nov 1, 2026

    @doubledare704

    Thanks for the report. I reproduced this on main (numpy 2.5.4), and it is broader than the failure report. The equality check itself is wrong for numpy integer scalars against anything other than approx(<ndarray>):

    a = np.uint8(255)
    a == pytest.approx(254, abs=2)                       # False, should be True
    np.array([a]) == pytest.approx([254], abs=2)         # False, should be True
    {"a": a} == pytest.approx({"a": 254}, abs=2)         # False, should be True

    The cause is ApproxScalar.__eq__ evaluating self.expected - actual with a raw numpy.uint8, so 254 - uint8(255) wraps to 255. The approx(<ndarray>) path avoids this because _yield_comparisons already unboxes via _unbox_numpy_scalar.

    Unboxing only in ApproxSequenceLike._repr_compare fixes the report for lists but leaves equality wrong, and -Werror still fails at approx.py:602 before the report is built.

    I have a fix that unboxes actual once in ApproxScalar.__eq__ and also in the sequence and mapping _repr_compare, with tests for list, tuple, dict and scalar under np.errstate(all="raise"): https://github.com/doubledare704/pytest/tree/fix-approx-numpy-int-overflow

    I'll open a PR from it.

    Side note: the mapping report divides by the expected value for the relative difference while the sequence report divides by the actual value. That is pre-existing and I haven't changed it.

  2. rinopatrick commented on Nov 1, 2026

    @rinopatrick

    I'd like to work on this — the fix looks like unboxing numpy scalars on the actual side in ApproxSequenceLike (same as ApproxNumpy._yield_comparisons already does), for both the equality check and the failure report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions