From b91dccb42249187beff83d246be4b223fcddb7ec Mon Sep 17 00:00:00 2001 From: Lavkesh Dwivedi <9712103+lavkeshdwivedi@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:58:43 -0500 Subject: [PATCH] fix: guard against bare unparameterized dict/list in construct_type and transform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_args(dict) and get_args(list) return empty tuples when the type has no type parameters. Three code paths assumed at least one/two elements and crashed on bare annotations: - _models.py: `_, items_type = get_args(type_)` → ValueError for bare dict - _models.py: `inner_type = args[0]` → IndexError for bare list - _utils/_transform.py (sync + async): `get_args(stripped_type)[1]` → IndexError for bare dict All three now guard with len checks and fall back to `object` as the item/value type when no type arguments are present, matching the behaviour callers would expect from unparameterized collections. Fixes #1619, #1626, #1628 --- src/anthropic/_models.py | 5 +++-- src/anthropic/_utils/_transform.py | 6 ++++-- tests/test_models.py | 16 ++++++++++++++++ tests/test_transform.py | 10 ++++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/anthropic/_models.py b/src/anthropic/_models.py index dc00516bc..a74f41086 100644 --- a/src/anthropic/_models.py +++ b/src/anthropic/_models.py @@ -647,7 +647,8 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] if not is_mapping(value): return value - _, items_type = get_args(type_) # Dict[_, items_type] + dict_args = get_args(type_) # Dict[_, items_type] + items_type = dict_args[1] if len(dict_args) >= 2 else object return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} if ( @@ -668,7 +669,7 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] if not is_list(value): return value - inner_type = args[0] # List[inner_type] + inner_type = args[0] if args else object # List[inner_type] return [construct_type(value=entry, type_=inner_type) for entry in value] if origin == float: diff --git a/src/anthropic/_utils/_transform.py b/src/anthropic/_utils/_transform.py index 1331da174..d98440c6a 100644 --- a/src/anthropic/_utils/_transform.py +++ b/src/anthropic/_utils/_transform.py @@ -180,7 +180,8 @@ def _transform_recursive( return _transform_typeddict(data, stripped_type) if origin == dict and is_mapping(data): - items_type = get_args(stripped_type)[1] + _dict_args = get_args(stripped_type) + items_type = _dict_args[1] if len(_dict_args) >= 2 else object return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} if ( @@ -348,7 +349,8 @@ async def _async_transform_recursive( return await _async_transform_typeddict(data, stripped_type) if origin == dict and is_mapping(data): - items_type = get_args(stripped_type)[1] + _dict_args = get_args(stripped_type) + items_type = _dict_args[1] if len(_dict_args) >= 2 else object return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} if ( diff --git a/tests/test_models.py b/tests/test_models.py index 195f23079..a7fbb2f3b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1014,4 +1014,20 @@ class Model(BaseModel): # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) assert m.data["items"] == ["h", "e", "l", "l", "o"] + + +def test_construct_type_bare_dict_does_not_crash() -> None: + # Bare `dict` (no type parameters) previously raised: + # ValueError: not enough values to unpack (expected 2, got 0) + # because get_args(dict) returns () and the code unpacked it as two values. + result = construct_type(value={"key": "value"}, type_=dict) + assert result == {"key": "value"} + + +def test_construct_type_bare_list_does_not_crash() -> None: + # Bare `list` (no type parameter) previously raised: + # IndexError: tuple index out of range + # because get_args(list) returns () and the code indexed args[0]. + result = construct_type(value=["a", "b", "c"], type_=list) + assert result == ["a", "b", "c"] assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"] diff --git a/tests/test_transform.py b/tests/test_transform.py index 68fe46a2d..c7d6dc62f 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -502,3 +502,13 @@ async def test_strips_notgiven(use_async: bool) -> None: async def test_strips_omit(use_async: bool) -> None: assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} assert await transform({"foo_bar": omit}, Foo1, use_async) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_bare_dict_annotation_does_not_crash(use_async: bool) -> None: + # Bare `dict` (no type parameters) previously raised: + # IndexError: tuple index out of range + # because get_args(dict) returns () and the code indexed [1] unconditionally. + result = await transform({"key": "value"}, dict, use_async) + assert result == {"key": "value"}