From 7b9384f0ed411c3ff380c769b9a56eae17659c5d Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 11:36:57 -0700 Subject: [PATCH 1/8] Suggest missing 'self' when a method is called with too many positional argumen --- Lib/test/test_call.py | 16 +++++++++++ Python/ceval.c | 65 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index f42526aee194174..a7450e29a07ef14 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -910,6 +910,12 @@ def static_no_args(): def positional_only(arg, /): pass + def missing_self(another_arg): + pass + + def missing_self_no_args(): + pass + @cpython_only class TestErrorMessagesUseQualifiedName(unittest.TestCase): @@ -919,6 +925,16 @@ def check_raises_type_error(self, message): yield self.assertEqual(str(cm.exception), message) + def test_too_many_positional_but_missing_self(self): + msg = "A.missing_self() takes 1 positional argument but 2 were given. Did you forget to declare 'self' as the first parameter?" + 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 to declare 'self' as the first parameter?" + 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/Python/ceval.c b/Python/ceval.c index fbea1f67a36f442..c6b3461a7ed2ad3 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 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,71 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, kwonly_sig = Py_GetConstant(Py_CONSTANT_EMPTY_STR); assert(kwonly_sig != NULL); } + if (suggest_missing_self) { + self_hint = PyUnicode_FromString( + ". Did you forget to declare 'self' as the first parameter?"); + if (self_hint == NULL) { + Py_DECREF(sig); + Py_DECREF(kwonly_sig); + return; + } + } _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 >= argcount) { + // When declared count is more than provided, there is nothing to add + return 0; + } + + PyObject *self = PyStackRef_AsPyObjectBorrow(args[0]); + if (self == NULL) { + // When first arg is NULL, its not really about self + return 0; + } + + Py_ssize_t qualname_len; + const char *qualname = PyUnicode_AsUTF8AndSize( + func->func_qualname, &qualname_len); + if (qualname == NULL) { + PyErr_Clear(); + return 0; + } + + const char *method_dot = strrchr(qualname, '.'); + if (method_dot == NULL) { + return 0; + } + + const char *class_start = qualname; + for (const char *p = qualname; p < method_dot; p++) { + if (*p == '.') { + class_start = p + 1; + } + } + Py_ssize_t class_len = method_dot - class_start; + const char *type_name = Py_TYPE(self)->tp_name; + + return (strlen(type_name) == (size_t)class_len + && strncmp(type_name, class_start, (size_t)class_len) == 0); +} + static int positional_only_passed_as_keyword(PyThreadState *tstate, PyCodeObject *co, Py_ssize_t kwcount, PyObject* kwnames, @@ -1721,6 +1775,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 +1919,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; } From a4c0ffe3adc9611e3022c2698a5ca11980d7535b Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 19:07:50 -0700 Subject: [PATCH 2/8] Respond to comments about giving typeerror without hint and changing the error message --- Python/ceval.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index c6b3461a7ed2ad3..d54afeb1aae24d6 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1620,11 +1620,11 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, } if (suggest_missing_self) { self_hint = PyUnicode_FromString( - ". Did you forget to declare 'self' as the first parameter?"); + ". Did you forget the 'self' parameter in the function definition?"); if (self_hint == NULL) { + self_hint = Py_GetConstant(Py_CONSTANT_EMPTY_STR) Py_DECREF(sig); Py_DECREF(kwonly_sig); - return; } } _PyErr_Format(tstate, PyExc_TypeError, @@ -1653,7 +1653,7 @@ suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, PyObject *self = PyStackRef_AsPyObjectBorrow(args[0]); if (self == NULL) { - // When first arg is NULL, its not really about self + // When first arg is NULL, it's not really about self return 0; } From 38093f5d63b647bff975e40e4c8bf57c55dec21a Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 19:19:42 -0700 Subject: [PATCH 3/8] Add test for happy path --- Lib/test/test_call.py | 6 ++++++ Python/ceval.c | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index a7450e29a07ef14..7b8618b3de77cd0 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -910,6 +910,9 @@ def static_no_args(): def positional_only(arg, /): pass + def method_with_self(self, arg, kwarg=1): + pass + def missing_self(another_arg): pass @@ -925,6 +928,9 @@ def check_raises_type_error(self, message): 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 to declare 'self' as the first parameter?" with self.check_raises_type_error(msg): diff --git a/Python/ceval.c b/Python/ceval.c index d54afeb1aae24d6..0a351b4b5cbfb79 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1623,8 +1623,6 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, ". Did you forget the 'self' parameter in the function definition?"); if (self_hint == NULL) { self_hint = Py_GetConstant(Py_CONSTANT_EMPTY_STR) - Py_DECREF(sig); - Py_DECREF(kwonly_sig); } } _PyErr_Format(tstate, PyExc_TypeError, From 37e16129342e3d433ffe55c11999e21208cc1168 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 19:37:40 -0700 Subject: [PATCH 4/8] Missing semicolon --- Python/ceval.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/ceval.c b/Python/ceval.c index 0a351b4b5cbfb79..9e5862ba78de0a3 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1622,7 +1622,7 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, 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) + self_hint = Py_GetConstant(Py_CONSTANT_EMPTY_STR); } } _PyErr_Format(tstate, PyExc_TypeError, From 5cb69060912ff88ed8f495207cffe6ca6b7f1c6d Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 19:38:54 -0700 Subject: [PATCH 5/8] Fix tests --- Lib/test/test_call.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 7b8618b3de77cd0..7660e24011d22cf 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -932,12 +932,12 @@ 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 to declare 'self' as the first parameter?" + 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 to declare 'self' as the first parameter?" + 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() From d6ef62e0dac39c0d76fa2542596b181557fbe8ac Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 19:52:43 -0700 Subject: [PATCH 6/8] Be simpler --- Lib/test/test_call.py | 1 - Python/ceval.c | 45 +++++-------------------------------------- 2 files changed, 5 insertions(+), 41 deletions(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 7660e24011d22cf..c415f6f07c0c974 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -921,7 +921,6 @@ def missing_self_no_args(): @cpython_only class TestErrorMessagesUseQualifiedName(unittest.TestCase): - @contextlib.contextmanager def check_raises_type_error(self, message): with self.assertRaises(TypeError) as cm: diff --git a/Python/ceval.c b/Python/ceval.c index 9e5862ba78de0a3..d7b32a33991e351 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1575,7 +1575,7 @@ static void too_many_positional(PyThreadState *tstate, PyCodeObject *co, Py_ssize_t given, PyObject *defaults, _PyStackRef *localsplus, PyObject *qualname, - int suggest_missing_self) + int should_suggest_missing_self) { int plural; Py_ssize_t kwonly_given = 0; @@ -1618,7 +1618,7 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, kwonly_sig = Py_GetConstant(Py_CONSTANT_EMPTY_STR); assert(kwonly_sig != NULL); } - if (suggest_missing_self) { + if (should_suggest_missing_self) { self_hint = PyUnicode_FromString( ". Did you forget the 'self' parameter in the function definition?"); if (self_hint == NULL) { @@ -1641,44 +1641,9 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, } static int -suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, - _PyStackRef const *args, Py_ssize_t argcount) +suggest_missing_self(PyCodeObject *co, Py_ssize_t argcount) { - if (co->co_argcount >= argcount) { - // When declared count is more than provided, there is nothing to add - return 0; - } - - PyObject *self = PyStackRef_AsPyObjectBorrow(args[0]); - if (self == NULL) { - // When first arg is NULL, it's not really about self - return 0; - } - - Py_ssize_t qualname_len; - const char *qualname = PyUnicode_AsUTF8AndSize( - func->func_qualname, &qualname_len); - if (qualname == NULL) { - PyErr_Clear(); - return 0; - } - - const char *method_dot = strrchr(qualname, '.'); - if (method_dot == NULL) { - return 0; - } - - const char *class_start = qualname; - for (const char *p = qualname; p < method_dot; p++) { - if (*p == '.') { - class_start = p + 1; - } - } - Py_ssize_t class_len = method_dot - class_start; - const char *type_name = Py_TYPE(self)->tp_name; - - return (strlen(type_name) == (size_t)class_len - && strncmp(type_name, class_start, (size_t)class_len) == 0); + return (co->co_argcount + 1) == argcount } static int @@ -1773,7 +1738,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); + int missing_self_hint = suggest_missing_self(co, argcount); if (argcount > co->co_argcount) { n = co->co_argcount; } From 723d15a7fd8f47fe1a417038338dba63aadeb414 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 20:26:48 -0700 Subject: [PATCH 7/8] Change methodology --- Python/ceval.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index d7b32a33991e351..894bfe9fb4c988b 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1641,9 +1641,15 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, } static int -suggest_missing_self(PyCodeObject *co, Py_ssize_t argcount) +suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, _PyStackRef const *args, Py_ssize_t argcount) { - return (co->co_argcount + 1) == 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 @@ -1738,7 +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(co, argcount); + int missing_self_hint = suggest_missing_self(func, co, args, argcount); if (argcount > co->co_argcount) { n = co->co_argcount; } From febd510542f57ac638be3e9f307afc7b5486c530 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 20:37:27 -0700 Subject: [PATCH 8/8] Add misc --- .../2026-06-26-20-37-22.gh-issue-152315.iVS7u5.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-20-37-22.gh-issue-152315.iVS7u5.rst 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?"