From 919b41af427855f911d63d39b9975d61e13f256a Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 26 Feb 2026 15:03:37 +0000 Subject: [PATCH 1/6] fix(unittest.util): Deduplicate tail elements in sorted_list_difference sorted_list_difference failed to deduplicate remaining elements when one list was exhausted, causing duplicate values in the result. Fix uses dict.fromkeys() to deduplicate before extending. sim: https://taskei.amazon.dev/tasks/TODO --- Lib/unittest/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/unittest/util.py b/Lib/unittest/util.py index c7e6b941978cd5..127cacfc2146c4 100644 --- a/Lib/unittest/util.py +++ b/Lib/unittest/util.py @@ -98,8 +98,8 @@ def sorted_list_difference(expected, actual): while actual[j] == a: j += 1 except IndexError: - missing.extend(expected[i:]) - unexpected.extend(actual[j:]) + missing.extend(dict.fromkeys(expected[i:])) + unexpected.extend(dict.fromkeys(actual[j:])) break return missing, unexpected From 1a351e703df6b7b3ac26f698067c4b68e36b67b0 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 5 Mar 2026 14:02:49 +0000 Subject: [PATCH 2/6] test(unittest.util): Add tests for sorted_list_difference tail deduplication Exercise the except-IndexError path where one list is exhausted and the remaining tail of the other list contains duplicates. These cases were previously untested and would have passed even without the fix in the preceding commit. --- Lib/test/test_unittest/test_util.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_unittest/test_util.py b/Lib/test/test_unittest/test_util.py index d590a333930278..130583c64bee76 100644 --- a/Lib/test/test_unittest/test_util.py +++ b/Lib/test/test_unittest/test_util.py @@ -26,6 +26,17 @@ def test_sorted_list_difference(self): self.assertEqual(sorted_list_difference([2], [1, 1]), ([2], [1])) self.assertEqual(sorted_list_difference([1, 2], [1, 1]), ([2], [])) + def test_sorted_list_difference_tail_deduplication(self): + # Tail deduplication when one list is exhausted before the other. + # These exercise the except-IndexError path in sorted_list_difference. + self.assertEqual(sorted_list_difference([], [0, 0]), ([], [0])) + self.assertEqual(sorted_list_difference([0, 0], []), ([0], [])) + self.assertEqual(sorted_list_difference([], [1, 1, 2, 2]), ([], [1, 2])) + self.assertEqual(sorted_list_difference([1, 1, 2, 2], []), ([1, 2], [])) + # One list exhausts mid-way, leaving duplicated tail in the other. + self.assertEqual(sorted_list_difference([1], [1, 2, 2, 3, 3]), ([], [2, 3])) + self.assertEqual(sorted_list_difference([1, 2, 2, 3, 3], [1]), ([2, 3], [])) + def test_unorderable_list_difference(self): self.assertEqual(unorderable_list_difference([], []), ([], [])) self.assertEqual(unorderable_list_difference([1, 2], []), ([2, 1], [])) From 1db31220effc79181406ee9e1778ed8855d619e0 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:13:11 +0000 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst diff --git a/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst b/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst new file mode 100644 index 00000000000000..3583f63975a015 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst @@ -0,0 +1,4 @@ +Fix :func:`unittest.util.sorted_list_difference` to deduplicate remaining +elements when one input list is exhausted before the other. Previously, +duplicates in the tail were included in the output despite the documented +guarantee that "Duplicate elements in either input list are ignored." From 832952ea01321950a88c6b82ef824bd5bbf7678f Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 5 Mar 2026 14:22:39 +0000 Subject: [PATCH 4/6] news: use literal markup for undocumented unittest.util function Sphinx cannot resolve :func: references to unittest.util since it is not part of the public documented API. Use double backticks instead. --- .../next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst b/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst index 3583f63975a015..8fdb79506a7a97 100644 --- a/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst +++ b/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst @@ -1,4 +1,4 @@ -Fix :func:`unittest.util.sorted_list_difference` to deduplicate remaining +Fix ``unittest.util.sorted_list_difference()`` to deduplicate remaining elements when one input list is exhausted before the other. Previously, duplicates in the tail were included in the output despite the documented guarantee that "Duplicate elements in either input list are ignored." From 0d475d2891ebe12c29346dba5b3273e829b97c41 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 5 Mar 2026 15:29:55 +0000 Subject: [PATCH 5/6] news: shorten NEWS entry --- .../Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst b/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst index 8fdb79506a7a97..e9401bb08c6774 100644 --- a/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst +++ b/Misc/NEWS.d/next/Library/2026-03-05-14-13-10.gh-issue-145546.3tnlxx.rst @@ -1,4 +1,2 @@ -Fix ``unittest.util.sorted_list_difference()`` to deduplicate remaining -elements when one input list is exhausted before the other. Previously, -duplicates in the tail were included in the output despite the documented -guarantee that "Duplicate elements in either input list are ignored." +Fix ``unittest.util.sorted_list_difference()`` to deduplicate remaining +elements when one input list is exhausted before the other. From 16219a5751b457d507045283f08cff0dae4e8eb4 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 5 Mar 2026 15:43:16 +0000 Subject: [PATCH 6/6] Replace dict.fromkeys with equality-based dedup for unhashable types dict.fromkeys requires hashable elements, which narrows the input contract. Replace with a simple consecutive-duplicate removal that only uses equality comparison, matching the main loop's approach. Add test cases for strings and unhashable types (lists). --- Lib/test/test_unittest/test_util.py | 22 ++++++++++++++++++++++ Lib/unittest/util.py | 15 +++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_unittest/test_util.py b/Lib/test/test_unittest/test_util.py index 130583c64bee76..09ce09b91b7ac2 100644 --- a/Lib/test/test_unittest/test_util.py +++ b/Lib/test/test_unittest/test_util.py @@ -37,6 +37,28 @@ def test_sorted_list_difference_tail_deduplication(self): self.assertEqual(sorted_list_difference([1], [1, 2, 2, 3, 3]), ([], [2, 3])) self.assertEqual(sorted_list_difference([1, 2, 2, 3, 3], [1]), ([2, 3], [])) + def test_sorted_list_difference_strings(self): + self.assertEqual( + sorted_list_difference(['a', 'b'], ['b', 'c']), + (['a'], ['c'])) + self.assertEqual( + sorted_list_difference([], ['a', 'a', 'b']), + ([], ['a', 'b'])) + self.assertEqual( + sorted_list_difference(['a', 'a', 'b'], []), + (['a', 'b'], [])) + + def test_sorted_list_difference_unhashable(self): + self.assertEqual( + sorted_list_difference([[1], [2]], [[2], [3]]), + ([[1]], [[3]])) + self.assertEqual( + sorted_list_difference([], [[0], [0]]), + ([], [[0]])) + self.assertEqual( + sorted_list_difference([[0], [0]], []), + ([[0]], [])) + def test_unorderable_list_difference(self): self.assertEqual(unorderable_list_difference([], []), ([], [])) self.assertEqual(unorderable_list_difference([1, 2], []), ([2, 1], [])) diff --git a/Lib/unittest/util.py b/Lib/unittest/util.py index 127cacfc2146c4..1c6dafcc0db7e4 100644 --- a/Lib/unittest/util.py +++ b/Lib/unittest/util.py @@ -63,6 +63,17 @@ def safe_repr(obj, short=False): def strclass(cls): return "%s.%s" % (cls.__module__, cls.__qualname__) +def _dedupe_sorted(lst): + """Remove consecutive duplicate elements from a sorted list. + + Only requires that elements support equality comparison, + not hashing.""" + result = [] + for item in lst: + if not result or result[-1] != item: + result.append(item) + return result + def sorted_list_difference(expected, actual): """Finds elements in only one or the other of two, sorted input lists. @@ -98,8 +109,8 @@ def sorted_list_difference(expected, actual): while actual[j] == a: j += 1 except IndexError: - missing.extend(dict.fromkeys(expected[i:])) - unexpected.extend(dict.fromkeys(actual[j:])) + missing.extend(_dedupe_sorted(expected[i:])) + unexpected.extend(_dedupe_sorted(actual[j:])) break return missing, unexpected