From b5dd5faba9b55fbffa28e943d1fb9e7723dc41e1 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:56:53 -0700 Subject: [PATCH] Fix TypeError when a keypath sets a string subkey through a list value Setting a keypath whose parent value is a list but whose next key is a non-integer string (e.g. b['a.x'] = 1 with a=[1, 2]) raised a raw TypeError: list indices must be integers or slices, not str, while the same assignment over a scalar/None value overrides it with a new dict. A list cannot hold a string key, so treat it like a scalar and override it, keeping the behavior consistent (b['a.x'] -> {'a': {'x': 1}}). Valid integer indexes still address the list unchanged. --- benedict/dicts/keylist/keylist_util.py | 6 +++++- tests/dicts/keypath/test_keypath_dict.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/benedict/dicts/keylist/keylist_util.py b/benedict/dicts/keylist/keylist_util.py index cfa773c1..9d8b9aa1 100644 --- a/benedict/dicts/keylist/keylist_util.py +++ b/benedict/dicts/keylist/keylist_util.py @@ -28,7 +28,11 @@ def _get_item_key_and_value(item: Any, key: Any) -> tuple[Any, Any]: def _get_or_new_item_value(item: Any, key: Any, subkey: Any) -> Any: try: _, value = _get_item_key_and_value(item, key) - if not type_util.is_dict_or_list_or_tuple(value): + # a list can only hold an integer-index subkey; anything else needs a dict + keeps_subkey = type_util.is_dict(value) or ( + type_util.is_list_or_tuple(value) and _get_index(subkey) is not None + ) + if not keeps_subkey: raise TypeError except (IndexError, KeyError, TypeError): value = _new_item_value(subkey) diff --git a/tests/dicts/keypath/test_keypath_dict.py b/tests/dicts/keypath/test_keypath_dict.py index 639cd1eb..9c6a703e 100644 --- a/tests/dicts/keypath/test_keypath_dict.py +++ b/tests/dicts/keypath/test_keypath_dict.py @@ -499,6 +499,23 @@ def test_setitem_override_existing_item(self) -> None: r = {"a": {"b": {"c": {"d": 3}}}} self.assertEqual(b, r) + def test_setitem_string_subkey_overrides_list_value(self) -> None: + # a string subkey cannot index a list, so it overrides the list like a scalar + b = KeypathDict({"a": [1, 2]}) + b["a.x"] = 1 + self.assertEqual(b, {"a": {"x": 1}}) + b = KeypathDict({"a": [1, 2]}) + b["a.x.y"] = 1 + self.assertEqual(b, {"a": {"x": {"y": 1}}}) + # a list-of-scalar element is likewise overridden by a string subkey + b = KeypathDict({"a": [5]}) + b["a[0].x"] = 3 + self.assertEqual(b, {"a": [{"x": 3}]}) + # a valid integer index keeps the list + b = KeypathDict({"a": [1, 2]}) + b["a[0]"] = 9 + self.assertEqual(b, {"a": [9, 2]}) + def test_setitem_with_keys_list(self) -> None: d: dict[str, Any] = { "a": {