https://github.com/python/cpython/commit/0babcdd54775bd1cc6bc745767c13d5baa3aedcc
commit: 0babcdd54775bd1cc6bc745767c13d5baa3aedcc
branch: main
author: Suren Nihalani <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2026-10-12T11:23:35-04:00
summary:

gh-152315: Add a suggestion for missing `self` parameters (GH-152326)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-20-37-22.gh-issue-152315.iVS7u5.rst
M Doc/whatsnew/3.16.rst
M Lib/test/test_call.py
M Python/ceval.c

diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 759651d61359812..eb5bbba5bc41c1e 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -98,6 +98,11 @@ Other language changes
   they reference) alive indefinitely.
   (Contributed by Ɓukasz Langa in :gh:`102960`.)
 
+* :exc:`TypeError` messages for some instance methods called with too many
+  positional arguments now suggest checking whether the method definition is
+  missing the ``self`` parameter. (Contributed by Suren Nihalani in
+  :gh:`152315`.)
+
 
 New modules
 ===========
diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py
index 76f1c351e159086..7115359156eb696 100644
--- a/Lib/test/test_call.py
+++ b/Lib/test/test_call.py
@@ -947,7 +947,17 @@ def f(self, *args, **kwargs):
         self.assertEqual(kwargs_captured, [{"baz": "bar"}])
 
 class A:
-    def method_two_args(self, x, y):
+    def method_two_args(arg1=None, arg2=None):
+        pass
+
+    def method_one_arg(arg1):
+        pass
+
+    def method_zero_arg():
+        pass
+
+    @classmethod
+    def classmethod_one_arg(arg1):
         pass
 
     @staticmethod
@@ -955,42 +965,152 @@ def static_no_args():
         pass
 
     @staticmethod
-    def positional_only(arg, /):
+    def static_positional_only_one_arg(arg1, /):
+        pass
+
+    @staticmethod
+    def static_one_arg(arg1):
+        pass
+
+    def method_one_arg_named_self(self):
+        pass
+
+    def method_one_arg_named_cls(cls):
+        pass
+
+    def method_two_arg_second_param_self(arg1, self):
+        pass
+
+
+class AMeta(type):
+
+    def method_one_arg(arg1):
+        pass
+
+    def method_two_arg(arg1, arg2):
+        pass
+
+    @classmethod
+    def classmethod_one_arg(arg1):
         pass
 
+    @staticmethod
+    def static_one_arg(arg1):
+        pass
+
+class AClassWithMetaclass(metaclass=AMeta):
+    pass
+
 @cpython_only
-class TestErrorMessagesUseQualifiedName(unittest.TestCase):
+class TestIncorrectNumberOfPositionalArgs(unittest.TestCase):
+    def test_too_many_positional_with_defaults_suggests_missing_self(self):
+        msg = r"\.method_two_args\(\) takes from 0 to 2 positional arguments 
but 3 were given\. Did you forget the 'self' parameter in the function 
definition\?"
+        with self.assertRaisesRegex(TypeError, msg):
+            A().method_two_args("woof", "loud")
 
-    @contextlib.contextmanager
-    def check_raises_type_error(self, message):
-        with self.assertRaises(TypeError) as cm:
-            yield
-        self.assertEqual(str(cm.exception), message)
+    def test_too_many_positional_but_missing_self_no_args(self):
+        msg = r"takes 0 positional arguments but 1 was given\. Did you forget 
the 'self' parameter in the function definition\?"
+        with self.assertRaisesRegex(TypeError, msg):
+            A().method_zero_arg()
 
-    def test_missing_arguments(self):
-        msg = "A.method_two_args() missing 1 required positional argument: 'y'"
-        with self.check_raises_type_error(msg):
-            A().method_two_args("x")
+    def test_too_many_positional_but_missing_self(self):
+        msg = r"\.method_one_arg\(\) takes 1 positional argument but 2 were 
given\. Did you forget the 'self' parameter in the function definition\?"
+        with self.assertRaisesRegex(TypeError, msg):
+            A().method_one_arg("quiet")
 
     def test_too_many_positional(self):
-        msg = "A.static_no_args() takes 0 positional arguments but 1 was given"
-        with self.check_raises_type_error(msg):
+        msg = r"takes 0 positional arguments but 1 was given"
+        with self.assertRaisesRegex(TypeError, msg):
             A.static_no_args("oops it's an arg")
 
     def test_positional_only_passed_as_keyword(self):
-        msg = "A.positional_only() got some positional-only arguments passed 
as keyword arguments: 'arg'"
-        with self.check_raises_type_error(msg):
-            A.positional_only(arg="x")
+        msg = r"\.static_positional_only_one_arg\(\) got some positional-only 
arguments passed as keyword arguments: 'arg1'"
+        with self.assertRaisesRegex(TypeError, msg):
+            A.static_positional_only_one_arg(arg1="ball")
 
     def test_unexpected_keyword(self):
-        msg = "A.method_two_args() got an unexpected keyword argument 'bad'"
-        with self.check_raises_type_error(msg):
+        msg = r"method_two_args\(\) got an unexpected keyword argument 'bad'"
+        with self.assertRaisesRegex(TypeError, msg):
             A().method_two_args(bad="x")
 
     def test_multiple_values(self):
-        msg = "A.method_two_args() got multiple values for argument 'x'"
-        with self.check_raises_type_error(msg):
-            A().method_two_args("x", "y", x="oops")
+        msg = r"\.method_two_args\(\) got multiple values for argument 'arg1'"
+        with self.assertRaisesRegex(TypeError, msg):
+            A().method_two_args("quiet", "low", arg1="oops")
+
+    def test_unbound_method_with_self_keeps_missing_argument_error(self):
+        msg = r"\.method_one_arg\(\) missing 1 required positional argument: 
'arg1'"
+        with self.assertRaisesRegex(TypeError, msg):
+            A.method_one_arg()
+
+    def test_classmethod_missing_cls_does_not_suggest_missing_self(self):
+        msg = r"\.classmethod_one_arg\(\) takes 1 positional argument but 2 
were given"
+        with self.assertRaisesRegex(TypeError, msg) as cm:
+            A.classmethod_one_arg("arg1")
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def 
test_classmethod_missing_cls_via_instance_does_not_suggest_missing_self(self):
+        msg = r"\.classmethod_one_arg\(\) takes 1 positional argument but 2 
were given"
+        with self.assertRaisesRegex(TypeError, msg) as cm:
+            A().classmethod_one_arg("poodle")
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def test_staticmethod_too_many_args_does_not_suggest_missing_self(self):
+        msg = r"\.static_one_arg\(\) takes 1 positional argument but 2 were 
given"
+        with self.assertRaisesRegex(TypeError, msg) as cm:
+            A.static_one_arg(1, 2)
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def 
test_staticmethod_too_many_args_via_instance_does_not_suggest_missing_self(self):
+        msg = r"\.static_one_arg\(\) takes 1 positional argument but 2 were 
given"
+        with self.assertRaisesRegex(TypeError, msg) as cm:
+            A().static_one_arg(1, 2)
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def test_metaclass_missing_receiver_does_not_suggest_missing_self(self):
+        msg = r"AMeta\.method_one_arg\(\) takes 1 positional argument but 2 
were given"
+        with self.assertRaisesRegex(TypeError, msg) as cm:
+            AClassWithMetaclass.method_one_arg("standard")
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def 
test_metaclass_method_too_many_args_does_not_suggest_missing_self(self):
+        msg = r"AMeta\.method_two_arg\(\) takes 2 positional arguments but 3 
were given"
+        with self.assertRaisesRegex(TypeError, msg) as cm:
+            AClassWithMetaclass.method_two_arg("trail", "river")
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def test_metaclass_classmethod_does_not_suggest_missing_self(self):
+        msg = r"AMeta\.classmethod_one_arg\(\) takes 1 positional argument but 
2 were given"
+        with self.assertRaisesRegex(TypeError, msg) as cm:
+            AClassWithMetaclass.classmethod_one_arg("standard")
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def test_metaclass_staticmethod_does_not_suggest_missing_self(self):
+        msg = r"AMeta\.static_one_arg\(\) takes 1 positional argument but 2 
were given"
+        with self.assertRaisesRegex(TypeError, msg) as cm:
+            AClassWithMetaclass.static_one_arg("show", "working")
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def test_more_than_two_surplus_args_does_not_suggest(self):
+        with self.assertRaises(TypeError) as cm:
+            A().method_one_arg("arg1", "arg2", "arg3")
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def test_when_arg_named_self_does_not_suggest(self):
+        with self.assertRaises(TypeError) as cm:
+            A().method_one_arg_named_self("arg1")
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def test_when_arg_named_cls_does_not_suggest(self):
+        with self.assertRaises(TypeError) as cm:
+            A().method_one_arg_named_cls("arg1")
+        self.assertNotIn("Did you forget", str(cm.exception))
+
+    def test_when_second_param_is_self(self):
+        with self.assertRaises(TypeError) as cm:
+            A().method_two_arg_second_param_self()
+        self.assertNotIn("Did you forget", str(cm.exception))
+
 
 @cpython_only
 class TestErrorMessagesSuggestions(unittest.TestCase):
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-20-37-22.gh-issue-152315.iVS7u5.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-20-37-22.gh-issue-152315.iVS7u5.rst
new file mode 100644
index 000000000000000..2f5849afbddb8ad
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-20-37-22.gh-issue-152315.iVS7u5.rst
@@ -0,0 +1,3 @@
+:exc:`TypeError` messages for some instance methods called with too many
+positional arguments now suggest checking whether the method definition is
+missing the ``self`` parameter.
diff --git a/Python/ceval.c b/Python/ceval.c
index b90de789accd44f..acf012c44a53050 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -1607,12 +1607,14 @@ missing_arguments(PyThreadState *tstate, PyCodeObject 
*co,
 static void
 too_many_positional(PyThreadState *tstate, PyCodeObject *co,
                     Py_ssize_t given, PyObject *defaults,
-                    _PyStackRef *localsplus, PyObject *qualname)
+                    _PyStackRef *localsplus, PyObject *qualname,
+                    int should_suggest_missing_self)
 {
     int plural;
     Py_ssize_t kwonly_given = 0;
     Py_ssize_t i;
     PyObject *sig, *kwonly_sig;
+    const char *self_hint = "";
     Py_ssize_t co_argcount = co->co_argcount;
 
     assert((co->co_flags & CO_VARARGS) == 0);
@@ -1650,18 +1652,55 @@ too_many_positional(PyThreadState *tstate, PyCodeObject 
*co,
         kwonly_sig = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
         assert(kwonly_sig != NULL);
     }
+    if (should_suggest_missing_self) {
+        self_hint = ". Did you forget the 'self' parameter "
+                    "in the function definition?";
+    }
     _PyErr_Format(tstate, PyExc_TypeError,
-                  "%U() takes %U positional argument%s but %zd%U %s given",
+                  "%U() takes %U positional argument%s but %zd%U %s given%s",
                   qualname,
                   sig,
                   plural ? "s" : "",
                   given,
                   kwonly_sig,
-                  given == 1 && !kwonly_given ? "was" : "were");
+                  given == 1 && !kwonly_given ? "was" : "were",
+                  self_hint
+                );
     Py_DECREF(sig);
     Py_DECREF(kwonly_sig);
 }
 
+static int
+suggest_missing_self(PyFunctionObject *func, PyCodeObject *co,
+                     PyObject *first_argument, Py_ssize_t argcount)
+{
+    /* Missing self shows up as exactly one extra positional argument. */
+    if ((co->co_argcount + 1) != argcount) {
+        return 0;
+    }
+
+    if (first_argument == NULL || PyType_Check(first_argument)) {
+        /* When first arg is NULL, it's not really about self.
+           If it's a type object, then it's a classmethod. */
+        return 0;
+    }
+
+    if (co->co_argcount > 0) {
+        PyObject *first_parameter_name = 
PyTuple_GET_ITEM(co->co_localsplusnames, 0);
+        /* If the receiver parameter is already declared, another hint would 
be misleading. */
+        if (PyUnicode_CompareWithASCIIString(first_parameter_name, "self") == 
0 ||
+            PyUnicode_CompareWithASCIIString(first_parameter_name, "cls") == 0)
+        {
+            return 0;
+        }
+    }
+    /* If the current function matches on the type, it's likely worth adding 
the hint. */
+    PyTypeObject *self_cls = Py_TYPE(first_argument);
+    PyFunctionObject *possibly_current_function =
+        (PyFunctionObject *)_PyType_Lookup(self_cls, co->co_name);
+    return possibly_current_function == func;
+}
+
 static int
 positional_only_passed_as_keyword(PyThreadState *tstate, PyCodeObject *co,
                                   Py_ssize_t kwcount, PyObject* kwnames,
@@ -1736,6 +1775,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject 
*func,
     /* Create a dictionary for keyword parameters (**kwags) */
     PyObject *kwdict;
     Py_ssize_t i;
+    PyObject *first_argument = NULL;
     if (co->co_flags & CO_VARKEYWORDS) {
         kwdict = PyDict_New();
         if (kwdict == NULL) {
@@ -1752,6 +1792,10 @@ initialize_locals(PyThreadState *tstate, 
PyFunctionObject *func,
         kwdict = NULL;
     }
 
+    if (argcount > co->co_argcount && !(co->co_flags & CO_VARARGS)) {
+        first_argument = PyStackRef_AsPyObjectNew(args[0]);
+    }
+
     /* Copy all positional arguments into local variables */
     Py_ssize_t j, n;
     if (argcount > co->co_argcount) {
@@ -1896,8 +1940,9 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject 
*func,
 
     /* Check the number of positional arguments */
     if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) {
+        int missing_self_hint = suggest_missing_self(func, co, first_argument, 
argcount);
         too_many_positional(tstate, co, argcount, func->func_defaults, 
localsplus,
-                            func->func_qualname);
+                            func->func_qualname, missing_self_hint);
         goto fail_post_args;
     }
 
@@ -1956,6 +2001,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject 
*func,
             goto fail_post_args;
         }
     }
+    Py_XDECREF(first_argument);
     return 0;
 
 fail_pre_positional:
@@ -1972,6 +2018,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject 
*func,
     }
     /* fall through */
 fail_post_args:
+    Py_XDECREF(first_argument);
     return -1;
 }
 

_______________________________________________
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