From f4c720e982c8d2205b958fc478788917a9a4cb6d Mon Sep 17 00:00:00 2001 From: passionworkeer Date: Sat, 14 Mar 2026 20:41:47 +0800 Subject: [PATCH 1/4] fix: handle tuples in deepcopy_minimal to prevent dict mutation The deepcopy_minimal function only handled dict and list types, but not tuples. When a tuple containing a dict was passed, the dict inside the tuple could be mutated in-place during processing. This fix adds tuple handling to recursively copy tuple elements, preventing in-place mutations of dicts or lists nested inside tuples. Closes #1202 --- src/anthropic/_utils/_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/anthropic/_utils/_utils.py b/src/anthropic/_utils/_utils.py index eec7f4a1..203d6db2 100644 --- a/src/anthropic/_utils/_utils.py +++ b/src/anthropic/_utils/_utils.py @@ -181,6 +181,7 @@ def deepcopy_minimal(item: _T) -> _T: - mappings, e.g. `dict` - list + - tuple This is done for performance reasons. """ @@ -188,6 +189,8 @@ def deepcopy_minimal(item: _T) -> _T: return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) if is_list(item): return cast(_T, [deepcopy_minimal(entry) for entry in item]) + if isinstance(item, tuple): + return cast(_T, tuple(deepcopy_minimal(entry) for entry in item)) return item From 97de9afbeae07c1c50770013af5dbf22cfe50d09 Mon Sep 17 00:00:00 2001 From: TRIX Date: Sat, 21 Mar 2026 15:29:14 +0800 Subject: [PATCH 2/4] fix: update deepcopy_minimal tuple handling per PR feedback - Use is_tuple() TypeGuard helper instead of isinstance check - Update test_ignores_other_types to reflect new tuple behavior - Add assertion that dict nested inside tuple is copied --- src/anthropic/_utils/_utils.py | 2 +- tests/test_deepcopy.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/anthropic/_utils/_utils.py b/src/anthropic/_utils/_utils.py index 203d6db2..d66ea71d 100644 --- a/src/anthropic/_utils/_utils.py +++ b/src/anthropic/_utils/_utils.py @@ -189,7 +189,7 @@ def deepcopy_minimal(item: _T) -> _T: return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) if is_list(item): return cast(_T, [deepcopy_minimal(entry) for entry in item]) - if isinstance(item, tuple): + if is_tuple(item): return cast(_T, tuple(deepcopy_minimal(entry) for entry in item)) return item diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py index edbbfb22..67d0c15a 100644 --- a/tests/test_deepcopy.py +++ b/tests/test_deepcopy.py @@ -52,7 +52,16 @@ def test_ignores_other_types() -> None: assert_different_identities(obj1, obj2) assert obj1["foo"] is my_obj - # tuples + # tuples - new tuple created but immutable contents not copied obj3 = ("a", "b") obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 + assert obj3 is not obj4 # new tuple created + assert obj3[0] is obj4[0] # but immutable strings are same object + assert obj3[1] is obj4[1] + + # tuples with dicts inside - dict should be copied + inner_dict = {"bar": True} + obj5 = ("a", inner_dict) + obj6 = deepcopy_minimal(obj5) + assert obj5 is not obj6 # new tuple created + assert obj5[1] is not obj6[1] # dict inside is copied From 8af4400791b4ead2da79d93c1c33796f3550bd5d Mon Sep 17 00:00:00 2001 From: passionworkeer <2089966424@qq.com> Date: Sat, 28 Mar 2026 08:10:42 +0800 Subject: [PATCH 3/4] test(deepcopy): add dedicated test_simple_tuple and test_nested_tuple Adds explicit standalone tuple coverage alongside the existing test_ignores_other_types tuple cases, matching the pattern of test_simple_dict / test_nested_dict and test_simple_list / test_nested_list. Confirms immutable elements are preserved by identity while mutable elements (dicts/lists inside tuples) are deep-copied. Co-Authored-By: Claude Sonnet 4.6 --- tests/test_deepcopy.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py index 67d0c15a..e15ab634 100644 --- a/tests/test_deepcopy.py +++ b/tests/test_deepcopy.py @@ -41,6 +41,22 @@ def test_nested_list() -> None: assert_different_identities(obj1[1], obj2[1]) +def test_simple_tuple() -> None: + obj1 = ("a", "b") + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert obj1[0] is obj2[0] # immutable strings are same object + assert obj1[1] is obj2[1] + + +def test_nested_tuple() -> None: + obj1 = ("a", {"bar": True}) + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1[1], obj2[1]) + assert obj1[0] is obj2[0] # immutable string preserved + + class MyObject: ... From 25dacc4383e702516873a20e77b85396c9b88876 Mon Sep 17 00:00:00 2001 From: passionworkeer <2089966424@qq.com> Date: Sat, 28 Mar 2026 08:13:41 +0800 Subject: [PATCH 4/4] test: add regression coverage for tuple contents in deepcopy_minimal - Add test cases for list-inside-tuple and deeply-nested tuple structures (tuple -> dict -> list -> dict) to ensure all mutable contents within tuples are recursively deep-copied. - Remove duplicate tuple cases from test_ignores_other_types now that test_simple_tuple and test_nested_tuple cover them explicitly. - Confirms immutable elements (strings, ints) inside tuples are returned as the same object (no unnecessary copy) while mutable elements are properly isolated. Co-Authored-By: Claude Sonnet 4.6 --- tests/test_deepcopy.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py index e15ab634..03d2bd71 100644 --- a/tests/test_deepcopy.py +++ b/tests/test_deepcopy.py @@ -68,16 +68,18 @@ def test_ignores_other_types() -> None: assert_different_identities(obj1, obj2) assert obj1["foo"] is my_obj - # tuples - new tuple created but immutable contents not copied - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is not obj4 # new tuple created - assert obj3[0] is obj4[0] # but immutable strings are same object - assert obj3[1] is obj4[1] - - # tuples with dicts inside - dict should be copied - inner_dict = {"bar": True} - obj5 = ("a", inner_dict) - obj6 = deepcopy_minimal(obj5) - assert obj5 is not obj6 # new tuple created - assert obj5[1] is not obj6[1] # dict inside is copied + # tuples with lists inside - list should be copied + inner_list = [1, {"x": "y"}] + obj7 = ("z", inner_list) + obj8 = deepcopy_minimal(obj7) + assert obj7 is not obj8 # new tuple created + assert obj7[1] is not obj8[1] # list inside is copied + assert obj7[1][1] is not obj8[1][1] # dict inside that list is also copied + + # deeply nested: tuple -> dict -> list -> dict + obj9 = ({"items": [{"name": "a"}]},) + obj10 = deepcopy_minimal(obj9) + assert obj9 is not obj10 + assert obj9[0] is not obj10[0] # dict in tuple copied + assert obj9[0]["items"] is not obj10[0]["items"] # list in dict copied + assert obj9[0]["items"][0] is not obj10[0]["items"][0] # dict in list copied