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]