diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index f42526aee194174..c415f6f07c0c974 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -910,15 +910,36 @@ def static_no_args(): def positional_only(arg, /): pass + def method_with_self(self, arg, kwarg=1): + pass + + def missing_self(another_arg): + pass + + def missing_self_no_args(): + pass + @cpython_only class TestErrorMessagesUseQualifiedName(unittest.TestCase): - @contextlib.contextmanager def check_raises_type_error(self, message): with self.assertRaises(TypeError) as cm: yield self.assertEqual(str(cm.exception), message) + def test_happy_path(self): + self.assertIs(None, A().method_with_self(1, kwarg=2)) + + def test_too_many_positional_but_missing_self(self): + msg = "A.missing_self() takes 1 positional argument but 2 were given. Did you forget the 'self' parameter in the function definition?" + with self.check_raises_type_error(msg): + A().missing_self("another_arg") + + def test_too_many_positional_but_missing_self_no_args(self): + msg = "A.missing_self_no_args() takes 0 positional arguments but 1 was given. Did you forget the 'self' parameter in the function definition?" + with self.check_raises_type_error(msg): + A().missing_self_no_args() + def test_missing_arguments(self): msg = "A.method_two_args() missing 1 required positional argument: 'y'" with self.check_raises_type_error(msg): 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..eb002b56b0df637 --- /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 @@ +If number of arguments differs by one for bound methods and the number of +positional arg differs, we we add an additional hint in the TypeError: "Did +you forget the 'self' parameter in the function definition?" diff --git a/Python/ceval.c b/Python/ceval.c index fbea1f67a36f442..894bfe9fb4c988b 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1574,12 +1574,13 @@ 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; + PyObject *sig, *kwonly_sig, *self_hint = Py_GetConstant(Py_CONSTANT_EMPTY_STR); Py_ssize_t co_argcount = co->co_argcount; assert((co->co_flags & CO_VARARGS) == 0); @@ -1617,18 +1618,40 @@ 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 = PyUnicode_FromString( + ". Did you forget the 'self' parameter in the function definition?"); + if (self_hint == NULL) { + self_hint = Py_GetConstant(Py_CONSTANT_EMPTY_STR); + } + } _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%U", qualname, sig, plural ? "s" : "", given, kwonly_sig, - given == 1 && !kwonly_given ? "was" : "were"); + given == 1 && !kwonly_given ? "was" : "were", + self_hint + ); + Py_DECREF(self_hint); Py_DECREF(sig); Py_DECREF(kwonly_sig); } +static int +suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, _PyStackRef const *args, Py_ssize_t argcount) +{ + if ((co->co_argcount + 1) != argcount || argcount == 0) { + return 0; + } + PyObject *first_argument = PyStackRef_AsPyObjectBorrow(args[0]); + 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, @@ -1721,6 +1744,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func, /* Copy all positional arguments into local variables */ Py_ssize_t j, n; + int missing_self_hint = suggest_missing_self(func, co, args, argcount); if (argcount > co->co_argcount) { n = co->co_argcount; } @@ -1864,7 +1888,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func, /* Check the number of positional arguments */ if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) { too_many_positional(tstate, co, argcount, func->func_defaults, localsplus, - func->func_qualname); + func->func_qualname, missing_self_hint); goto fail_post_args; }