From 1a237dae0162ffc93b0f785bd811dff49201dada Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 15:26:15 +0800 Subject: [PATCH 01/22] expose _PyObject_LookupSpecialMethod to types.lookup_special_method --- Doc/library/types.rst | 8 ++++++++ Lib/test/test_types.py | 45 ++++++++++++++++++++++++++++++++++++++++-- Modules/_typesmodule.c | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/Doc/library/types.rst b/Doc/library/types.rst index 01f4df3c89091f..ae97ef5e9a146f 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -521,6 +521,14 @@ Additional Utility Classes and Functions .. versionadded:: 3.4 +.. function:: lookup_special_method(obj, attr) + + Do a method lookup in the type without looking in the instance dictionary + but still binding it to the instance. Returns None if the method is not + found. + + .. versionadded:: 3.15 + Coroutine Utility Functions --------------------------- diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 39d57c5f5b61c9..bc585ce6ebb9b9 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -41,7 +41,8 @@ def clear_typing_caches(): class TypesTests(unittest.TestCase): def test_names(self): - c_only_names = {'CapsuleType', 'LazyImportType'} + c_only_names = {'CapsuleType', 'LazyImportType', + 'lookup_special_method'} ignored = {'new_class', 'resolve_bases', 'prepare_class', 'get_original_bases', 'DynamicClassAttribute', 'coroutine'} @@ -59,7 +60,7 @@ def test_names(self): 'MemberDescriptorType', 'MethodDescriptorType', 'MethodType', 'MethodWrapperType', 'ModuleType', 'NoneType', 'NotImplementedType', 'SimpleNamespace', 'TracebackType', - 'UnionType', 'WrapperDescriptorType', + 'UnionType', 'WrapperDescriptorType', 'lookup_special_method', } self.assertEqual(all_names, set(c_types.__all__)) self.assertEqual(all_names - c_only_names, set(py_types.__all__)) @@ -726,6 +727,46 @@ def test_frame_locals_proxy_type(self): self.assertIsNotNone(frame) self.assertIsInstance(frame.f_locals, types.FrameLocalsProxyType) + def test_lookup_special_method(self): + class CM1: + def __enter__(self): + return "__enter__ from class __dict__" + + class CM2: + def __init__(self): + def __enter__(self): + return "__enter__ from instance __dict__" + self.__enter__ = __enter__ + + class CM3: + __slots__ = ("__enter__",) + def __init__(self): + def __enter__(self): + return "__enter__ from __slots__" + self.__enter__ = __enter__ + cm1 = CM1() + meth = types.lookup_special_method(cm1, "__enter__") + self.assertIsNotNone(meth) + self.assertEqual(meth(cm1), "__enter__ from class __dict__") + + meth = types.lookup_special_method(cm1, "__missing__") + self.assertIsNone(meth) + + with self.assertRaises(TypeError): + types.lookup_special_method(cm1, 123) + + cm2 = CM2() + meth = types.lookup_special_method(cm2, "__enter__") + self.assertIsNone(meth) + + cm3 = CM3() + meth = types.lookup_special_method(cm3, "__enter__") + self.assertIsNotNone(meth) + self.assertEqual(meth(cm3), "__enter__ from __slots__") + + meth = types.lookup_special_method([], "__len__") + self.assertIsNotNone(meth) + self.assertEqual(meth([]), 0) class UnionTests(unittest.TestCase): diff --git a/Modules/_typesmodule.c b/Modules/_typesmodule.c index 6c9e7a0a3ba053..bf216de42bc799 100644 --- a/Modules/_typesmodule.c +++ b/Modules/_typesmodule.c @@ -6,6 +6,34 @@ #include "pycore_namespace.h" // _PyNamespace_Type #include "pycore_object.h" // _PyNone_Type, _PyNotImplemented_Type #include "pycore_unionobject.h" // _PyUnion_Type +#include "pycore_typeobject.h" // _PyObject_LookupSpecialMethod +#include "pycore_stackref.h" // _PyStackRef + +static PyObject * +_types_lookup_special_method_impl(PyObject *self, PyObject *args) +{ + PyObject *obj, *attr; + if (!PyArg_ParseTuple(args, "OO", &obj, &attr)) { + return NULL; + } + if (!PyUnicode_Check(attr)) { + PyErr_Format(PyExc_TypeError, "attribute name must be string, not '%.200s'", + Py_TYPE(attr)->tp_name); + return NULL; + } + _PyStackRef method_and_self[2]; + method_and_self[0] = PyStackRef_NULL; + method_and_self[1] = PyStackRef_FromPyObjectBorrow(obj); + int result = _PyObject_LookupSpecialMethod(attr, method_and_self); + if (result == -1) { + return NULL; + } + if (result == 0) { + Py_RETURN_NONE; + } + PyObject *method = PyStackRef_AsPyObjectSteal(method_and_self[0]); + return Py_BuildValue("O", method); +} static int _types_exec(PyObject *m) @@ -60,12 +88,20 @@ static struct PyModuleDef_Slot _typesmodule_slots[] = { {0, NULL} }; +static PyMethodDef _typesmodule_methods[] = { + {"lookup_special_method", _types_lookup_special_method_impl, METH_VARARGS, + "Do a method lookup in the type without looking in the instance " + "dictionary but still binding it to the instance. Returns None if the " + "method is not found."}, + {NULL, NULL, 0, NULL}}; + static struct PyModuleDef typesmodule = { .m_base = PyModuleDef_HEAD_INIT, .m_name = "_types", .m_doc = "Define names for built-in types.", .m_size = 0, .m_slots = _typesmodule_slots, + .m_methods = _typesmodule_methods, }; PyMODINIT_FUNC From d00b90ee1e9613071aeef009d4623069664363b3 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 17:47:20 +0800 Subject: [PATCH 02/22] blurb --- .../next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst diff --git a/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst b/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst new file mode 100644 index 00000000000000..affeb577377270 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst @@ -0,0 +1,2 @@ +Expose ``_PyObject_LookupSpecialMethod()`` as +``types.lookup_special_method(obj, attr)``. From be4bcc2b09567b25948a8a3ef28a7a0f6ab6db1e Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 18:46:14 +0800 Subject: [PATCH 03/22] Add signature; Fix test_inspect.test_inspect error --- Doc/library/types.rst | 2 +- Modules/_typesmodule.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/types.rst b/Doc/library/types.rst index ae97ef5e9a146f..a8e53431f7378e 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -521,7 +521,7 @@ Additional Utility Classes and Functions .. versionadded:: 3.4 -.. function:: lookup_special_method(obj, attr) +.. function:: lookup_special_method(obj, attr, /) Do a method lookup in the type without looking in the instance dictionary but still binding it to the instance. Returns None if the method is not diff --git a/Modules/_typesmodule.c b/Modules/_typesmodule.c index bf216de42bc799..1a28fea13e1d68 100644 --- a/Modules/_typesmodule.c +++ b/Modules/_typesmodule.c @@ -90,9 +90,9 @@ static struct PyModuleDef_Slot _typesmodule_slots[] = { static PyMethodDef _typesmodule_methods[] = { {"lookup_special_method", _types_lookup_special_method_impl, METH_VARARGS, + "lookup_special_method(obj, attr, /)\n--\n\n" "Do a method lookup in the type without looking in the instance " - "dictionary but still binding it to the instance. Returns None if the " - "method is not found."}, + "dictionary. Returns None if the method is not found."}, {NULL, NULL, 0, NULL}}; static struct PyModuleDef typesmodule = { From 62b5d214f488ca812a3f9e9ea82c14ff847c3b46 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 22:17:31 +0800 Subject: [PATCH 04/22] adjust indent width as 4 --- Modules/_typesmodule.c | 43 +++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/Modules/_typesmodule.c b/Modules/_typesmodule.c index 1a28fea13e1d68..f3a0b0586a8174 100644 --- a/Modules/_typesmodule.c +++ b/Modules/_typesmodule.c @@ -12,27 +12,28 @@ static PyObject * _types_lookup_special_method_impl(PyObject *self, PyObject *args) { - PyObject *obj, *attr; - if (!PyArg_ParseTuple(args, "OO", &obj, &attr)) { - return NULL; - } - if (!PyUnicode_Check(attr)) { - PyErr_Format(PyExc_TypeError, "attribute name must be string, not '%.200s'", - Py_TYPE(attr)->tp_name); - return NULL; - } - _PyStackRef method_and_self[2]; - method_and_self[0] = PyStackRef_NULL; - method_and_self[1] = PyStackRef_FromPyObjectBorrow(obj); - int result = _PyObject_LookupSpecialMethod(attr, method_and_self); - if (result == -1) { - return NULL; - } - if (result == 0) { - Py_RETURN_NONE; - } - PyObject *method = PyStackRef_AsPyObjectSteal(method_and_self[0]); - return Py_BuildValue("O", method); + PyObject *obj, *attr; + if (!PyArg_ParseTuple(args, "OO", &obj, &attr)) { + return NULL; + } + if (!PyUnicode_Check(attr)) { + PyErr_Format(PyExc_TypeError, + "attribute name must be string, not '%.200s'", + Py_TYPE(attr)->tp_name); + return NULL; + } + _PyStackRef method_and_self[2]; + method_and_self[0] = PyStackRef_NULL; + method_and_self[1] = PyStackRef_FromPyObjectBorrow(obj); + int result = _PyObject_LookupSpecialMethod(attr, method_and_self); + if (result == -1) { + return NULL; + } + if (result == 0) { + Py_RETURN_NONE; + } + PyObject *method = PyStackRef_AsPyObjectSteal(method_and_self[0]); + return Py_BuildValue("O", method); } static int From 6f8d459ae871f80b6262d86a4165a9685ad4b530 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 22:18:23 +0800 Subject: [PATCH 05/22] remove unnecessary Py_BuildValue() --- Modules/_typesmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_typesmodule.c b/Modules/_typesmodule.c index f3a0b0586a8174..a8cac0c13df2a7 100644 --- a/Modules/_typesmodule.c +++ b/Modules/_typesmodule.c @@ -33,7 +33,7 @@ _types_lookup_special_method_impl(PyObject *self, PyObject *args) Py_RETURN_NONE; } PyObject *method = PyStackRef_AsPyObjectSteal(method_and_self[0]); - return Py_BuildValue("O", method); + return method; } static int From f6c716e7891302b0fd84776c093e632e57d61e62 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 22:19:25 +0800 Subject: [PATCH 06/22] versionadded:: next --- Doc/library/types.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/types.rst b/Doc/library/types.rst index a8e53431f7378e..1bb512fb865bd2 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -527,7 +527,7 @@ Additional Utility Classes and Functions but still binding it to the instance. Returns None if the method is not found. - .. versionadded:: 3.15 + .. versionadded:: next Coroutine Utility Functions From db6264872802c5f630ef7c6bec7959cad76fb6d7 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 22:19:47 +0800 Subject: [PATCH 07/22] surround None with backticks --- Doc/library/types.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/types.rst b/Doc/library/types.rst index 1bb512fb865bd2..730a2bd9334a69 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -524,7 +524,7 @@ Additional Utility Classes and Functions .. function:: lookup_special_method(obj, attr, /) Do a method lookup in the type without looking in the instance dictionary - but still binding it to the instance. Returns None if the method is not + but still binding it to the instance. Returns ``None`` if the method is not found. .. versionadded:: next From 49f891cfdfd9520d80c8b5cda90d66867e0f2481 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 22:21:09 +0800 Subject: [PATCH 08/22] add :func: in news entry --- .../next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst b/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst index affeb577377270..94c432df48d923 100644 --- a/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst +++ b/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst @@ -1,2 +1,2 @@ Expose ``_PyObject_LookupSpecialMethod()`` as -``types.lookup_special_method(obj, attr)``. +:func:`types.lookup_special_method(obj, attr)`. From 05d25e4317fd1d7b9b1b2d32056710075bfc3ffb Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 22:25:44 +0800 Subject: [PATCH 09/22] add whats new in python3.15 --- Doc/whatsnew/3.15.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 0e440ccfd011f0..18b5306d33905a 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1064,6 +1064,8 @@ types This represents the type of the :attr:`frame.f_locals` attribute, as described in :pep:`667`. +* Expose ``_PyObject_LookupSpecialMethod()`` as + :func:`types.lookup_special_method(obj, attr)`. unicodedata ----------- From 00582fe308a9b213893aa22d2e8c751ae5885d0c Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 22:27:09 +0800 Subject: [PATCH 10/22] add positional-only marker --- Doc/whatsnew/3.15.rst | 2 +- .../next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 18b5306d33905a..afc7460053d2b4 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1065,7 +1065,7 @@ types as described in :pep:`667`. * Expose ``_PyObject_LookupSpecialMethod()`` as - :func:`types.lookup_special_method(obj, attr)`. + :func:`types.lookup_special_method(obj, attr, /)`. unicodedata ----------- diff --git a/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst b/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst index 94c432df48d923..a55a5cf86faeec 100644 --- a/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst +++ b/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst @@ -1,2 +1,2 @@ Expose ``_PyObject_LookupSpecialMethod()`` as -:func:`types.lookup_special_method(obj, attr)`. +:func:`types.lookup_special_method(obj, attr, /)`. From 25af12d7061c56ebd96c4a6fc346a7c41fb6da3a Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 23:14:22 +0800 Subject: [PATCH 11/22] use Argument Clinic intead of PyArg_ParseTuple --- Modules/_typesmodule.c | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/Modules/_typesmodule.c b/Modules/_typesmodule.c index a8cac0c13df2a7..6ec40681cca06b 100644 --- a/Modules/_typesmodule.c +++ b/Modules/_typesmodule.c @@ -8,14 +8,31 @@ #include "pycore_unionobject.h" // _PyUnion_Type #include "pycore_typeobject.h" // _PyObject_LookupSpecialMethod #include "pycore_stackref.h" // _PyStackRef +#include "clinic/_typesmodule.c.h" + +/*[clinic input] +module _types +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=530308b1011b659d]*/ + +/*[clinic input] +_types.lookup_special_method + + obj: 'O' + attr: 'O' + / + +Lookup special method name `attr` on `obj`. + +Lookup method `attr` on `obj` without looking in the instance dictionary. +Returns `None` if the method is not found. +[clinic start generated code]*/ static PyObject * -_types_lookup_special_method_impl(PyObject *self, PyObject *args) +_types_lookup_special_method_impl(PyObject *module, PyObject *obj, + PyObject *attr) +/*[clinic end generated code: output=890e22cc0b8e0d34 input=f26012b0c90b81cd]*/ { - PyObject *obj, *attr; - if (!PyArg_ParseTuple(args, "OO", &obj, &attr)) { - return NULL; - } if (!PyUnicode_Check(attr)) { PyErr_Format(PyExc_TypeError, "attribute name must be string, not '%.200s'", @@ -90,11 +107,9 @@ static struct PyModuleDef_Slot _typesmodule_slots[] = { }; static PyMethodDef _typesmodule_methods[] = { - {"lookup_special_method", _types_lookup_special_method_impl, METH_VARARGS, - "lookup_special_method(obj, attr, /)\n--\n\n" - "Do a method lookup in the type without looking in the instance " - "dictionary. Returns None if the method is not found."}, - {NULL, NULL, 0, NULL}}; + _TYPES_LOOKUP_SPECIAL_METHOD_METHODDEF + {NULL, NULL, 0, NULL} +}; static struct PyModuleDef typesmodule = { .m_base = PyModuleDef_HEAD_INIT, From 6fcb64e4b7912602b227ba4551bfc9b071ae3c9d Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 23:18:40 +0800 Subject: [PATCH 12/22] add _typesmodule.c.h --- Modules/clinic/_typesmodule.c.h | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 Modules/clinic/_typesmodule.c.h diff --git a/Modules/clinic/_typesmodule.c.h b/Modules/clinic/_typesmodule.c.h new file mode 100644 index 00000000000000..094d43bbdaeff9 --- /dev/null +++ b/Modules/clinic/_typesmodule.c.h @@ -0,0 +1,40 @@ +/*[clinic input] +preserve +[clinic start generated code]*/ + +#include "pycore_modsupport.h" // _PyArg_CheckPositional() + +PyDoc_STRVAR(_types_lookup_special_method__doc__, +"lookup_special_method($module, obj, attr, /)\n" +"--\n" +"\n" +"Lookup special method name `attr` on `obj`.\n" +"\n" +"Lookup method `attr` on `obj` without looking in the instance dictionary.\n" +"Returns `None` if the method is not found."); + +#define _TYPES_LOOKUP_SPECIAL_METHOD_METHODDEF \ + {"lookup_special_method", _PyCFunction_CAST(_types_lookup_special_method), METH_FASTCALL, _types_lookup_special_method__doc__}, + +static PyObject * +_types_lookup_special_method_impl(PyObject *module, PyObject *obj, + PyObject *attr); + +static PyObject * +_types_lookup_special_method(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *obj; + PyObject *attr; + + if (!_PyArg_CheckPositional("lookup_special_method", nargs, 2, 2)) { + goto exit; + } + obj = args[0]; + attr = args[1]; + return_value = _types_lookup_special_method_impl(module, obj, attr); + +exit: + return return_value; +} +/*[clinic end generated code: output=5e1740bceb7577bc input=a9049054013a1b77]*/ From eecfe28206a35cfd65ce6a6088d7f7911c130bea Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 20 Feb 2026 00:31:54 +0800 Subject: [PATCH 13/22] add pure Python fallback --- Lib/test/test_types.py | 27 ++++++++++++------- Lib/types.py | 48 +++++++++++++++++++++++++++++++++ Modules/_typesmodule.c | 33 ++++++++++++++++++++--- Modules/clinic/_typesmodule.c.h | 33 ++++++++++++++++++++--- 4 files changed, 125 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index bc585ce6ebb9b9..237b343599b8ba 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -41,10 +41,10 @@ def clear_typing_caches(): class TypesTests(unittest.TestCase): def test_names(self): - c_only_names = {'CapsuleType', 'LazyImportType', - 'lookup_special_method'} + c_only_names = {'CapsuleType', 'LazyImportType'} ignored = {'new_class', 'resolve_bases', 'prepare_class', - 'get_original_bases', 'DynamicClassAttribute', 'coroutine'} + 'get_original_bases', 'DynamicClassAttribute', 'coroutine', + 'lookup_special_method'} for name in c_types.__all__: if name not in c_only_names | ignored: @@ -727,7 +727,7 @@ def test_frame_locals_proxy_type(self): self.assertIsNotNone(frame) self.assertIsInstance(frame.f_locals, types.FrameLocalsProxyType) - def test_lookup_special_method(self): + def _test_lookup_special_method(self, lookup): class CM1: def __enter__(self): return "__enter__ from class __dict__" @@ -745,29 +745,36 @@ def __enter__(self): return "__enter__ from __slots__" self.__enter__ = __enter__ cm1 = CM1() - meth = types.lookup_special_method(cm1, "__enter__") + meth = lookup(cm1, "__enter__") self.assertIsNotNone(meth) self.assertEqual(meth(cm1), "__enter__ from class __dict__") - meth = types.lookup_special_method(cm1, "__missing__") + meth = lookup(cm1, "__missing__") self.assertIsNone(meth) with self.assertRaises(TypeError): - types.lookup_special_method(cm1, 123) + lookup(cm1, 123) cm2 = CM2() - meth = types.lookup_special_method(cm2, "__enter__") + meth = lookup(cm2, "__enter__") self.assertIsNone(meth) cm3 = CM3() - meth = types.lookup_special_method(cm3, "__enter__") + meth = lookup(cm3, "__enter__") self.assertIsNotNone(meth) self.assertEqual(meth(cm3), "__enter__ from __slots__") - meth = types.lookup_special_method([], "__len__") + meth = lookup([], "__len__") self.assertIsNotNone(meth) self.assertEqual(meth([]), 0) + def test_lookup_special_method(self): + c_lookup = getattr(c_types, "lookup_special_method") + py_lookup = getattr(types, "lookup_special_method") + self._test_lookup_special_method(c_lookup) + self._test_lookup_special_method(py_lookup) + + class UnionTests(unittest.TestCase): def test_or_types_operator(self): diff --git a/Lib/types.py b/Lib/types.py index b4f9a5c5140860..90ad8dd3b59d89 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -81,6 +81,54 @@ def _m(self): pass del sys, _f, _g, _C, _c, _ag, _cell_factory # Not for export + def lookup_special_method(obj, attr, /): + """Lookup special method name `attr` on `obj`. + + Lookup method `attr` on `obj` without looking in the instance + dictionary. For methods defined in class `__dict__` or `__slots__`, it + returns the unbound function (descriptor), not a bound method. The + caller is responsible for passing the object as the first argument when + calling it: + + class A: + def __enter__(self): + pass + + class B: + __slots__ = ("__enter__",) + + def __init__(self): + def __enter__(self): + pass + self.__enter__ = __enter__ + + a = A() + b = B() + enter_a = types.lookup_special_method(a, "__enter__") + enter_b = types.lookup_special_method(b, "__enter__") + + result_a = enter_a(a) + result_b = enter_b(b) + + For other descriptors (property, etc.), it returns the result of the + descriptor's `__get__` method. Returns `None` if the method is not + found. + """ + from inspect import getattr_static, isfunction, ismethoddescriptor + cls = type(obj) + try: + descr = getattr_static(cls, attr) + except AttributeError: + return None + if hasattr(descr, "__get__"): + if isfunction(descr) or ismethoddescriptor(descr): + # do not create bound method to mimic the behavior of + # _PyObject_LookupSpecialMethod + return descr + else: + return descr.__get__(obj, cls) + return descr + # Provide a PEP 3115 compliant mechanism for class creation def new_class(name, bases=(), kwds=None, exec_body=None): diff --git a/Modules/_typesmodule.c b/Modules/_typesmodule.c index 6ec40681cca06b..db670817d6eb7f 100644 --- a/Modules/_typesmodule.c +++ b/Modules/_typesmodule.c @@ -24,14 +24,41 @@ _types.lookup_special_method Lookup special method name `attr` on `obj`. -Lookup method `attr` on `obj` without looking in the instance dictionary. -Returns `None` if the method is not found. +Lookup method `attr` on `obj` without looking in the instance +dictionary. For methods defined in class `__dict__` or `__slots__`, it +returns the unbound function (descriptor), not a bound method. The +caller is responsible for passing the object as the first argument when +calling it: + + class A: + def __enter__(self): + pass + + class B: + __slots__ = ("__enter__",) + + def __init__(self): + def __enter__(self): + pass + self.__enter__ = __enter__ + + a = A() + b = B() + enter_a = types.lookup_special_method(a, "__enter__") + enter_b = types.lookup_special_method(b, "__enter__") + + result_a = enter_a(a) + result_b = enter_b(b) + +For other descriptors (property, etc.), it returns the result of the +descriptor's `__get__` method. Returns `None` if the method is not +found. [clinic start generated code]*/ static PyObject * _types_lookup_special_method_impl(PyObject *module, PyObject *obj, PyObject *attr) -/*[clinic end generated code: output=890e22cc0b8e0d34 input=f26012b0c90b81cd]*/ +/*[clinic end generated code: output=890e22cc0b8e0d34 input=fca9cb0e313a7848]*/ { if (!PyUnicode_Check(attr)) { PyErr_Format(PyExc_TypeError, diff --git a/Modules/clinic/_typesmodule.c.h b/Modules/clinic/_typesmodule.c.h index 094d43bbdaeff9..035db834093f76 100644 --- a/Modules/clinic/_typesmodule.c.h +++ b/Modules/clinic/_typesmodule.c.h @@ -10,8 +10,35 @@ PyDoc_STRVAR(_types_lookup_special_method__doc__, "\n" "Lookup special method name `attr` on `obj`.\n" "\n" -"Lookup method `attr` on `obj` without looking in the instance dictionary.\n" -"Returns `None` if the method is not found."); +"Lookup method `attr` on `obj` without looking in the instance\n" +"dictionary. For methods defined in class `__dict__` or `__slots__`, it\n" +"returns the unbound function (descriptor), not a bound method. The\n" +"caller is responsible for passing the object as the first argument when\n" +"calling it:\n" +"\n" +" class A:\n" +" def __enter__(self):\n" +" pass\n" +"\n" +" class B:\n" +" __slots__ = (\"__enter__\",)\n" +"\n" +" def __init__(self):\n" +" def __enter__(self):\n" +" pass\n" +" self.__enter__ = __enter__\n" +"\n" +" a = A()\n" +" b = B()\n" +" enter_a = types.lookup_special_method(a, \"__enter__\")\n" +" enter_b = types.lookup_special_method(b, \"__enter__\")\n" +"\n" +" result_a = enter_a(a)\n" +" result_b = enter_b(b)\n" +"\n" +"For other descriptors (property, etc.), it returns the result of the\n" +"descriptor\'s `__get__` method. Returns `None` if the method is not\n" +"found."); #define _TYPES_LOOKUP_SPECIAL_METHOD_METHODDEF \ {"lookup_special_method", _PyCFunction_CAST(_types_lookup_special_method), METH_FASTCALL, _types_lookup_special_method__doc__}, @@ -37,4 +64,4 @@ _types_lookup_special_method(PyObject *module, PyObject *const *args, Py_ssize_t exit: return return_value; } -/*[clinic end generated code: output=5e1740bceb7577bc input=a9049054013a1b77]*/ +/*[clinic end generated code: output=11a3b8dd4cb5f673 input=a9049054013a1b77]*/ From e722141ae579f57fd80a31596402e1fd7a8b1668 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 20 Feb 2026 00:46:38 +0800 Subject: [PATCH 14/22] improve code example in the docstring --- Doc/library/types.rst | 36 ++++++++++++++++++++++++++--- Lib/types.py | 38 +++++++++++++++---------------- Modules/_typesmodule.c | 40 ++++++++++++++++----------------- Modules/clinic/_typesmodule.c.h | 40 ++++++++++++++++----------------- 4 files changed, 92 insertions(+), 62 deletions(-) diff --git a/Doc/library/types.rst b/Doc/library/types.rst index 730a2bd9334a69..ef2a8808a97687 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -523,9 +523,39 @@ Additional Utility Classes and Functions .. function:: lookup_special_method(obj, attr, /) - Do a method lookup in the type without looking in the instance dictionary - but still binding it to the instance. Returns ``None`` if the method is not - found. + Lookup special method name ``attr`` on ``obj``. + + Lookup method ``attr`` on ``obj`` without looking in the instance + dictionary. For methods defined in class ``__dict__`` or ``__slots__``, it + returns the unbound function (descriptor), not a bound method. The + caller is responsible for passing the object as the first argument when + calling it: + + .. code-block:: python + + >>> class A: + ... def __enter__(self): + ... return "A.__enter__" + ... + >>> class B: + ... __slots__ = ("__enter__",) + ... def __init__(self): + ... def __enter__(self): + ... return "B.__enter__" + ... self.__enter__ = __enter__ + ... + >>> a = A() + >>> b = B() + >>> enter_a = types.lookup_special_method(a, "__enter__") + >>> enter_b = types.lookup_special_method(b, "__enter__") + >>> enter_a(a) + 'A.__enter__' + >>> enter_b(b) + 'B.__enter__' + + For other descriptors (property, etc.), it returns the result of the + descriptor's ``__get__`` method. Returns ``None`` if the method is not + found. .. versionadded:: next diff --git a/Lib/types.py b/Lib/types.py index 90ad8dd3b59d89..c6152fd3e1cf99 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -90,25 +90,25 @@ def lookup_special_method(obj, attr, /): caller is responsible for passing the object as the first argument when calling it: - class A: - def __enter__(self): - pass - - class B: - __slots__ = ("__enter__",) - - def __init__(self): - def __enter__(self): - pass - self.__enter__ = __enter__ - - a = A() - b = B() - enter_a = types.lookup_special_method(a, "__enter__") - enter_b = types.lookup_special_method(b, "__enter__") - - result_a = enter_a(a) - result_b = enter_b(b) + >>> class A: + ... def __enter__(self): + ... return "A.__enter__" + ... + >>> class B: + ... __slots__ = ("__enter__",) + ... def __init__(self): + ... def __enter__(self): + ... return "B.__enter__" + ... self.__enter__ = __enter__ + ... + >>> a = A() + >>> b = B() + >>> enter_a = types.lookup_special_method(a, "__enter__") + >>> enter_b = types.lookup_special_method(b, "__enter__") + >>> enter_a(a) + 'A.__enter__' + >>> enter_b(b) + 'B.__enter__' For other descriptors (property, etc.), it returns the result of the descriptor's `__get__` method. Returns `None` if the method is not diff --git a/Modules/_typesmodule.c b/Modules/_typesmodule.c index db670817d6eb7f..051b05965548c2 100644 --- a/Modules/_typesmodule.c +++ b/Modules/_typesmodule.c @@ -30,25 +30,25 @@ returns the unbound function (descriptor), not a bound method. The caller is responsible for passing the object as the first argument when calling it: - class A: - def __enter__(self): - pass - - class B: - __slots__ = ("__enter__",) - - def __init__(self): - def __enter__(self): - pass - self.__enter__ = __enter__ - - a = A() - b = B() - enter_a = types.lookup_special_method(a, "__enter__") - enter_b = types.lookup_special_method(b, "__enter__") - - result_a = enter_a(a) - result_b = enter_b(b) +>>> class A: +... def __enter__(self): +... return "A.__enter__" +... +>>> class B: +... __slots__ = ("__enter__",) +... def __init__(self): +... def __enter__(self): +... return "B.__enter__" +... self.__enter__ = __enter__ +... +>>> a = A() +>>> b = B() +>>> enter_a = types.lookup_special_method(a, "__enter__") +>>> enter_b = types.lookup_special_method(b, "__enter__") +>>> enter_a(a) +'A.__enter__' +>>> enter_b(b) +'B.__enter__' For other descriptors (property, etc.), it returns the result of the descriptor's `__get__` method. Returns `None` if the method is not @@ -58,7 +58,7 @@ found. static PyObject * _types_lookup_special_method_impl(PyObject *module, PyObject *obj, PyObject *attr) -/*[clinic end generated code: output=890e22cc0b8e0d34 input=fca9cb0e313a7848]*/ +/*[clinic end generated code: output=890e22cc0b8e0d34 input=e317288370125cd5]*/ { if (!PyUnicode_Check(attr)) { PyErr_Format(PyExc_TypeError, diff --git a/Modules/clinic/_typesmodule.c.h b/Modules/clinic/_typesmodule.c.h index 035db834093f76..fe20e34a65e9d1 100644 --- a/Modules/clinic/_typesmodule.c.h +++ b/Modules/clinic/_typesmodule.c.h @@ -16,25 +16,25 @@ PyDoc_STRVAR(_types_lookup_special_method__doc__, "caller is responsible for passing the object as the first argument when\n" "calling it:\n" "\n" -" class A:\n" -" def __enter__(self):\n" -" pass\n" -"\n" -" class B:\n" -" __slots__ = (\"__enter__\",)\n" -"\n" -" def __init__(self):\n" -" def __enter__(self):\n" -" pass\n" -" self.__enter__ = __enter__\n" -"\n" -" a = A()\n" -" b = B()\n" -" enter_a = types.lookup_special_method(a, \"__enter__\")\n" -" enter_b = types.lookup_special_method(b, \"__enter__\")\n" -"\n" -" result_a = enter_a(a)\n" -" result_b = enter_b(b)\n" +">>> class A:\n" +"... def __enter__(self):\n" +"... return \"A.__enter__\"\n" +"...\n" +">>> class B:\n" +"... __slots__ = (\"__enter__\",)\n" +"... def __init__(self):\n" +"... def __enter__(self):\n" +"... return \"B.__enter__\"\n" +"... self.__enter__ = __enter__\n" +"...\n" +">>> a = A()\n" +">>> b = B()\n" +">>> enter_a = types.lookup_special_method(a, \"__enter__\")\n" +">>> enter_b = types.lookup_special_method(b, \"__enter__\")\n" +">>> enter_a(a)\n" +"\'A.__enter__\'\n" +">>> enter_b(b)\n" +"\'B.__enter__\'\n" "\n" "For other descriptors (property, etc.), it returns the result of the\n" "descriptor\'s `__get__` method. Returns `None` if the method is not\n" @@ -64,4 +64,4 @@ _types_lookup_special_method(PyObject *module, PyObject *const *args, Py_ssize_t exit: return return_value; } -/*[clinic end generated code: output=11a3b8dd4cb5f673 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=664edfaf350f71d0 input=a9049054013a1b77]*/ From f43112ac5db2318a0c72d0b49c4137dde449a538 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 20 Feb 2026 00:54:51 +0800 Subject: [PATCH 15/22] fix py:func reference target not found --- Doc/whatsnew/3.15.rst | 4 ++-- .../Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index afc7460053d2b4..593d2cd657a061 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1064,8 +1064,8 @@ types This represents the type of the :attr:`frame.f_locals` attribute, as described in :pep:`667`. -* Expose ``_PyObject_LookupSpecialMethod()`` as - :func:`types.lookup_special_method(obj, attr, /)`. +* Expose ``_PyObject_LookupSpecialMethod`` as + :func:`types.lookup_special_method`. unicodedata ----------- diff --git a/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst b/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst index a55a5cf86faeec..78e6ef94ac5b34 100644 --- a/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst +++ b/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst @@ -1,2 +1,2 @@ -Expose ``_PyObject_LookupSpecialMethod()`` as -:func:`types.lookup_special_method(obj, attr, /)`. +Expose ``_PyObject_LookupSpecialMethod`` as +:func:`types.lookup_special_method`. From 66504989b86d84fcb2a7a91f54977deaa7e3f168 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 20 Feb 2026 01:06:58 +0800 Subject: [PATCH 16/22] fix typo in test --- Lib/test/test_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 237b343599b8ba..221605fd7bf26f 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -770,7 +770,7 @@ def __enter__(self): def test_lookup_special_method(self): c_lookup = getattr(c_types, "lookup_special_method") - py_lookup = getattr(types, "lookup_special_method") + py_lookup = getattr(py_types, "lookup_special_method") self._test_lookup_special_method(c_lookup) self._test_lookup_special_method(py_lookup) From d3f6bda3982f79c089820cb491f8a856363928b3 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 20 Feb 2026 01:19:53 +0800 Subject: [PATCH 17/22] add attr check in Python fallback --- Lib/types.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/types.py b/Lib/types.py index c6152fd3e1cf99..8cb545991b7c4b 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -116,6 +116,10 @@ def lookup_special_method(obj, attr, /): """ from inspect import getattr_static, isfunction, ismethoddescriptor cls = type(obj) + if not isinstance(attr, str): + raise TypeError( + f"attribute name must be string, not '{type(attr).__name__}'" + ) try: descr = getattr_static(cls, attr) except AttributeError: From da5dc8187090a77ae877327c1e7700ef235530d5 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 20 Feb 2026 17:56:08 +0800 Subject: [PATCH 18/22] ensure Python fallback has same behavior as _PyObject_LookupSpecialMethod() on classmethod, staticmethod, and property --- Lib/test/test_types.py | 26 +++++++++++++++++++++++++- Lib/types.py | 11 ++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 221605fd7bf26f..3ef42b26ff64f0 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -747,12 +747,15 @@ def __enter__(self): cm1 = CM1() meth = lookup(cm1, "__enter__") self.assertIsNotNone(meth) + with self.assertRaisesRegex( + TypeError, "missing 1 required positional argument") as cm: + meth() self.assertEqual(meth(cm1), "__enter__ from class __dict__") meth = lookup(cm1, "__missing__") self.assertIsNone(meth) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, "attribute name must be string"): lookup(cm1, 123) cm2 = CM2() @@ -768,6 +771,27 @@ def __enter__(self): self.assertIsNotNone(meth) self.assertEqual(meth([]), 0) + class Person: + @classmethod + def hi(cls): + return f"hi from {cls.__name__}" + @staticmethod + def hello(): + return "hello from static method" + @property + def name(self): + return "name from property" + p = Person() + meth = lookup(p, "hi") + self.assertIsNotNone(meth) + self.assertEqual(meth(), "hi from Person") + + meth = lookup(p, "hello") + self.assertIsNotNone(meth) + self.assertEqual(meth(), "hello from static method") + + self.assertEqual(lookup(p, "name"), "name from property") + def test_lookup_special_method(self): c_lookup = getattr(c_types, "lookup_special_method") py_lookup = getattr(py_types, "lookup_special_method") diff --git a/Lib/types.py b/Lib/types.py index 8cb545991b7c4b..f382bfd0c402af 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -110,11 +110,11 @@ def lookup_special_method(obj, attr, /): >>> enter_b(b) 'B.__enter__' - For other descriptors (property, etc.), it returns the result of the - descriptor's `__get__` method. Returns `None` if the method is not - found. + For other descriptors (classmethod, staticmethod, property, etc.), it + returns the result of the descriptor's `__get__` method. Returns `None` + if the method is not found. """ - from inspect import getattr_static, isfunction, ismethoddescriptor + from inspect import getattr_static, isfunction cls = type(obj) if not isinstance(attr, str): raise TypeError( @@ -125,7 +125,8 @@ def lookup_special_method(obj, attr, /): except AttributeError: return None if hasattr(descr, "__get__"): - if isfunction(descr) or ismethoddescriptor(descr): + if isfunction(descr) or isinstance(descr,( + MethodDescriptorType, WrapperDescriptorType)): # do not create bound method to mimic the behavior of # _PyObject_LookupSpecialMethod return descr From 97a6eb8e0a466ea96897016d4a40ffdddf462259 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 20 Feb 2026 17:58:19 +0800 Subject: [PATCH 19/22] replace inspect.isfunction with isinstance(descr, FunctionType) --- Lib/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/types.py b/Lib/types.py index f382bfd0c402af..60f4b9a431eb1e 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -114,7 +114,7 @@ def lookup_special_method(obj, attr, /): returns the result of the descriptor's `__get__` method. Returns `None` if the method is not found. """ - from inspect import getattr_static, isfunction + from inspect import getattr_static cls = type(obj) if not isinstance(attr, str): raise TypeError( @@ -125,8 +125,8 @@ def lookup_special_method(obj, attr, /): except AttributeError: return None if hasattr(descr, "__get__"): - if isfunction(descr) or isinstance(descr,( - MethodDescriptorType, WrapperDescriptorType)): + if isinstance(descr, ( + FunctionType, MethodDescriptorType, WrapperDescriptorType)): # do not create bound method to mimic the behavior of # _PyObject_LookupSpecialMethod return descr From 671185e25e69b04c54ca82c0a5e1aaa983d0f5c3 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 22 Feb 2026 17:05:52 +0800 Subject: [PATCH 20/22] 1. expose _PyObject_LookupSpecial instead of _PyObject_LookupSpecialMethod; 2. raise AttributeError instead of returning None; 3. add optional `default` parameter, similar to `getattr`: lookup_special(object, name[, default]) 4. There are different opinions on which module should the function be in. Leave it in `types` until there is consensus. --- Doc/library/types.rst | 42 ++------ Lib/test/test_types.py | 49 ++++------ Lib/types.py | 58 +++-------- ...-02-19-17-46-59.gh-issue-144989.JAuJyG.rst | 3 +- Modules/_typesmodule.c | 98 ++++++------------- Modules/clinic/_typesmodule.c.h | 67 ------------- 6 files changed, 72 insertions(+), 245 deletions(-) delete mode 100644 Modules/clinic/_typesmodule.c.h diff --git a/Doc/library/types.rst b/Doc/library/types.rst index ef2a8808a97687..77c8296c14690e 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -521,41 +521,13 @@ Additional Utility Classes and Functions .. versionadded:: 3.4 -.. function:: lookup_special_method(obj, attr, /) - - Lookup special method name ``attr`` on ``obj``. - - Lookup method ``attr`` on ``obj`` without looking in the instance - dictionary. For methods defined in class ``__dict__`` or ``__slots__``, it - returns the unbound function (descriptor), not a bound method. The - caller is responsible for passing the object as the first argument when - calling it: - - .. code-block:: python - - >>> class A: - ... def __enter__(self): - ... return "A.__enter__" - ... - >>> class B: - ... __slots__ = ("__enter__",) - ... def __init__(self): - ... def __enter__(self): - ... return "B.__enter__" - ... self.__enter__ = __enter__ - ... - >>> a = A() - >>> b = B() - >>> enter_a = types.lookup_special_method(a, "__enter__") - >>> enter_b = types.lookup_special_method(b, "__enter__") - >>> enter_a(a) - 'A.__enter__' - >>> enter_b(b) - 'B.__enter__' - - For other descriptors (property, etc.), it returns the result of the - descriptor's ``__get__`` method. Returns ``None`` if the method is not - found. +.. function:: lookup_special(object, name, /) + lookup_special(object, name, default, /) + + Lookup method name *name* on *object* skipping the instance dictionary. + *name* must be a string. If the named special attribute does not exist, + *default* is returned if provided, otherwise :exc:`AttributeError` is + raised. .. versionadded:: next diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 3ef42b26ff64f0..8f2d84bca78a36 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -44,7 +44,7 @@ def test_names(self): c_only_names = {'CapsuleType', 'LazyImportType'} ignored = {'new_class', 'resolve_bases', 'prepare_class', 'get_original_bases', 'DynamicClassAttribute', 'coroutine', - 'lookup_special_method'} + 'lookup_special'} for name in c_types.__all__: if name not in c_only_names | ignored: @@ -60,7 +60,7 @@ def test_names(self): 'MemberDescriptorType', 'MethodDescriptorType', 'MethodType', 'MethodWrapperType', 'ModuleType', 'NoneType', 'NotImplementedType', 'SimpleNamespace', 'TracebackType', - 'UnionType', 'WrapperDescriptorType', 'lookup_special_method', + 'UnionType', 'WrapperDescriptorType', 'lookup_special', } self.assertEqual(all_names, set(c_types.__all__)) self.assertEqual(all_names - c_only_names, set(py_types.__all__)) @@ -727,7 +727,7 @@ def test_frame_locals_proxy_type(self): self.assertIsNotNone(frame) self.assertIsInstance(frame.f_locals, types.FrameLocalsProxyType) - def _test_lookup_special_method(self, lookup): + def _test_lookup_special(self, lookup): class CM1: def __enter__(self): return "__enter__ from class __dict__" @@ -745,31 +745,24 @@ def __enter__(self): return "__enter__ from __slots__" self.__enter__ = __enter__ cm1 = CM1() - meth = lookup(cm1, "__enter__") - self.assertIsNotNone(meth) - with self.assertRaisesRegex( - TypeError, "missing 1 required positional argument") as cm: - meth() - self.assertEqual(meth(cm1), "__enter__ from class __dict__") - - meth = lookup(cm1, "__missing__") - self.assertIsNone(meth) - with self.assertRaisesRegex(TypeError, "attribute name must be string"): lookup(cm1, 123) + with self.assertRaises(AttributeError): + lookup(cm1, "__missing__") + self.assertEqual(lookup(cm1, "__missing__", "default"), "default") + meth = lookup(cm1, "__enter__") + self.assertEqual(meth(), "__enter__ from class __dict__") cm2 = CM2() - meth = lookup(cm2, "__enter__") - self.assertIsNone(meth) + with self.assertRaises(AttributeError): + lookup(cm2, "__enter__") cm3 = CM3() meth = lookup(cm3, "__enter__") - self.assertIsNotNone(meth) self.assertEqual(meth(cm3), "__enter__ from __slots__") meth = lookup([], "__len__") - self.assertIsNotNone(meth) - self.assertEqual(meth([]), 0) + self.assertEqual(meth(), 0) class Person: @classmethod @@ -782,21 +775,15 @@ def hello(): def name(self): return "name from property" p = Person() - meth = lookup(p, "hi") - self.assertIsNotNone(meth) - self.assertEqual(meth(), "hi from Person") - - meth = lookup(p, "hello") - self.assertIsNotNone(meth) - self.assertEqual(meth(), "hello from static method") - + self.assertEqual(lookup(p, "hi")(), "hi from Person") + self.assertEqual(lookup(p, "hello")(), "hello from static method") self.assertEqual(lookup(p, "name"), "name from property") - def test_lookup_special_method(self): - c_lookup = getattr(c_types, "lookup_special_method") - py_lookup = getattr(py_types, "lookup_special_method") - self._test_lookup_special_method(c_lookup) - self._test_lookup_special_method(py_lookup) + def test_lookup_special(self): + c_lookup = getattr(c_types, "lookup_special") + py_lookup = getattr(py_types, "lookup_special") + self._test_lookup_special(c_lookup) + self._test_lookup_special(py_lookup) class UnionTests(unittest.TestCase): diff --git a/Lib/types.py b/Lib/types.py index 60f4b9a431eb1e..cf79d1b5053e30 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -81,57 +81,27 @@ def _m(self): pass del sys, _f, _g, _C, _c, _ag, _cell_factory # Not for export - def lookup_special_method(obj, attr, /): - """Lookup special method name `attr` on `obj`. - - Lookup method `attr` on `obj` without looking in the instance - dictionary. For methods defined in class `__dict__` or `__slots__`, it - returns the unbound function (descriptor), not a bound method. The - caller is responsible for passing the object as the first argument when - calling it: - - >>> class A: - ... def __enter__(self): - ... return "A.__enter__" - ... - >>> class B: - ... __slots__ = ("__enter__",) - ... def __init__(self): - ... def __enter__(self): - ... return "B.__enter__" - ... self.__enter__ = __enter__ - ... - >>> a = A() - >>> b = B() - >>> enter_a = types.lookup_special_method(a, "__enter__") - >>> enter_b = types.lookup_special_method(b, "__enter__") - >>> enter_a(a) - 'A.__enter__' - >>> enter_b(b) - 'B.__enter__' - - For other descriptors (classmethod, staticmethod, property, etc.), it - returns the result of the descriptor's `__get__` method. Returns `None` - if the method is not found. + def lookup_special(object, name, *args): + """Lookup method name `name` on `object` skipping the instance + dictionary. + + `name` must be a string. If the named special attribute does not exist, + `default` is returned if provided, otherwise AttributeError is raised. """ from inspect import getattr_static - cls = type(obj) - if not isinstance(attr, str): + cls = type(object) + if not isinstance(name, str): raise TypeError( - f"attribute name must be string, not '{type(attr).__name__}'" + f"attribute name must be string, not '{type(name).__name__}'" ) try: - descr = getattr_static(cls, attr) + descr = getattr_static(cls, name) except AttributeError: - return None + if args: + return args[0] + raise if hasattr(descr, "__get__"): - if isinstance(descr, ( - FunctionType, MethodDescriptorType, WrapperDescriptorType)): - # do not create bound method to mimic the behavior of - # _PyObject_LookupSpecialMethod - return descr - else: - return descr.__get__(obj, cls) + return descr.__get__(object, cls) return descr diff --git a/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst b/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst index 78e6ef94ac5b34..6e5cf92a893d46 100644 --- a/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst +++ b/Misc/NEWS.d/next/Library/2026-02-19-17-46-59.gh-issue-144989.JAuJyG.rst @@ -1,2 +1 @@ -Expose ``_PyObject_LookupSpecialMethod`` as -:func:`types.lookup_special_method`. +Expose ``_PyObject_LookupSpecial`` as :func:`types.lookup_special`. diff --git a/Modules/_typesmodule.c b/Modules/_typesmodule.c index 051b05965548c2..e6a62ce989f758 100644 --- a/Modules/_typesmodule.c +++ b/Modules/_typesmodule.c @@ -6,78 +6,44 @@ #include "pycore_namespace.h" // _PyNamespace_Type #include "pycore_object.h" // _PyNone_Type, _PyNotImplemented_Type #include "pycore_unionobject.h" // _PyUnion_Type -#include "pycore_typeobject.h" // _PyObject_LookupSpecialMethod -#include "pycore_stackref.h" // _PyStackRef -#include "clinic/_typesmodule.c.h" +#include "pycore_typeobject.h" // _PyObject_LookupSpecial +#include "pycore_modsupport.h" // _PyArg_CheckPositional -/*[clinic input] -module _types -[clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=530308b1011b659d]*/ - -/*[clinic input] -_types.lookup_special_method - - obj: 'O' - attr: 'O' - / - -Lookup special method name `attr` on `obj`. - -Lookup method `attr` on `obj` without looking in the instance -dictionary. For methods defined in class `__dict__` or `__slots__`, it -returns the unbound function (descriptor), not a bound method. The -caller is responsible for passing the object as the first argument when -calling it: - ->>> class A: -... def __enter__(self): -... return "A.__enter__" -... ->>> class B: -... __slots__ = ("__enter__",) -... def __init__(self): -... def __enter__(self): -... return "B.__enter__" -... self.__enter__ = __enter__ -... ->>> a = A() ->>> b = B() ->>> enter_a = types.lookup_special_method(a, "__enter__") ->>> enter_b = types.lookup_special_method(b, "__enter__") ->>> enter_a(a) -'A.__enter__' ->>> enter_b(b) -'B.__enter__' - -For other descriptors (property, etc.), it returns the result of the -descriptor's `__get__` method. Returns `None` if the method is not -found. -[clinic start generated code]*/ +PyDoc_STRVAR(lookup_special_doc, +"lookup_special(object, name[, default], /)\n\ +\n\ +Lookup method name `name` on `object` skipping the instance dictionary.\n\ +`name` must be a string. If the named special attribute does not\n\ +exist,`default` is returned if provided, otherwise AttributeError is raised."); static PyObject * -_types_lookup_special_method_impl(PyObject *module, PyObject *obj, - PyObject *attr) -/*[clinic end generated code: output=890e22cc0b8e0d34 input=e317288370125cd5]*/ +_types_lookup_special_impl(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { - if (!PyUnicode_Check(attr)) { + PyObject *v, *name, *result; + + if (!_PyArg_CheckPositional("lookup_special", nargs, 2, 3)) + return NULL; + + v = args[0]; + name = args[1]; + if (!PyUnicode_Check(name)) { PyErr_Format(PyExc_TypeError, "attribute name must be string, not '%.200s'", - Py_TYPE(attr)->tp_name); + Py_TYPE(name)->tp_name); return NULL; } - _PyStackRef method_and_self[2]; - method_and_self[0] = PyStackRef_NULL; - method_and_self[1] = PyStackRef_FromPyObjectBorrow(obj); - int result = _PyObject_LookupSpecialMethod(attr, method_and_self); - if (result == -1) { - return NULL; - } - if (result == 0) { - Py_RETURN_NONE; + result = _PyObject_LookupSpecial(v, name); + if (result == NULL) { + if (nargs > 2) { + PyObject *dflt = args[2]; + return Py_NewRef(dflt); + } else { + PyErr_Format(PyExc_AttributeError, + "'%.50s' object has no special attribute '%U'", + Py_TYPE(v)->tp_name, name); + } } - PyObject *method = PyStackRef_AsPyObjectSteal(method_and_self[0]); - return method; + return result; } static int @@ -134,9 +100,9 @@ static struct PyModuleDef_Slot _typesmodule_slots[] = { }; static PyMethodDef _typesmodule_methods[] = { - _TYPES_LOOKUP_SPECIAL_METHOD_METHODDEF - {NULL, NULL, 0, NULL} -}; + {"lookup_special", _PyCFunction_CAST(_types_lookup_special_impl), + METH_FASTCALL, lookup_special_doc}, + {NULL, NULL, 0, NULL}}; static struct PyModuleDef typesmodule = { .m_base = PyModuleDef_HEAD_INIT, diff --git a/Modules/clinic/_typesmodule.c.h b/Modules/clinic/_typesmodule.c.h deleted file mode 100644 index fe20e34a65e9d1..00000000000000 --- a/Modules/clinic/_typesmodule.c.h +++ /dev/null @@ -1,67 +0,0 @@ -/*[clinic input] -preserve -[clinic start generated code]*/ - -#include "pycore_modsupport.h" // _PyArg_CheckPositional() - -PyDoc_STRVAR(_types_lookup_special_method__doc__, -"lookup_special_method($module, obj, attr, /)\n" -"--\n" -"\n" -"Lookup special method name `attr` on `obj`.\n" -"\n" -"Lookup method `attr` on `obj` without looking in the instance\n" -"dictionary. For methods defined in class `__dict__` or `__slots__`, it\n" -"returns the unbound function (descriptor), not a bound method. The\n" -"caller is responsible for passing the object as the first argument when\n" -"calling it:\n" -"\n" -">>> class A:\n" -"... def __enter__(self):\n" -"... return \"A.__enter__\"\n" -"...\n" -">>> class B:\n" -"... __slots__ = (\"__enter__\",)\n" -"... def __init__(self):\n" -"... def __enter__(self):\n" -"... return \"B.__enter__\"\n" -"... self.__enter__ = __enter__\n" -"...\n" -">>> a = A()\n" -">>> b = B()\n" -">>> enter_a = types.lookup_special_method(a, \"__enter__\")\n" -">>> enter_b = types.lookup_special_method(b, \"__enter__\")\n" -">>> enter_a(a)\n" -"\'A.__enter__\'\n" -">>> enter_b(b)\n" -"\'B.__enter__\'\n" -"\n" -"For other descriptors (property, etc.), it returns the result of the\n" -"descriptor\'s `__get__` method. Returns `None` if the method is not\n" -"found."); - -#define _TYPES_LOOKUP_SPECIAL_METHOD_METHODDEF \ - {"lookup_special_method", _PyCFunction_CAST(_types_lookup_special_method), METH_FASTCALL, _types_lookup_special_method__doc__}, - -static PyObject * -_types_lookup_special_method_impl(PyObject *module, PyObject *obj, - PyObject *attr); - -static PyObject * -_types_lookup_special_method(PyObject *module, PyObject *const *args, Py_ssize_t nargs) -{ - PyObject *return_value = NULL; - PyObject *obj; - PyObject *attr; - - if (!_PyArg_CheckPositional("lookup_special_method", nargs, 2, 2)) { - goto exit; - } - obj = args[0]; - attr = args[1]; - return_value = _types_lookup_special_method_impl(module, obj, attr); - -exit: - return return_value; -} -/*[clinic end generated code: output=664edfaf350f71d0 input=a9049054013a1b77]*/ From 98735f66ca106e487a9be587cd094e20f20d570b Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 22 Feb 2026 20:30:52 +0800 Subject: [PATCH 21/22] fix signature error in test_inspect.py This is temporary as which module should the function be put is not determined yet in the discourse discussion. --- Lib/test/test_inspect/test_inspect.py | 6 +++++- Modules/_typesmodule.c | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index e4a3a7d9add2c2..4d19ac1d2f3adc 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6154,6 +6154,10 @@ def test_builtins_have_signatures(self): methods_no_signature, methods_unsupported_signature) def test_types_module_has_signatures(self): + no_signature = set() + # These need PEP 457 groups + needs_groups = {'lookup_special'} + no_signature |= needs_groups unsupported_signature = {'CellType'} methods_no_signature = { 'AsyncGeneratorType': {'athrow'}, @@ -6161,7 +6165,7 @@ def test_types_module_has_signatures(self): 'GeneratorType': {'throw'}, 'FrameLocalsProxyType': {'setdefault', 'pop', 'get'}, } - self._test_module_has_signatures(types, + self._test_module_has_signatures(types, no_signature, unsupported_signature=unsupported_signature, methods_no_signature=methods_no_signature) diff --git a/Modules/_typesmodule.c b/Modules/_typesmodule.c index e6a62ce989f758..0a8a9d6f52f52b 100644 --- a/Modules/_typesmodule.c +++ b/Modules/_typesmodule.c @@ -14,7 +14,7 @@ PyDoc_STRVAR(lookup_special_doc, \n\ Lookup method name `name` on `object` skipping the instance dictionary.\n\ `name` must be a string. If the named special attribute does not\n\ -exist,`default` is returned if provided, otherwise AttributeError is raised."); +exist,`default` is returned if provided, otherwise `AttributeError` is raised."); static PyObject * _types_lookup_special_impl(PyObject *self, PyObject *const *args, Py_ssize_t nargs) From f0f26e4c5bab02fbb7ea1da51db3b89aaf05ee2a Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 22 Feb 2026 20:40:05 +0800 Subject: [PATCH 22/22] fix whatsnew This is temporary as which module should the function be put is not determined yet in the discourse discussion. --- Doc/whatsnew/3.15.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 593d2cd657a061..a246c5be5ede75 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1064,8 +1064,7 @@ types This represents the type of the :attr:`frame.f_locals` attribute, as described in :pep:`667`. -* Expose ``_PyObject_LookupSpecialMethod`` as - :func:`types.lookup_special_method`. +* Expose ``_PyObject_LookupSpecial`` as :func:`types.lookup_special`. unicodedata -----------