From 1a237dae0162ffc93b0f785bd811dff49201dada Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 19 Feb 2026 15:26:15 +0800 Subject: [PATCH 01/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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