diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 8e1dd93f6ed873..269116a16354da 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..7115359156eb69 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 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 8cf02651d9a408..fab687dc6e5d21 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; }