From 65a0f561c3c3714f70de0c47a669024046e0a72d Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 26 Feb 2026 15:03:37 +0000 Subject: [PATCH 1/7] 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 b773efb7a6d8774b5dea1c0dc624c98458cd84a1 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 5 Mar 2026 14:02:49 +0000 Subject: [PATCH 2/7] 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 8cdbb0ab565ec92f39588ac08964f6c6c4eaf418 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/7] =?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 351df20da7b2fdfc1c4dde1b912dfff3418ce251 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 5 Mar 2026 14:22:39 +0000 Subject: [PATCH 4/7] 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 20ebc5f02d8e80774021f73a59366608b5929fc2 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 5 Mar 2026 15:29:55 +0000 Subject: [PATCH 5/7] 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 ac25521a74d1b8c9a7ecd72e94c1b8901c3bab4a Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Thu, 5 Mar 2026 15:43:16 +0000 Subject: [PATCH 6/7] 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 From b965a083475cec518fbf9a0ecb0ef8635bb60b15 Mon Sep 17 00:00:00 2001 From: Stefan Zetzsche Date: Tue, 10 Mar 2026 14:31:17 +0000 Subject: [PATCH 7/7] Remove unnecessary docstring detail from _dedupe_sorted --- Lib/unittest/util.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/unittest/util.py b/Lib/unittest/util.py index 1c6dafcc0db7e4..0681163c979587 100644 --- a/Lib/unittest/util.py +++ b/Lib/unittest/util.py @@ -64,10 +64,7 @@ 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.""" + """Remove consecutive duplicate elements from a sorted list.""" result = [] for item in lst: if not result or result[-1] != item: