From 7e5327e0c92597defb15b6d49b45ce57ceda7eb2 Mon Sep 17 00:00:00 2001 From: bilibili12433014 <73748897+bilibili12433014@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:35:20 +0800 Subject: [PATCH 1/6] Implement attribute-based fallback for subscript access Added attribute-based fallback for __getitem__ method. --- Objects/abstract.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Objects/abstract.c b/Objects/abstract.c index 8adad8407d04d4..deceeef87f96ad 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -201,6 +201,20 @@ PyObject_GetItem(PyObject *o, PyObject *key) ((PyTypeObject *)o)->tp_name); return NULL; } + + // Try attribute-based fallback: obj.__getitem__(key) + PyObject *attr_getitem = PyObject_GetAttr(o, &_Py_ID(__getitem__)); + if (attr_getitem != NULL) { + if (PyCallable_Check(attr_getitem)) { + PyObject *res = PyObject_CallOneArg(attr_getitem, key); + Py_DECREF(attr_getitem); + return res; + } + Py_DECREF(attr_getitem); + } + else { + PyErr_Clear(); + } return type_error("'%.200s' object is not subscriptable", o); } From 41bc293ef1db936a08d5c07fdfb7b7eecc0b6fb8 Mon Sep 17 00:00:00 2001 From: bilibili12433014 <73748897+bilibili12433014@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:44:12 +0800 Subject: [PATCH 2/6] Refine subscription process explanation in docs --- Doc/reference/datamodel.rst | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index ebadbc215a0eed..e3c971b2db9ae4 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -3037,9 +3037,8 @@ the class method :meth:`~object.__class_getitem__` may be called instead. object if it is properly defined. Presented with the :term:`expression` ``obj[x]``, the Python interpreter -follows something like the following process to decide whether -:meth:`~object.__getitem__` or :meth:`~object.__class_getitem__` should be -called:: +follows a process similar to the following to determine which operation should +be used to perform the subscription:: from inspect import isclass @@ -3048,21 +3047,37 @@ called:: class_of_obj = type(obj) - # If the class of obj defines __getitem__, - # call class_of_obj.__getitem__(obj, x) + # 1. If the class defines a mapping slot (__getitem__), + # call it directly. if hasattr(class_of_obj, '__getitem__'): return class_of_obj.__getitem__(obj, x) - # Else, if obj is a class and defines __class_getitem__, + # 2. If the class defines a sequence slot (sq_item) + # and the key is an integer, call sq_item. + if hasattr(class_of_obj, '__getitem__') is False: + # simplified model: CPython checks tp_as_sequence->sq_item + if isinstance(x, int) and hasattr(class_of_obj, '__len__'): + return obj.__getitem__(x) + + # 3. If obj is a class and defines __class_getitem__, # call obj.__class_getitem__(x) - elif isclass(obj) and hasattr(obj, '__class_getitem__'): + if isclass(obj) and hasattr(obj, '__class_getitem__'): return obj.__class_getitem__(x) - # Else, raise an exception + # 4. Attribute-based fallback: if __getitem__ is provided + # by __getattribute__ or __getattr__, call it. + try: + attr_getitem = obj.__getattribute__('__getitem__') + except AttributeError: + pass else: - raise TypeError( - f"'{class_of_obj.__name__}' object is not subscriptable" - ) + if callable(attr_getitem): + return attr_getitem(x) + + # 5. Otherwise, raise an exception. + raise TypeError( + f"'{class_of_obj.__name__}' object is not subscriptable" + ) In Python, all classes are themselves instances of other classes. The class of a class is known as that class's :term:`metaclass`, and most classes have the From 4a5e6c9d975e850267755029f8d787dc8d4e6102 Mon Sep 17 00:00:00 2001 From: bilibili12433014 <73748897+bilibili12433014@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:15:58 +0800 Subject: [PATCH 3/6] add NEWS files: Enhance subscripting with attribute-based fallback --- .../next/Core_and_Builtins/subscript-attr-fallback.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/subscript-attr-fallback.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/subscript-attr-fallback.rst b/Misc/NEWS.d/next/Core_and_Builtins/subscript-attr-fallback.rst new file mode 100644 index 00000000000000..8510a130a0654e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/subscript-attr-fallback.rst @@ -0,0 +1,6 @@ +The subscript operator (``obj[key]``) now falls back to an attribute-based +lookup of ``__getitem__`` when the type does not define a mapping or +sequence slot. This lookup follows normal attribute resolution rules, +including ``__getattribute__`` and ``__getattr__``. This allows classes +that provide ``__getitem__`` purely as an attribute or through dynamic +attribute handlers to support subscripting. From d1d9fd96827d405274574e8c3ac582fa4715f00b Mon Sep 17 00:00:00 2001 From: bilibili12433014 <73748897+bilibili12433014@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:45:24 +0800 Subject: [PATCH 4/6] Implement test for __getitem__ attribute fallback --- Lib/test/test_descr.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 82a48ad4d1aced..dad3c165c25ef2 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -6261,5 +6261,23 @@ class IntSubclass(int): weakref_descriptor.__get__(IntSubclass(), IntSubclass) +class TestGetItemAttributeFallback(unittest.TestCase): + + def test_attribute_fallback_for_configview(self): + class ConfigView: + def __init__(self, target): + self.target = target + + def __getattr__(self, name): + return getattr(self.target, name) + + class Config: + def __getitem__(self, key): + return ("view", key) + + cfg = ConfigView(Config()) + self.assertEqual(cfg["x"], ("view", "x")) + + if __name__ == "__main__": unittest.main() From a9f3e6bb904ccb2817d74926f4869d0c4eee3e44 Mon Sep 17 00:00:00 2001 From: bilibili12433014 <73748897+bilibili12433014@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:18:25 +0800 Subject: [PATCH 5/6] Rename subscript-attr-fallback.rst to 2025-11-26-23-15-10.gh-issue-141980.A8cX7Q.rst --- ...allback.rst => 2025-11-26-23-15-10.gh-issue-141980.A8cX7Q.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Core_and_Builtins/{subscript-attr-fallback.rst => 2025-11-26-23-15-10.gh-issue-141980.A8cX7Q.rst} (100%) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/subscript-attr-fallback.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-26-23-15-10.gh-issue-141980.A8cX7Q.rst similarity index 100% rename from Misc/NEWS.d/next/Core_and_Builtins/subscript-attr-fallback.rst rename to Misc/NEWS.d/next/Core_and_Builtins/2025-11-26-23-15-10.gh-issue-141980.A8cX7Q.rst From 0d6935639c6e95376665ce7f1f229640a25705fd Mon Sep 17 00:00:00 2001 From: bilibili12433014 <73748897+bilibili12433014@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:27:13 +0800 Subject: [PATCH 6/6] Fix formatting and spacing in abstract.c --- Objects/abstract.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/abstract.c b/Objects/abstract.c index deceeef87f96ad..842e8e420d47c5 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -201,7 +201,7 @@ PyObject_GetItem(PyObject *o, PyObject *key) ((PyTypeObject *)o)->tp_name); return NULL; } - + // Try attribute-based fallback: obj.__getitem__(key) PyObject *attr_getitem = PyObject_GetAttr(o, &_Py_ID(__getitem__)); if (attr_getitem != NULL) { @@ -211,7 +211,7 @@ PyObject_GetItem(PyObject *o, PyObject *key) return res; } Py_DECREF(attr_getitem); - } + } else { PyErr_Clear(); }