Skip to content

Display output failure message in insertion order of dictionary #13587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions changelog/13503.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed the order of dictionary keys in assertion failure messages.
Previously, dictionary diffs were shown in alphabetical order, regardless of how the keys appeared in the original dicts.
Now, common keys are shown in the insertion order of the left-hand dictionary,
and differing keys are shown in the insertion order of their respective dictionaries
16 changes: 14 additions & 2 deletions src/_pytest/_io/pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def __init__(
indent: int = 4,
width: int = 80,
depth: int | None = None,
sort_dicts: bool = True,
) -> None:
"""Handle pretty printing operations onto a stream using a set of
configured parameters.
Expand All @@ -75,6 +76,9 @@ def __init__(
depth
The maximum depth to print out nested structures.

sort_dicts
If true, dict keys are sorted.

"""
if indent < 0:
raise ValueError("indent must be >= 0")
Expand All @@ -85,6 +89,7 @@ def __init__(
self._depth = depth
self._indent_per_level = indent
self._width = width
self._sort_dicts = sort_dicts

def pformat(self, object: Any) -> str:
sio = _StringIO()
Expand Down Expand Up @@ -162,7 +167,10 @@ def _pprint_dict(
) -> None:
write = stream.write
write("{")
items = sorted(object.items(), key=_safe_tuple)
if self._sort_dicts:
items = sorted(object.items(), key=_safe_tuple)
else:
items = object.items()
self._format_dict_items(items, stream, indent, allowance, context, level)
write("}")

Expand Down Expand Up @@ -608,7 +616,11 @@ def _safe_repr(
components: list[str] = []
append = components.append
level += 1
for k, v in sorted(object.items(), key=_safe_tuple):
if self._sort_dicts:
items = sorted(object.items(), key=_safe_tuple)
else:
items = object.items()
for k, v in items:
krepr = self._safe_repr(k, context, maxlevels, level)
vrepr = self._safe_repr(v, context, maxlevels, level)
append(f"{krepr}: {vrepr}")
Expand Down
35 changes: 23 additions & 12 deletions src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,8 @@ def _compare_eq_iterable(
# dynamic import to speedup pytest
import difflib

left_formatting = PrettyPrinter().pformat(left).splitlines()
right_formatting = PrettyPrinter().pformat(right).splitlines()
left_formatting = PrettyPrinter(sort_dicts=False).pformat(left).splitlines()
right_formatting = PrettyPrinter(sort_dicts=False).pformat(right).splitlines()

explanation = ["", "Full diff:"]
# "right" is the expected base against which we compare "left",
Expand Down Expand Up @@ -505,29 +505,36 @@ def _compare_eq_dict(
set_left = set(left)
set_right = set(right)
common = set_left.intersection(set_right)
same = {k: left[k] for k in common if left[k] == right[k]}
same = {k: left[k] for k in left if k in right and left[k] == right[k]}
if same and verbose < 2:
explanation += [f"Omitting {len(same)} identical items, use -vv to show"]
elif same:
explanation += ["Common items:"]
explanation += highlighter(pprint.pformat(same)).splitlines()
# Common items are displayed in the order of the left dict
explanation += highlighter(pprint.pformat(same, sort_dicts=False)).splitlines()
diff = {k for k in common if left[k] != right[k]}
if diff:
explanation += ["Differing items:"]
for k in diff:
explanation += [
highlighter(saferepr({k: left[k]}))
+ " != "
+ highlighter(saferepr({k: right[k]}))
]
# Differing items are displayed in the order of the left dict
for k in left:
if k in diff:
explanation += [
highlighter(saferepr({k: left[k]}))
+ " != "
+ highlighter(saferepr({k: right[k]}))
]
extra_left = set_left - set_right
len_extra_left = len(extra_left)
if len_extra_left:
explanation.append(
f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:"
)
explanation.extend(
highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines()
highlighter(
pprint.pformat(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the caveats of not working for nesting

Copy link
Author

@ritvi-alagusankar ritvi-alagusankar Jul 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a test case for nested dictionaries, and it appears to be working as expected. The extra_left set only includes keys from the top level of the dictionary, so nested dicts shouldn't pose an issue. Let me know if this is the concern you were referring to.

{k: left[k] for k in left if k in extra_left}, sort_dicts=False
)
).splitlines()
)
extra_right = set_right - set_left
len_extra_right = len(extra_right)
Expand All @@ -536,7 +543,11 @@ def _compare_eq_dict(
f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:"
)
explanation.extend(
highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines()
highlighter(
pprint.pformat(
{k: right[k] for k in right if k in extra_right}, sort_dicts=False
)
).splitlines()
)
return explanation

Expand Down
78 changes: 78 additions & 0 deletions testing/test_error_diffs.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,84 @@ def test_this():
""",
id="Compare dicts with differing items",
),
pytest.param(
"""
def test_this():
result = {'d': 4, 'c': 3, 'b': 2, 'a': 1}
expected = {'d': 4, 'c': 3, 'e': 5}
assert result == expected
""",
"""
> assert result == expected
E AssertionError: assert {'d': 4, 'c': 3, 'b': 2, 'a': 1} == {'d': 4, 'c': 3, 'e': 5}
E Common items:
E {'d': 4, 'c': 3}
E Left contains 2 more items:
E {'b': 2, 'a': 1}
E Right contains 1 more item:
E {'e': 5}
E Full diff:
E {
E 'd': 4,
E 'c': 3,
E - 'e': 5,
E ? ^ ^
E + 'b': 2,
E ? ^ ^
E + 'a': 1,
E }
""",
id="Compare dicts and check order of diff",
),
pytest.param(
"""
def test_this():
result = {
"b": {"m": 3, "n": 4},
"a": {"x": 1, "y": 2},
"c": {"k": 5, "l": 6}
}
expected = {
"c": {"l": 6, "k": 5},
"e": {"v": 8},
"d": {"u": 7}
}
assert result == expected
""",
"""
> assert result == expected
E AssertionError: assert {'b': {'m': 3, 'n': 4}, 'a': {'x': 1, 'y': 2}, 'c': {'k': 5, 'l': 6}} == {'c': {'l': 6, 'k': 5}, 'e': {'v': 8}, 'd': {'u': 7}}
E Common items:
E {'c': {'k': 5, 'l': 6}}
E Left contains 2 more items:
E {'b': {'m': 3, 'n': 4}, 'a': {'x': 1, 'y': 2}}
E Right contains 2 more items:
E {'e': {'v': 8}, 'd': {'u': 7}}
E Full diff:
E {
E + 'b': {
E + 'm': 3,
E + 'n': 4,
E + },
E + 'a': {
E + 'x': 1,
E + 'y': 2,
E + },
E 'c': {
E + 'k': 5,
E 'l': 6,
E - 'k': 5,
E - },
E - 'e': {
E - 'v': 8,
E - },
E - 'd': {
E - 'u': 7,
E },
E }
""",
id="Compare nested dicts and check order of diff",
),
pytest.param(
"""
def test_this():
Expand Down