From 3b3d2868556b9d0dc7aad09dfcced3b3fab4cd61 Mon Sep 17 00:00:00 2001 From: Muneeb Sheikh Date: Sun, 11 Oct 2026 10:30:58 -0400 Subject: [PATCH 1/2] Handle zero timedeltas in approx failure details A zero timedelta does not compare equal to numeric zero. Include it in the relative-difference guards so failing duration sequences keep their mismatch table and mappings report an infinite relative difference. --- AUTHORS | 1 + src/_pytest/approx.py | 7 +++++-- testing/python/approx.py | 41 +++++++++++++++++++++++++++------------- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/AUTHORS b/AUTHORS index bd1e27cb37d..fd1a205f194 100644 --- a/AUTHORS +++ b/AUTHORS @@ -352,6 +352,7 @@ minbang930 Minh Dau Miro HronĨok Mulat Mekonen +Muneeb Sheikh mrbean-bremen Nathan Goldbaum Nathan Rousseau diff --git a/src/_pytest/approx.py b/src/_pytest/approx.py index 70a881e3326..af24ff08d56 100644 --- a/src/_pytest/approx.py +++ b/src/_pytest/approx.py @@ -341,7 +341,10 @@ def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]: # TODO: The type error here seems correct. abs(approx_value.expected - other_value), # type: ignore[operator] ) - if approx_value.expected == 0: + if ( + approx_value.expected == 0 + or approx_value.expected == timedelta(0) + ): rel_diff_is_infinite = True else: max_rel_diff = _max_diff( @@ -443,7 +446,7 @@ def _repr_compare(self, other_side: Sequence[float]) -> list[str]: except TypeError: pass else: - if other_value == 0: + if other_value == 0 or other_value == timedelta(0): rel_diff_is_infinite = True else: max_rel_diff = _max_diff( diff --git a/testing/python/approx.py b/testing/python/approx.py index e0bfc37f85d..f8fc48d8ee4 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -1252,19 +1252,34 @@ def test_decimal_repr_compare_is_float_free( explanation = approx(expected)._repr_compare(other) assert explanation[1:3] == wanted - def test_mapping_relative_diff_undefined(self) -> None: - """A zero expected value that does not compare equal to 0. - - timedelta(0) != 0, so the divide-by-zero guard does not catch it and - the relative difference stays unknown. - """ - explanation = approx({"a": datetime.timedelta(0)}, rel=0.01)._repr_compare( - {"a": datetime.timedelta(seconds=1)} - ) - assert explanation[1:3] == [ - "Max absolute difference: 0:00:01", - "Max relative difference: -inf", - ] + @pytest.mark.parametrize( + ("actual", "expected"), + ( + pytest.param( + [datetime.timedelta(0), datetime.timedelta(seconds=5)], + [datetime.timedelta(seconds=10), datetime.timedelta(seconds=10)], + id="sequence", + ), + pytest.param( + { + "a": datetime.timedelta(seconds=10), + "b": datetime.timedelta(seconds=5), + }, + {"a": datetime.timedelta(0), "b": datetime.timedelta(seconds=10)}, + id="mapping", + ), + ), + ) + def test_zero_timedelta_relative_difference(self, actual, expected) -> None: + """Zero durations retain the mismatch table and an infinite relative diff.""" + with pytest.raises(AssertionError) as exc_info: + assert actual == approx(expected, rel=0.01) + + message = str(exc_info.value) + assert "Mismatched elements: 2 / 2" in message + assert "Max absolute difference: 0:00:10" in message + assert "Max relative difference: inf" in message + assert "Index" in message def test_mixed_decimal_and_float_sequence_does_not_hide_float_operation( self, monkeypatch From ff8acb19f760678c62ddfdad6a5d221b3f8dc736 Mon Sep 17 00:00:00 2001 From: Muneeb Sheikh Date: Sun, 11 Oct 2026 10:31:34 -0400 Subject: [PATCH 2/2] Add changelog for zero-duration approx diagnostics --- changelog/15165.bugfix.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/15165.bugfix.rst diff --git a/changelog/15165.bugfix.rst b/changelog/15165.bugfix.rst new file mode 100644 index 00000000000..b38af9692c3 --- /dev/null +++ b/changelog/15165.bugfix.rst @@ -0,0 +1,3 @@ +Fixed ``pytest.approx`` failure details for sequences and mappings with zero +``timedelta`` values. The mismatch table is preserved and a relative difference +with a zero duration denominator is reported as infinite.