Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2d4ed10
show correct extra item in "contains one more item" diffs
SemTiOne Aug 19, 2026
b998798
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Aug 22, 2026
30c57ff
Add parametrized case for Left-side fallback
SemTiOne Aug 22, 2026
af7fbb5
Initialize i to fix mypy unbound
SemTiOne Aug 22, 2026
223e8db
Update changelog
SemTiOne Aug 25, 2026
5ad60d5
Update comment in _compare_sequence.py
SemTiOne Aug 25, 2026
0784ec2
Add fallback test
SemTiOne Aug 25, 2026
9769228
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Aug 25, 2026
18ecead
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Aug 27, 2026
57ed551
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Sep 7, 2026
2a21e8a
Harden tail check against exotic __eq__
SemTiOne Sep 7, 2026
d20867d
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Sep 8, 2026
fa61412
Cover exotic-eq fallback branches
SemTiOne Sep 8, 2026
71596d2
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Sep 8, 2026
68158d8
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Sep 16, 2026
272a07e
Harden tail check against slice failures
SemTiOne Sep 16, 2026
ec3511f
Merge branch 'main' of https://github.com/SemTiOne/pytest into fix/13…
SemTiOne Oct 16, 2026
b220629
Simplify extra-item fix to first-differing-index rule
SemTiOne Oct 16, 2026
886f14b
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Oct 16, 2026
484121b
Verify suffix before reporting extra sequence item
SemTiOne Nov 2, 2026
854bfb0
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Nov 2, 2026
a024726
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Nov 5, 2026
5385e1a
Fix sequence suffix check for mixed container types
SemTiOne Nov 5, 2026
a87743a
Merge branch 'main' into fix/13652-misleading-extra-item-message
SemTiOne Nov 7, 2026
f3ff79a
Fix extra-item report for identical nan objects
SemTiOne Nov 7, 2026
9fad72d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 7, 2026
d8f9262
Change return type of __eq__ method to Any in AmbiguousEq class
SemTiOne Nov 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/13652.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest now reports the actual extra item in ``Left contains one more item`` and ``Right contains one more item`` assertion messages. This works for lists and tuples, including mixed ``list``/``tuple`` comparisons and when both sequences contain the same ``nan`` object. If pytest cannot determine the extra item, it keeps the previous message.
33 changes: 30 additions & 3 deletions src/_pytest/assertion/_compare_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ def _compare_eq_iterable(
yield highlighter(line.rstrip(), lexer="diff")


def _items_equal(a: object, b: object) -> bool:
if a is b:
return True
try:
return bool(a == b)
except Exception:
return False


def _suffix_matches(
longer: Sequence[object], shorter: Sequence[object], index: int
) -> bool:
return all(
_items_equal(longer[index + 1 + offset], shorter[index + offset])
for offset in range(len(shorter) - index)
)


def _compare_eq_sequence(
left: Sequence[object],
right: Sequence[object],
Expand All @@ -52,8 +70,10 @@ def _compare_eq_sequence(
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
len_left = len(left)
len_right = len(right)
found_diff = False
i = -1
for i in range(min(len_left, len_right)):
if left[i] != right[i]:
if not _items_equal(left[i], right[i]):
if comparing_bytes:
# when comparing bytes, we want to see their ascii representation
# instead of their numeric values (#5260)
Expand All @@ -73,6 +93,7 @@ def _compare_eq_sequence(
f"At index {i} diff:"
f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}"
)
found_diff = True
break

if comparing_bytes:
Expand All @@ -84,11 +105,17 @@ def _compare_eq_sequence(
if len_diff:
if len_diff > 0:
dir_with_more = "Left"
extra = saferepr(left[len_right])
if found_diff and len_diff == 1 and _suffix_matches(left, right, i):
extra = saferepr(left[i])
else:
extra = saferepr(left[len_right])
else:
len_diff = 0 - len_diff
dir_with_more = "Right"
extra = saferepr(right[len_left])
if found_diff and len_diff == 1 and _suffix_matches(right, left, i):
extra = saferepr(right[i])
else:
extra = saferepr(right[len_left])

if len_diff == 1:
yield f"{dir_with_more} contains one more item: {highlighter(extra)}"
Expand Down
79 changes: 79 additions & 0 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from collections.abc import Iterator
from collections.abc import Mapping
from collections.abc import MutableSequence
from collections.abc import Sequence
import dataclasses
import sys
import textwrap
Expand Down Expand Up @@ -1170,6 +1171,84 @@ def test_sequence_different_items(self) -> None:
" )",
]

@pytest.mark.parametrize(
("left", "right", "expected"),
[
([], [1], "Right contains one more item: 1"),
([1], [], "Left contains one more item: 1"),
([1, 2, 3], [1, 2, 3, 4], "Right contains one more item: 4"),
([1, 2, 3], [1, 2, 0, 3], "Right contains one more item: 0"),
([1, 2, 0, 3], [1, 2, 3], "Left contains one more item: 0"),
([1, 2, 3], (0, 1, 2, 3), "Right contains one more item: 0"),
((1, 2, 3), [0, 1, 2, 3], "Right contains one more item: 0"),
([1, 2, 3], (1, 2, 0, 3), "Right contains one more item: 0"),
([1, 2, 0, 3], (1, 2, 3), "Left contains one more item: 0"),
([1, 2, 9], [1, 3], "Left contains one more item: 9"),
([1, 3], [1, 2, 9], "Right contains one more item: 9"),
(
[1, 2, 3],
[0, 1, 2, 3, 4],
"Right contains 2 more items, first extra item: 3",
),
],
)
def test_sequence_extra_item_message(
self, left: Sequence[object], right: Sequence[object], expected: str
) -> None:
lines = callequal(left, right, verbose=1)
assert lines is not None
assert expected in lines

def test_sequence_extra_item_nan_identity(self) -> None:
nan = float("nan")

lines = callequal([1, nan], [0, 1, nan], verbose=1)
assert lines is not None
assert "Right contains one more item: 0" in lines

lines = callequal([0, 1, nan], [1, nan], verbose=1)
assert lines is not None
assert "Left contains one more item: 0" in lines

lines = callequal([nan, 1], [nan, 0, 1], verbose=1)
assert lines is not None
assert "Right contains one more item: 0" in lines
assert not any("nan != nan" in line for line in lines)

lines = callequal([1, nan], [1, nan, 0], verbose=1)
assert lines is not None
assert "Right contains one more item: 0" in lines
assert not any("nan != nan" in line for line in lines)

def test_sequence_extra_item_broken_eq_falls_back(self) -> None:
class BrokenEq:
def __eq__(self, other: object) -> bool:
raise RuntimeError("boom")

def __repr__(self) -> str:
return "BrokenEq()"

lines = callequal([1, BrokenEq()], [0, 1, BrokenEq()], verbose=1)
assert lines is not None
assert "Right contains one more item: BrokenEq()" in lines
assert not any("representation of details failed" in line for line in lines)

def test_sequence_extra_item_ambiguous_eq_falls_back(self) -> None:
class AmbiguousEq:
def __eq__(self, other: object) -> Any:
return self

def __bool__(self) -> bool:
raise ValueError("ambiguous")

def __repr__(self) -> str:
return "AmbiguousEq()"

lines = callequal([1, AmbiguousEq()], [0, 1, AmbiguousEq()], verbose=1)
assert lines is not None
assert "Right contains one more item: AmbiguousEq()" in lines
assert not any("representation of details failed" in line for line in lines)

def test_set(self) -> None:
expl = callequal({0, 1}, {0, 2})
assert expl is not None
Expand Down
24 changes: 23 additions & 1 deletion testing/test_error_diffs.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_this():
> assert result == expected
E assert [1, 3] == [1, 2, 3]
E At index 1 diff: 3 != 2
E Right contains one more item: 3
E Right contains one more item: 2
E Full diff: (-: missing in left side, +: extra in left side)
E [
E 1,
Expand All @@ -76,6 +76,28 @@ def test_this():
""",
id="Compare lists, one item missing",
),
pytest.param(
"""
def test_this():
result = [1, 2, 3]
expected = [1, 2, 0, 3]
assert result == expected
""",
"""
> assert result == expected
E assert [1, 2, 3] == [1, 2, 0, 3]
E At index 2 diff: 3 != 0
E Right contains one more item: 0
E Full diff: (-: missing in left side, +: extra in left side)
E [
E 1,
E 2,
E - 0,
E 3,
E ]
""",
id="Compare lists, one extra item inserted mid-list",
),
pytest.param(
"""
def test_this():
Expand Down
Loading