diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 243bd078d37c99..c1b9c932bf46f7 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 76f1c351e15908..9729db3931877f 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,132 @@ def static_no_args(): pass @staticmethod - def positional_only(arg, /): + def static_positional_only_one_arg(arg1, /): pass -@cpython_only -class TestErrorMessagesUseQualifiedName(unittest.TestCase): + @staticmethod + def static_one_arg(arg1): + 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 TestIncorrectNumberOfPositionalArgs(unittest.TestCase): @contextlib.contextmanager - def check_raises_type_error(self, message): + def assert_type_error_and_msg_in(self, message: str): with self.assertRaises(TypeError) as cm: yield - self.assertEqual(str(cm.exception), message) - - 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") + self.assertIn(message, str(cm.exception)) + + def test_too_many_positional_with_defaults_suggests_missing_self(self): + """A method with defaults that omits self should still get the hint.""" + msg = ".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.assert_type_error_and_msg_in(msg): + A().method_two_args("woof", "loud") + + def test_too_many_positional_but_missing_self_no_args(self): + """A zero-argument method called through an instance should get the hint.""" + msg = "takes 0 positional arguments but 1 was given. Did you forget the 'self' parameter in the function definition?" + with self.assert_type_error_and_msg_in(msg): + A().method_zero_arg() + + def test_too_many_positional_but_missing_self(self): + """A bound instance method missing self should get the targeted hint.""" + msg = ".method_one_arg() takes 1 positional argument but 2 were given. Did you forget the 'self' parameter in the function definition?" + with self.assert_type_error_and_msg_in(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 = "takes 0 positional arguments but 1 was given" + with self.assert_type_error_and_msg_in(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 = ".static_positional_only_one_arg() got some positional-only arguments passed as keyword arguments: 'arg1'" + with self.assert_type_error_and_msg_in(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 = "method_two_args() got an unexpected keyword argument 'bad'" + with self.assert_type_error_and_msg_in(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 = ".method_two_args() got multiple values for argument 'arg1'" + with self.assert_type_error_and_msg_in(msg): + A().method_two_args("quiet", "low", arg1="oops") + + def test_unbound_method_with_self_keeps_missing_argument_error(self): + """Calling an unbound method without self should keep the missing-arg error.""" + msg = ".method_one_arg() missing 1 required positional argument: 'arg1'" + with self.assert_type_error_and_msg_in(msg): + A.method_one_arg() + + def test_classmethod_missing_cls_does_not_suggest_missing_self(self): + """A classmethod missing cls conceptually should not suggest self.""" + msg = ".classmethod_one_arg() takes 1 positional argument but 2 were given" + with self.assert_type_error_and_msg_in(msg): + A.classmethod_one_arg("poodle") + + def test_classmethod_missing_cls_via_instance_does_not_suggest_missing_self(self): + """A classmethod called through an instance should not suggest self.""" + msg = ".classmethod_one_arg() takes 1 positional argument but 2 were given" + with self.assert_type_error_and_msg_in(msg): + A().classmethod_one_arg("poodle") + + def test_staticmethod_too_many_args_does_not_suggest_missing_self(self): + """A staticmethod with too many arguments should not suggest self.""" + msg = ".static_one_arg() takes 1 positional argument but 2 were given" + with self.assert_type_error_and_msg_in(msg): + A.static_one_arg(1, 2) + + def test_staticmethod_too_many_args_via_instance_does_not_suggest_missing_self(self): + """A staticmethod called through an instance should not suggest self.""" + msg = ".static_one_arg() takes 1 positional argument but 2 were given" + with self.assert_type_error_and_msg_in(msg): + A().static_one_arg(1, 2) + + def test_metaclass_missing_receiver_does_not_suggest_missing_self(self): + """A metaclass receiver error should not suggest an instance self.""" + msg = "AMeta.method_one_arg() takes 1 positional argument but 2 were given" + with self.assert_type_error_and_msg_in(msg): + AClassWithMetaclass.method_one_arg("standard") + + def test_metaclass_method_too_many_args_does_not_suggest_missing_self(self): + """A metaclass method with too many arguments should not suggest self.""" + msg = "AMeta.method_two_arg() takes 2 positional arguments but 3 were given" + with self.assert_type_error_and_msg_in(msg): + AClassWithMetaclass.method_two_arg("trail", "river") + + def test_metaclass_classmethod_does_not_suggest_missing_self(self): + """A classmethod on a metaclass should not suggest instance self.""" + msg = "AMeta.classmethod_one_arg() takes 1 positional argument but 2 were given" + with self.assert_type_error_and_msg_in(msg): + AClassWithMetaclass.classmethod_one_arg("standard") + + def test_metaclass_staticmethod_does_not_suggest_missing_self(self): + """A staticmethod on a metaclass should not suggest instance self.""" + msg = "AMeta.static_one_arg() takes 1 positional argument but 2 were given" + with self.assert_type_error_and_msg_in(msg): + AClassWithMetaclass.static_one_arg("show", "working") @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 00000000000000..2f5849afbddb8a --- /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 3a859c05a03724..6bda04cd33da76 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1605,12 +1605,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); @@ -1648,18 +1650,57 @@ 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) { + /* Don't confuse the user when they've already declared a + common convention of cls/self. */ + 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, @@ -1734,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) { @@ -1750,6 +1792,14 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func, kwdict = NULL; } + /* Pin the first argument for the "missing self" hint: the surplus + argument cleanup below may close args[0] before the hint is computed. + The pin is only needed when the "too many positional arguments" + error is about to be raised. */ + if (argcount > co->co_argcount && !(co->co_flags & CO_VARARGS)) { + first_argument = Py_NewRef(PyStackRef_AsPyObjectBorrow(args[0])); + } + /* Copy all positional arguments into local variables */ Py_ssize_t j, n; if (argcount > co->co_argcount) { @@ -1894,8 +1944,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; } @@ -1954,6 +2005,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func, goto fail_post_args; } } + Py_XDECREF(first_argument); return 0; fail_pre_positional: @@ -1970,6 +2022,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func, } /* fall through */ fail_post_args: + Py_XDECREF(first_argument); return -1; }