https://github.com/python/cpython/commit/ffa68529b40210d4c55ad3a4114e48007ae0e55a
commit: ffa68529b40210d4c55ad3a4114e48007ae0e55a
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-02-05T11:32:56+01:00
summary:

gh-144330: Initialize classmethod and staticmethod in new (#144469)

Move classmethod and staticmethod initialization from __init__() to
__new__().

PyClassMethod_New() and PyStaticMethod_New() now copy attributes of
the wrapped functions: __module__, __name__, __qualname__ and
__doc__.

Change static type initialization: initialize PyStaticMethod_Type and
PyCFunction_Type earlier.

Remove test_refleaks_in_classmethod___init__() and
test_refleaks_in_staticmethod___init__() tests from test_descr since
classmethod and staticmethod have no __init__() method anymore.

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-02-04-11-19-45.gh-issue-144330.kOowSb.rst
M Lib/test/test_descr.py
M Objects/funcobject.c
M Objects/object.c

diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 0dc61ca7fb0da3..d6e3719479a214 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -1727,6 +1727,18 @@ def annotated(cls) -> int: pass
                     del method.__annotate__
                     self.assertIs(method.__annotate__, original_annotate)
 
+    def test_classmethod_without_dict_access(self):
+        class Spam:
+            @classmethod
+            def method(cls, x, y):
+                pass
+
+        obj = Spam.__dict__['method']
+        self.assertIsInstance(obj, classmethod)
+        self.assertEqual(obj.__annotations__, {})
+        self.assertEqual(obj.__name__, 'method')
+        self.assertEqual(obj.__module__, __name__)
+
     def test_staticmethod_annotations_without_dict_access(self):
         # gh-125017: this used to crash
         class Spam:
@@ -1737,15 +1749,8 @@ def __new__(cls, x, y):
         obj = Spam.__dict__['__new__']
         self.assertIsInstance(obj, staticmethod)
         self.assertEqual(obj.__annotations__, {})
-
-    @support.refcount_test
-    def test_refleaks_in_classmethod___init__(self):
-        gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
-        cm = classmethod(None)
-        refs_before = gettotalrefcount()
-        for i in range(100):
-            cm.__init__(None)
-        self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
+        self.assertEqual(obj.__name__, '__new__')
+        self.assertEqual(obj.__module__, __name__)
 
     @support.impl_detail("the module 'xxsubtype' is internal")
     @unittest.skipIf(xxsubtype is None, "requires xxsubtype module")
@@ -1822,15 +1827,6 @@ class D(C):
         del sm.x
         self.assertNotHasAttr(sm, "x")
 
-    @support.refcount_test
-    def test_refleaks_in_staticmethod___init__(self):
-        gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
-        sm = staticmethod(None)
-        refs_before = gettotalrefcount()
-        for i in range(100):
-            sm.__init__(None)
-        self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
-
     @support.impl_detail("the module 'xxsubtype' is internal")
     @unittest.skipIf(xxsubtype is None, "requires xxsubtype module")
     def test_staticmethods_in_c(self):
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-04-11-19-45.gh-issue-144330.kOowSb.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-04-11-19-45.gh-issue-144330.kOowSb.rst
new file mode 100644
index 00000000000000..b3c61e162f4ffb
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-04-11-19-45.gh-issue-144330.kOowSb.rst
@@ -0,0 +1,2 @@
+Move ``classmethod`` and ``staticmethod`` initialization from ``__init__()``
+to ``__new__()``. Patch by Victor Stinner.
diff --git a/Objects/funcobject.c b/Objects/funcobject.c
index b659ac8023373b..2bf21fa045e3f1 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -1466,33 +1466,59 @@ static PyObject *
 cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
 {
     classmethod *cm = (classmethod *)self;
-
-    if (cm->cm_callable == NULL) {
-        PyErr_SetString(PyExc_RuntimeError,
-                        "uninitialized classmethod object");
-        return NULL;
-    }
     if (type == NULL)
         type = (PyObject *)(Py_TYPE(obj));
     return PyMethod_New(cm->cm_callable, type);
 }
 
 static int
-cm_init(PyObject *self, PyObject *args, PyObject *kwds)
+cm_set_callable(classmethod *cm, PyObject *callable)
 {
-    classmethod *cm = (classmethod *)self;
-    PyObject *callable;
+    assert(callable != NULL);
+    if (cm->cm_callable == callable) {
+        // cm_init() sets the same callable than cm_new()
+        return 0;
+    }
 
-    if (!_PyArg_NoKeywords("classmethod", kwds))
-        return -1;
-    if (!PyArg_UnpackTuple(args, "classmethod", 1, 1, &callable))
-        return -1;
     Py_XSETREF(cm->cm_callable, Py_NewRef(callable));
+    return functools_wraps((PyObject *)cm, cm->cm_callable);
+}
+
+static PyObject *
+cm_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+    if (!_PyArg_NoKeywords("classmethod", kwds)) {
+        return NULL;
+    }
+    PyObject *callable;  // borrowed ref
+    if (!PyArg_UnpackTuple(args, "classmethod", 1, 1, &callable)) {
+        return NULL;
+    }
 
-    if (functools_wraps((PyObject *)cm, cm->cm_callable) < 0) {
+    classmethod *cm = (classmethod *)PyType_GenericAlloc(type, 0);
+    if (cm == NULL) {
+        return NULL;
+    }
+    if (cm_set_callable(cm, callable) < 0) {
+        Py_DECREF(cm);
+        return NULL;
+    }
+    return (PyObject *)cm;
+}
+
+static int
+cm_init(PyObject *self, PyObject *args, PyObject *kwds)
+{
+    if (!_PyArg_NoKeywords("classmethod", kwds)) {
         return -1;
     }
-    return 0;
+    PyObject *callable;  // borrowed ref
+    if (!PyArg_UnpackTuple(args, "classmethod", 1, 1, &callable)) {
+        return -1;
+    }
+
+    classmethod *cm = (classmethod *)self;
+    return cm_set_callable(cm, callable);
 }
 
 static PyMemberDef cm_memberlist[] = {
@@ -1623,7 +1649,7 @@ PyTypeObject PyClassMethod_Type = {
     offsetof(classmethod, cm_dict),             /* tp_dictoffset */
     cm_init,                                    /* tp_init */
     PyType_GenericAlloc,                        /* tp_alloc */
-    PyType_GenericNew,                          /* tp_new */
+    cm_new,                                     /* tp_new */
     PyObject_GC_Del,                            /* tp_free */
 };
 
@@ -1632,8 +1658,12 @@ PyClassMethod_New(PyObject *callable)
 {
     classmethod *cm = (classmethod *)
         PyType_GenericAlloc(&PyClassMethod_Type, 0);
-    if (cm != NULL) {
-        cm->cm_callable = Py_NewRef(callable);
+    if (cm == NULL) {
+        return NULL;
+    }
+    if (cm_set_callable(cm, callable) < 0) {
+        Py_DECREF(cm);
+        return NULL;
     }
     return (PyObject *)cm;
 }
@@ -1699,31 +1729,57 @@ static PyObject *
 sm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
 {
     staticmethod *sm = (staticmethod *)self;
+    return Py_NewRef(sm->sm_callable);
+}
 
-    if (sm->sm_callable == NULL) {
-        PyErr_SetString(PyExc_RuntimeError,
-                        "uninitialized staticmethod object");
+static int
+sm_set_callable(staticmethod *sm, PyObject *callable)
+{
+    assert(callable != NULL);
+    if (sm->sm_callable == callable) {
+        // sm_init() sets the same callable than sm_new()
+        return 0;
+    }
+
+    Py_XSETREF(sm->sm_callable, Py_NewRef(callable));
+    return functools_wraps((PyObject *)sm, sm->sm_callable);
+}
+
+static PyObject *
+sm_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+    if (!_PyArg_NoKeywords("staticmethod", kwds)) {
         return NULL;
     }
-    return Py_NewRef(sm->sm_callable);
+    PyObject *callable;  // borrowed ref
+    if (!PyArg_UnpackTuple(args, "staticmethod", 1, 1, &callable)) {
+        return NULL;
+    }
+
+    staticmethod *sm = (staticmethod *)PyType_GenericAlloc(type, 0);
+    if (sm == NULL) {
+        return NULL;
+    }
+    if (sm_set_callable(sm, callable) < 0) {
+        Py_DECREF(sm);
+        return NULL;
+    }
+    return (PyObject *)sm;
 }
 
 static int
 sm_init(PyObject *self, PyObject *args, PyObject *kwds)
 {
-    staticmethod *sm = (staticmethod *)self;
-    PyObject *callable;
-
-    if (!_PyArg_NoKeywords("staticmethod", kwds))
+    if (!_PyArg_NoKeywords("staticmethod", kwds)) {
         return -1;
-    if (!PyArg_UnpackTuple(args, "staticmethod", 1, 1, &callable))
-        return -1;
-    Py_XSETREF(sm->sm_callable, Py_NewRef(callable));
-
-    if (functools_wraps((PyObject *)sm, sm->sm_callable) < 0) {
+    }
+    PyObject *callable;  // borrowed ref
+    if (!PyArg_UnpackTuple(args, "staticmethod", 1, 1, &callable)) {
         return -1;
     }
-    return 0;
+
+    staticmethod *sm = (staticmethod *)self;
+    return sm_set_callable(sm, callable);
 }
 
 static PyObject*
@@ -1858,7 +1914,7 @@ PyTypeObject PyStaticMethod_Type = {
     offsetof(staticmethod, sm_dict),            /* tp_dictoffset */
     sm_init,                                    /* tp_init */
     PyType_GenericAlloc,                        /* tp_alloc */
-    PyType_GenericNew,                          /* tp_new */
+    sm_new,                                     /* tp_new */
     PyObject_GC_Del,                            /* tp_free */
 };
 
@@ -1867,8 +1923,12 @@ PyStaticMethod_New(PyObject *callable)
 {
     staticmethod *sm = (staticmethod *)
         PyType_GenericAlloc(&PyStaticMethod_Type, 0);
-    if (sm != NULL) {
-        sm->sm_callable = Py_NewRef(callable);
+    if (sm == NULL) {
+        return NULL;
+    }
+    if (sm_set_callable(sm, callable) < 0) {
+        Py_DECREF(sm);
+        return NULL;
     }
     return (PyObject *)sm;
 }
diff --git a/Objects/object.c b/Objects/object.c
index 649b109d5cb0bc..38717def24239f 100644
--- a/Objects/object.c
+++ b/Objects/object.c
@@ -2446,13 +2446,17 @@ static PyTypeObject* static_types[] = {
     &PyBaseObject_Type,
     &PyType_Type,
 
+    // PyStaticMethod_Type and PyCFunction_Type are used by PyType_Ready()
+    // on other types and so must be initialized first.
+    &PyStaticMethod_Type,
+    &PyCFunction_Type,
+
     // Static types with base=&PyBaseObject_Type
     &PyAsyncGen_Type,
     &PyByteArrayIter_Type,
     &PyByteArray_Type,
     &PyBytesIter_Type,
     &PyBytes_Type,
-    &PyCFunction_Type,
     &PyCallIter_Type,
     &PyCapsule_Type,
     &PyCell_Type,
@@ -2509,7 +2513,6 @@ static PyTypeObject* static_types[] = {
     &PySetIter_Type,
     &PySet_Type,
     &PySlice_Type,
-    &PyStaticMethod_Type,
     &PyStdPrinter_Type,
     &PySuper_Type,
     &PyTraceBack_Type,

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to