Skip to content

Conversation

@RafiB982
Copy link

When dict assertions fail, pytest currently displays extra items using
unordered set iteration, which loses insertion order.

This change preserves insertion order for the
"Left contains N more items" and "Right contains N more items" sections
by iterating keys in mapping order instead of set order.

A focused regression test is included.

Closes #13503

@psf-chronographer psf-chronographer bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Dec 26, 2025
Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

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

Thanks @RafiB982, please take a look at my comments.

+ highlighter(saferepr({k: right[k]}))
]
extra_left = set_left - set_right
extra_left = [k for k in left if k not in right]
Copy link
Member

Choose a reason for hiding this comment

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

It is more efficient to reuse the set for the containment check:

Suggested change
extra_left = [k for k in left if k not in right]
extra_left = [k for k in left if k not in set_right]

highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines()
)
extra_right = set_right - set_left
extra_right = [k for k in right if k not in left]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
extra_right = [k for k in right if k not in left]
extra_right = [k for k in right if k not in set_left]

test_order="""
def test_order():
a = {
"first": 1,
Copy link
Member

Choose a reason for hiding this comment

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

Please let's use a dict with more keys for this test, only 2 seems few to catch any regressions. I suggest using simply "a", "b", etc, inserted in non-alphabetical order.

Comment on lines 2221 to 2235
stdout = re.sub(r"\x1b\[[0-9;]*m", "", stdout)

assert "Left contains 2 more items:" in stdout

dict_line = next(
line
for line in stdout.splitlines()
if "{" in line and "}" in line and "first" in line
)

first = dict_line.find("first")
second = dict_line.find("second")

assert first != -1 and second != -1
assert first < second
Copy link
Member

Choose a reason for hiding this comment

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

Could we use just result.stdout.fnmatch_lines instead to verify the output, instead of this custom parsing logic?

@RafiB982
Copy link
Author

Thanks for the review @nicoddemus

I’ve addressed the feedback by:

  1. reusing sets for containment checks in the implementation

  2. expanding the test to use more keys in non-alphabetical insertion order

  3. relying on -vv full assertion diffs and result.stdout.fnmatch_lines for verification

  4. The rebase conflict has been resolved and tox -e py310 passes successfully.

Please let me know if you’d like any further adjustments.

@RafiB982 RafiB982 requested a review from nicoddemus December 27, 2025 04:44
Comment on lines 2203 to 2229
def test_dict_extra_items_preserve_insertion_order(pytester):
pytester.makepyfile(
test_order="""
def test_order():
a = {
"b": 2,
"a": 1,
"d": 4,
"e": 5,
"c": 3,
}
assert a == {}
"""
)

result = pytester.runpytest("-vv")

result.stdout.fnmatch_lines(
[
"*Left contains 5 more items:*",
"*'b': 2*",
"*'a': 1*",
"*'d': 4*",
"*'e': 5*",
"*'c': 3*",
]
)
Copy link
Member

@nicoddemus nicoddemus Dec 27, 2025

Choose a reason for hiding this comment

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

Thanks, but I noticed the test as written still passes in main, because it ends up matching against several lines of the output by chance.

Here is the output of the internal test_order in your branch:

================================ FAILURES =================================
_______________________________ test_order ________________________________

    def test_order():
        a = {
            "b": 2,
            "a": 1,
            "d": 4,
            "e": 5,
            "c": 3,
        }
>       assert a == {}
E       AssertionError: assert {'b': 2, 'a': 1, 'd': 4, 'e': 5, 'c': 3} == {}
E
E         Left contains 5 more items:
E         {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
E
E         Full diff:
E         - {}
E         + {
E         +     'a': 1,
E         +     'b': 2,
E         +     'c': 3,
E         +     'd': 4,
E         +     'e': 5,
E         + }

.tmp\test_order.py:12: AssertionError
========================= short test summary info =========================
FAILED .tmp\test_order.py::test_order - AssertionError: assert {'b': 2, 'a': 1, 'd': 4, 'e': 5, 'c': 3} == {}

  Left contains 5 more items:
  {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

  Full diff:
  - {}
  + {
  +     'a': 1,
  +     'b': 2,
  +     'c': 3,
  +     'd': 4,
  +     'e': 5,
  + }
============================ 1 failed in 0.19s ============================

As we can see, the full diff still shows the keys in sorted order.

The problem is that the keys (for example 'a': 1) appear many times in the output and the test as written ends up matching that in different places, even though the "Full diff" output is still sorted.

Here is an improved version of the test which fails in main:

Suggested change
def test_dict_extra_items_preserve_insertion_order(pytester):
pytester.makepyfile(
test_order="""
def test_order():
a = {
"b": 2,
"a": 1,
"d": 4,
"e": 5,
"c": 3,
}
assert a == {}
"""
)
result = pytester.runpytest("-vv")
result.stdout.fnmatch_lines(
[
"*Left contains 5 more items:*",
"*'b': 2*",
"*'a': 1*",
"*'d': 4*",
"*'e': 5*",
"*'c': 3*",
]
)
def test_dict_extra_items_preserve_insertion_order(pytester: Pytester) -> None:
"""Assertion output of dict diff shows keys in insertion order (#13503)."""
pytester.makepyfile(
test_order="""
def test_order():
a = {
"b": 2,
"a": 1,
"d": 4,
"e": 5,
"c": 3,
}
assert a == {}
"""
)
result = pytester.runpytest("-vv")
result.stdout.fnmatch_lines(
[
"*Left contains 5 more items:*",
"*Full diff:",
"* + *'b': 2,",
"* + *'a': 1,",
"* + *'d': 4,",
"* + *'e': 5,",
"* + *'c': 3,",
"test_order.py:*: AssertionError",
]
)

The test above fails in main and also fails in this branch. It seems more work is needed to handle the Full diff output.

@@ -0,0 +1 @@
Preserved insertion order for extra dictionary items in assertion failure output.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Preserved insertion order for extra dictionary items in assertion failure output.
Display dictionary differences in assertion failures using the original key insertion order instead of sorted order.

@nicoddemus
Copy link
Member

The documentation failure is being addressed in #14067.

RafiB982 and others added 10 commits December 28, 2025 10:43
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](actions/cache@v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…pytest-dev#14059)

Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.9 to 8.0.0.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](peter-evans/create-pull-request@84ae59a...98357b1)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…4060)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](actions/download-artifact@v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…dev#14061)

Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.1 to 5.5.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](codecov/codecov-action@5a10915...671740a)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Pinning to `<9.0` to fix doc building.

Related to python-trio/sphinxcontrib-trio#399.
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.9 → v0.14.10](astral-sh/ruff-pre-commit@v0.14.9...v0.14.10)
- [github.com/woodruffw/zizmor-pre-commit: v1.18.0 → v1.19.0](zizmorcore/zizmor-pre-commit@v1.18.0...v1.19.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
@RafiB982
Copy link
Author

Hi @nicoddemus ,
I’ve addressed the latest feedback as follows:

  1. Updated test_dict_extra_items_preserve_insertion_order to assert against the Full diff section explicitly, ensuring the test fails on main and only passes when insertion order is preserved. This avoids false positives from matching keys appearing elsewhere in the output.

  2. Insertion order coverage: Expanded the test data to include multiple keys inserted in a non-alphabetical order (b, a, d, e, c) to better guard against regressions.

  3. Assertion output behavior: Updated the assertion diff logic to preserve dictionary insertion order when rendering extra items, including in the Full diff output.

  4. Reused precomputed key sets (set_left / set_right) for containment checks as suggested.

  5. Changelog
    Updated changelog/13503.bugfix.rst to reflect the corrected behavior.

I’ve rebased the branch and pushed the changes. Please let me know if anything else should be adjusted.

@RafiB982 RafiB982 requested a review from nicoddemus December 28, 2025 05:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failure output order of dictionary keys is alphabetical instead of insertion order

2 participants