From 87ffa904b27ca49407d551d18d39bbf4baab1dfa Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 11:36:57 -0700 Subject: [PATCH 01/18] 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 1e42356be21ddd..953ae779acda52 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 3f636c3416e120..c66ad18a995e8d 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1605,12 +1605,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); @@ -1648,18 +1649,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, @@ -1752,6 +1806,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; } @@ -1895,7 +1950,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 b73f5cdc609679e856ebdf8af98ed2b28d4f6036 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 19:07:50 -0700 Subject: [PATCH 02/18] 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 c66ad18a995e8d..542d3e066b9e93 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1651,11 +1651,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, @@ -1684,7 +1684,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 dfa5f6ff8bddddb99ecf9e7f859def18fdbbfc20 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 19:19:42 -0700 Subject: [PATCH 03/18] 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 953ae779acda52..51d10c8083b318 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 542d3e066b9e93..65b1e734135635 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1654,8 +1654,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 02dfa50981a168e041702154c1a06383967599d9 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 19:37:40 -0700 Subject: [PATCH 04/18] Missing semicolon --- Python/ceval.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/ceval.c b/Python/ceval.c index 65b1e734135635..6e6f9c088837cf 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1653,7 +1653,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 6165cd190ca74d967026d3613e887daae8f52d2d Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 19:38:54 -0700 Subject: [PATCH 05/18] 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 51d10c8083b318..3d599b58f29eaa 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 118a24c961bbc9ae695f77f503dea13323a704d4 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 19:52:43 -0700 Subject: [PATCH 06/18] 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 3d599b58f29eaa..34b97840f30cfa 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 6e6f9c088837cf..da9dd74db94cf0 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1606,7 +1606,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; @@ -1649,7 +1649,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) { @@ -1672,44 +1672,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 @@ -1804,7 +1769,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 0a52b0b3c685eb3a6c07a13350909854809d5044 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 20:26:48 -0700 Subject: [PATCH 07/18] Change methodology --- Python/ceval.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index da9dd74db94cf0..79e2f8d9412b60 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1672,9 +1672,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 @@ -1769,7 +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(co, argcount); + int missing_self_hint = suggest_missing_self(func, co, args, argcount); if (argcount > co->co_argcount) { n = co->co_argcount; } From aaf51cf5cc8b57685d5aab7c92f61cba1eef1f69 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Fri, 26 Jun 2026 20:37:27 -0700 Subject: [PATCH 08/18] 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 00000000000000..eb002b56b0df63 --- /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?" From 887456af1754d43cc4353e85274e0fadf9138c4a Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Thu, 2 Jul 2026 20:24:23 -0700 Subject: [PATCH 09/18] Dont give hints when first arg is already named self --- Lib/test/test_call.py | 9 ++++++--- Python/ceval.c | 13 ++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 34b97840f30cfa..f033e453089334 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -910,7 +910,7 @@ def static_no_args(): def positional_only(arg, /): pass - def method_with_self(self, arg, kwarg=1): + def method_with_self(self, arg): pass def missing_self(another_arg): @@ -919,6 +919,7 @@ def missing_self(another_arg): def missing_self_no_args(): pass + @cpython_only class TestErrorMessagesUseQualifiedName(unittest.TestCase): @contextlib.contextmanager @@ -927,8 +928,10 @@ 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_with_self_does_not_suggest_missing_self(self): + msg = "A.method_with_self() takes 2 positional arguments but 3 were given" + with self.check_raises_type_error(msg): + A().method_with_self(1, 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?" diff --git a/Python/ceval.c b/Python/ceval.c index 79e2f8d9412b60..5e0a1749c41e3a 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1674,9 +1674,20 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, static int suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, _PyStackRef const *args, Py_ssize_t argcount) { - if ((co->co_argcount + 1) != argcount || argcount == 0) { + if ( + ((co->co_argcount + 1) != argcount) || + argcount == 0 // When no args are passed, its not about self + ) { return 0; } + if (co->co_argcount > 0) { + PyObject *first_parameter_name = PyTuple_GET_ITEM(co->co_localsplusnames, 0); + if (PyUnicode_CompareWithASCIIString(first_parameter_name, "self") == 0) { + // if its already named self, hint won't make sense to the user. + 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); From b9c2e707da7ebae1781fb9725683fb4cebdbe3e0 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Thu, 2 Jul 2026 20:45:02 -0700 Subject: [PATCH 10/18] gh-152315: Refine missing self TypeError hint --- Doc/whatsnew/3.16.rst | 5 ++ Lib/test/test_call.py | 80 ++++++++++++++++++- ...-06-26-20-37-22.gh-issue-152315.iVS7u5.rst | 6 +- Python/ceval.c | 36 +++++---- 4 files changed, 104 insertions(+), 23 deletions(-) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 9edbccf3adef9e..f577ed0768cdc8 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -91,6 +91,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 f033e453089334..4c2234a5c93a5e 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -906,6 +906,10 @@ def method_two_args(self, x, y): def static_no_args(): pass + @staticmethod + def static_one(arg): + pass + @staticmethod def positional_only(arg, /): pass @@ -919,6 +923,25 @@ def missing_self(another_arg): def missing_self_no_args(): pass + def method_with_self_arg(x, self): + pass + + def zero_args_self(self): + pass + + @classmethod + def missing_cls(x): + pass + +class Meta(type): + def missing_meta_cls(x): + pass + + def method(cls, x): + pass + +class B(metaclass=Meta): + pass @cpython_only class TestErrorMessagesUseQualifiedName(unittest.TestCase): @@ -929,16 +952,19 @@ def check_raises_type_error(self, message): self.assertEqual(str(cm.exception), message) def test_too_many_positional_with_self_does_not_suggest_missing_self(self): - msg = "A.method_with_self() takes 2 positional arguments but 3 were given" - with self.check_raises_type_error(msg): - A().method_with_self(1, 2) + """A regular method with self should keep the normal too-many-args error.""" + msg = "A.method_with_self() takes 2 positional arguments but 3 were given" + with self.check_raises_type_error(msg): + A().method_with_self(1, 2) def test_too_many_positional_but_missing_self(self): + """A bound instance method missing self should get the targeted hint.""" 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): + """A zero-argument method called through an instance should get the hint.""" 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() @@ -968,6 +994,54 @@ def test_multiple_values(self): with self.check_raises_type_error(msg): A().method_two_args("x", "y", x="oops") + def test_self_in_wrong_position_keeps_missing_argument_error(self): + """A parameter named self in the wrong position is a missing-arg error.""" + msg = "A.method_with_self_arg() missing 1 required positional argument: 'self'" + with self.check_raises_type_error(msg): + A().method_with_self_arg() + + def test_unbound_method_with_self_keeps_missing_argument_error(self): + """Calling an unbound method without self should keep the missing-arg error.""" + msg = "A.zero_args_self() missing 1 required positional argument: 'self'" + with self.check_raises_type_error(msg): + A.zero_args_self() + + def test_classmethod_missing_cls_does_not_suggest_missing_self(self): + """A classmethod missing cls conceptually should not suggest self.""" + msg = "A.missing_cls() takes 1 positional argument but 2 were given" + with self.check_raises_type_error(msg): + A.missing_cls(1) + + def test_classmethod_missing_cls_via_instance_does_not_suggest_missing_self(self): + """A classmethod called through an instance should not suggest self.""" + msg = "A.missing_cls() takes 1 positional argument but 2 were given" + with self.check_raises_type_error(msg): + A().missing_cls(1) + + def test_staticmethod_too_many_args_does_not_suggest_missing_self(self): + """A staticmethod with too many arguments should not suggest self.""" + msg = "A.static_one() takes 1 positional argument but 2 were given" + with self.check_raises_type_error(msg): + A.static_one(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 = "A.static_one() takes 1 positional argument but 2 were given" + with self.check_raises_type_error(msg): + A().static_one(1, 2) + + def test_metaclass_missing_receiver_does_not_suggest_missing_self(self): + """A metaclass receiver error should not suggest an instance self.""" + msg = "Meta.missing_meta_cls() takes 1 positional argument but 2 were given" + with self.check_raises_type_error(msg): + B.missing_meta_cls(1) + + 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 = "Meta.method() takes 2 positional arguments but 3 were given" + with self.check_raises_type_error(msg): + B.method(1, 2) + @cpython_only class TestErrorMessagesSuggestions(unittest.TestCase): @contextlib.contextmanager 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 index eb002b56b0df63..4f901e2606906d 100644 --- 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 @@ -1,3 +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?" +Improve :exc:`TypeError` messages for instance methods called with too many +positional arguments when the method definition appears to be missing the +``self`` parameter. diff --git a/Python/ceval.c b/Python/ceval.c index 5e0a1749c41e3a..362f7a5aba7454 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1611,7 +1611,8 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, int plural; Py_ssize_t kwonly_given = 0; Py_ssize_t i; - PyObject *sig, *kwonly_sig, *self_hint = Py_GetConstant(Py_CONSTANT_EMPTY_STR); + PyObject *sig, *kwonly_sig; + const char *self_hint = ""; Py_ssize_t co_argcount = co->co_argcount; assert((co->co_flags & CO_VARARGS) == 0); @@ -1650,14 +1651,11 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, 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); - } + 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", + "%U() takes %U positional argument%s but %zd%U %s given%s", qualname, sig, plural ? "s" : "", @@ -1666,31 +1664,35 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, 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) +suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, + _PyStackRef const *args, Py_ssize_t argcount) { - if ( - ((co->co_argcount + 1) != argcount) || - argcount == 0 // When no args are passed, its not about self - ) { + if ((co->co_argcount + 1) != argcount || argcount == 0) { + return 0; + } + + PyObject *first_argument = PyStackRef_AsPyObjectBorrow(args[0]); + if (first_argument == NULL || PyType_Check(first_argument)) { return 0; } + if (co->co_argcount > 0) { PyObject *first_parameter_name = PyTuple_GET_ITEM(co->co_localsplusnames, 0); - if (PyUnicode_CompareWithASCIIString(first_parameter_name, "self") == 0) { - // if its already named self, hint won't make sense to the user. + if (PyUnicode_CompareWithASCIIString(first_parameter_name, "self") == 0 || + PyUnicode_CompareWithASCIIString(first_parameter_name, "cls") == 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); + PyFunctionObject *possibly_current_function = + (PyFunctionObject *)_PyType_Lookup(self_cls, co->co_name); return possibly_current_function == func; } From f61cf66694f0d70225684e9d22993604ad2eeffe Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Thu, 2 Jul 2026 20:57:18 -0700 Subject: [PATCH 11/18] gh-152315: Use descriptive TypeError test fixtures --- Lib/test/test_call.py | 94 +++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 4c2234a5c93a5e..0fade47ed2b6ec 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -898,49 +898,49 @@ def f(self, *args, **kwargs): self.assertEqual(args_captured, [("foo",)]) self.assertEqual(kwargs_captured, [{"baz": "bar"}]) -class A: - def method_two_args(self, x, y): +class Dog: + def bark(self, volume, pitch): pass @staticmethod - def static_no_args(): + def wag_tail_no_args(): pass @staticmethod - def static_one(arg): + def wag_tail(times): pass @staticmethod - def positional_only(arg, /): + def fetch(toy, /): pass - def method_with_self(self, arg): + def bark_with_self(self, sound): pass - def missing_self(another_arg): + def bark_missing_self(sound): pass - def missing_self_no_args(): + def wag_missing_self(): pass - def method_with_self_arg(x, self): + def bark_with_self_arg(sound, self): pass - def zero_args_self(self): + def sit(self): pass @classmethod - def missing_cls(x): + def register_breed(dog_type): pass -class Meta(type): - def missing_meta_cls(x): +class DogMeta(type): + def register_pack(dog_type): pass - def method(cls, x): + def track_pack(cls, scent): pass -class B(metaclass=Meta): +class Poodle(metaclass=DogMeta): pass @cpython_only @@ -953,94 +953,94 @@ def check_raises_type_error(self, message): def test_too_many_positional_with_self_does_not_suggest_missing_self(self): """A regular method with self should keep the normal too-many-args error.""" - msg = "A.method_with_self() takes 2 positional arguments but 3 were given" + msg = "Dog.bark_with_self() takes 2 positional arguments but 3 were given" with self.check_raises_type_error(msg): - A().method_with_self(1, 2) + Dog().bark_with_self("woof", "loud") def test_too_many_positional_but_missing_self(self): """A bound instance method missing self should get the targeted hint.""" - msg = "A.missing_self() takes 1 positional argument but 2 were given. Did you forget the 'self' parameter in the function definition?" + msg = "Dog.bark_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") + Dog().bark_missing_self("woof") def test_too_many_positional_but_missing_self_no_args(self): """A zero-argument method called through an instance should get the hint.""" - msg = "A.missing_self_no_args() takes 0 positional arguments but 1 was given. Did you forget the 'self' parameter in the function definition?" + msg = "Dog.wag_missing_self() 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() + Dog().wag_missing_self() def test_missing_arguments(self): - msg = "A.method_two_args() missing 1 required positional argument: 'y'" + msg = "Dog.bark() missing 1 required positional argument: 'pitch'" with self.check_raises_type_error(msg): - A().method_two_args("x") + Dog().bark("quiet") def test_too_many_positional(self): - msg = "A.static_no_args() takes 0 positional arguments but 1 was given" + msg = "Dog.wag_tail_no_args() takes 0 positional arguments but 1 was given" with self.check_raises_type_error(msg): - A.static_no_args("oops it's an arg") + Dog.wag_tail_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'" + msg = "Dog.fetch() got some positional-only arguments passed as keyword arguments: 'toy'" with self.check_raises_type_error(msg): - A.positional_only(arg="x") + Dog.fetch(toy="ball") def test_unexpected_keyword(self): - msg = "A.method_two_args() got an unexpected keyword argument 'bad'" + msg = "Dog.bark() got an unexpected keyword argument 'bad'" with self.check_raises_type_error(msg): - A().method_two_args(bad="x") + Dog().bark(bad="x") def test_multiple_values(self): - msg = "A.method_two_args() got multiple values for argument 'x'" + msg = "Dog.bark() got multiple values for argument 'volume'" with self.check_raises_type_error(msg): - A().method_two_args("x", "y", x="oops") + Dog().bark("quiet", "low", volume="oops") def test_self_in_wrong_position_keeps_missing_argument_error(self): """A parameter named self in the wrong position is a missing-arg error.""" - msg = "A.method_with_self_arg() missing 1 required positional argument: 'self'" + msg = "Dog.bark_with_self_arg() missing 1 required positional argument: 'self'" with self.check_raises_type_error(msg): - A().method_with_self_arg() + Dog().bark_with_self_arg() def test_unbound_method_with_self_keeps_missing_argument_error(self): """Calling an unbound method without self should keep the missing-arg error.""" - msg = "A.zero_args_self() missing 1 required positional argument: 'self'" + msg = "Dog.sit() missing 1 required positional argument: 'self'" with self.check_raises_type_error(msg): - A.zero_args_self() + Dog.sit() def test_classmethod_missing_cls_does_not_suggest_missing_self(self): """A classmethod missing cls conceptually should not suggest self.""" - msg = "A.missing_cls() takes 1 positional argument but 2 were given" + msg = "Dog.register_breed() takes 1 positional argument but 2 were given" with self.check_raises_type_error(msg): - A.missing_cls(1) + Dog.register_breed("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 = "A.missing_cls() takes 1 positional argument but 2 were given" + msg = "Dog.register_breed() takes 1 positional argument but 2 were given" with self.check_raises_type_error(msg): - A().missing_cls(1) + Dog().register_breed("poodle") def test_staticmethod_too_many_args_does_not_suggest_missing_self(self): """A staticmethod with too many arguments should not suggest self.""" - msg = "A.static_one() takes 1 positional argument but 2 were given" + msg = "Dog.wag_tail() takes 1 positional argument but 2 were given" with self.check_raises_type_error(msg): - A.static_one(1, 2) + Dog.wag_tail(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 = "A.static_one() takes 1 positional argument but 2 were given" + msg = "Dog.wag_tail() takes 1 positional argument but 2 were given" with self.check_raises_type_error(msg): - A().static_one(1, 2) + Dog().wag_tail(1, 2) def test_metaclass_missing_receiver_does_not_suggest_missing_self(self): """A metaclass receiver error should not suggest an instance self.""" - msg = "Meta.missing_meta_cls() takes 1 positional argument but 2 were given" + msg = "DogMeta.register_pack() takes 1 positional argument but 2 were given" with self.check_raises_type_error(msg): - B.missing_meta_cls(1) + Poodle.register_pack("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 = "Meta.method() takes 2 positional arguments but 3 were given" + msg = "DogMeta.track_pack() takes 2 positional arguments but 3 were given" with self.check_raises_type_error(msg): - B.method(1, 2) + Poodle.track_pack("trail", "river") @cpython_only class TestErrorMessagesSuggestions(unittest.TestCase): From a67750f49ce7b18a6ca0aec9441ba1210964ec24 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Thu, 2 Jul 2026 20:58:18 -0700 Subject: [PATCH 12/18] gh-152315: Document missing self heuristic --- Python/ceval.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Python/ceval.c b/Python/ceval.c index 362f7a5aba7454..31d5c11bdacbae 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1672,6 +1672,7 @@ static int suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, _PyStackRef const *args, Py_ssize_t argcount) { + /* Missing self shows up as exactly one extra positional argument. */ if ((co->co_argcount + 1) != argcount || argcount == 0) { return 0; } @@ -1683,6 +1684,7 @@ suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, 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) { From db5f2a42913009f1713183fa9ac538f7791219bf Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Thu, 2 Jul 2026 21:02:03 -0700 Subject: [PATCH 13/18] gh-152315: Cover metaclass descriptors --- Lib/test/test_call.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 0fade47ed2b6ec..3b3cd3acef154f 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -940,6 +940,14 @@ def register_pack(dog_type): def track_pack(cls, scent): pass + @classmethod + def classify_pack(dog_type): + pass + + @staticmethod + def tag_pack(label): + pass + class Poodle(metaclass=DogMeta): pass @@ -1042,6 +1050,18 @@ def test_metaclass_method_too_many_args_does_not_suggest_missing_self(self): with self.check_raises_type_error(msg): Poodle.track_pack("trail", "river") + def test_metaclass_classmethod_does_not_suggest_missing_self(self): + """A classmethod on a metaclass should not suggest instance self.""" + msg = "DogMeta.classify_pack() takes 1 positional argument but 2 were given" + with self.check_raises_type_error(msg): + Poodle.classify_pack("standard") + + def test_metaclass_staticmethod_does_not_suggest_missing_self(self): + """A staticmethod on a metaclass should not suggest instance self.""" + msg = "DogMeta.tag_pack() takes 1 positional argument but 2 were given" + with self.check_raises_type_error(msg): + Poodle.tag_pack("show", "working") + @cpython_only class TestErrorMessagesSuggestions(unittest.TestCase): @contextlib.contextmanager From 6721c676660977b93d9fe6203cf08b40f8864e5d Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Thu, 2 Jul 2026 21:02:50 -0700 Subject: [PATCH 14/18] gh-152315: Rename metaclass test fixture --- Lib/test/test_call.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 3b3cd3acef154f..5332d0efde617c 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -933,7 +933,7 @@ def sit(self): def register_breed(dog_type): pass -class DogMeta(type): +class AnimalMeta(type): def register_pack(dog_type): pass @@ -948,7 +948,7 @@ def classify_pack(dog_type): def tag_pack(label): pass -class Poodle(metaclass=DogMeta): +class Poodle(metaclass=AnimalMeta): pass @cpython_only @@ -1040,25 +1040,25 @@ def test_staticmethod_too_many_args_via_instance_does_not_suggest_missing_self(s def test_metaclass_missing_receiver_does_not_suggest_missing_self(self): """A metaclass receiver error should not suggest an instance self.""" - msg = "DogMeta.register_pack() takes 1 positional argument but 2 were given" + msg = "AnimalMeta.register_pack() takes 1 positional argument but 2 were given" with self.check_raises_type_error(msg): Poodle.register_pack("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 = "DogMeta.track_pack() takes 2 positional arguments but 3 were given" + msg = "AnimalMeta.track_pack() takes 2 positional arguments but 3 were given" with self.check_raises_type_error(msg): Poodle.track_pack("trail", "river") def test_metaclass_classmethod_does_not_suggest_missing_self(self): """A classmethod on a metaclass should not suggest instance self.""" - msg = "DogMeta.classify_pack() takes 1 positional argument but 2 were given" + msg = "AnimalMeta.classify_pack() takes 1 positional argument but 2 were given" with self.check_raises_type_error(msg): Poodle.classify_pack("standard") def test_metaclass_staticmethod_does_not_suggest_missing_self(self): """A staticmethod on a metaclass should not suggest instance self.""" - msg = "DogMeta.tag_pack() takes 1 positional argument but 2 were given" + msg = "AnimalMeta.tag_pack() takes 1 positional argument but 2 were given" with self.check_raises_type_error(msg): Poodle.tag_pack("show", "working") From c246bad00b598ffa136c1e3b4441758f347e09a7 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Thu, 2 Jul 2026 21:04:08 -0700 Subject: [PATCH 15/18] gh-152315: Align missing self docs wording --- .../2026-06-26-20-37-22.gh-issue-152315.iVS7u5.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 4f901e2606906d..2f5849afbddb8a 100644 --- 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 @@ -1,3 +1,3 @@ -Improve :exc:`TypeError` messages for instance methods called with too many -positional arguments when the method definition appears to be missing the -``self`` parameter. +: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. From 5cd94fef9907f2c1cb42f48d0d2c31f77a3e1768 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Thu, 2 Jul 2026 21:10:42 -0700 Subject: [PATCH 16/18] gh-152315: Add agent guidance --- AGENTS.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000000..c9c8356dcd8422 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,66 @@ +# AGENTS.md + +Guidance for future agent work in this CPython checkout. + +## Work From Repo Evidence + +- Start by reading the local code, tests, and recent commits touching the same area. CPython patches are usually narrow and issue-scoped. +- Prefer existing local patterns over new abstractions. If nearby code uses a helper, macro, assertion style, or test support utility, follow it. +- Keep unrelated cleanup out of the patch. Reviewers expect a clear line from issue to code to tests to NEWS/docs. +- Before changing behavior, make the current failure visible with a focused reproducer or a targeted test. + +## C Code Changes + +- Treat every new object-producing call as an error path. If a call can fail, either propagate the exception or deliberately avoid allocation. +- Be explicit about reference ownership. Pair new references with cleanup on every path, and avoid converting borrowed/constant data into owned objects unless needed. +- For OOM or NULL-handling fixes, preserve the original exception semantics and add only the minimum logic required. +- Do not rely on string formatting, `__qualname__`, or naming conventions when an exact runtime check is available. +- Keep comments sparse and useful. Add comments for non-obvious heuristics and ownership decisions, not for code that reads directly. + +## Tests + +- Add regression tests next to existing tests for the affected behavior, and keep assertions exact when CPython already has stable wording. +- Include both the positive case and the closest false-positive cases. Descriptor and binding changes usually need instance, classmethod, staticmethod, and metaclass coverage. +- Use descriptive fixtures and method names that make failure output understandable. +- When changing error handling, test the precise error message or exception path rather than only testing that an exception was raised. +- For test support improvements, prefer shared helpers over repeating skip or environment logic in many files. + +## Docs And NEWS + +- User-visible behavior changes normally need a `Misc/NEWS.d/next/...` entry. +- If a reviewer asks for a What's New note, keep its wording aligned with the NEWS entry, with contributor credit in What's New only. +- Use CPython reST roles such as `:exc:`, `:func:`, and `:gh:` where appropriate. +- Documentation changes should describe the behavior, not the implementation detail. + +## Reviews And PR Hygiene + +- Fetch and read the full PR conversation before responding to review feedback. Inline review threads may contain follow-up comments that change the desired fix. +- Group review feedback by behavior, not by file. Make sure each requested edge case is either covered by code/tests or explicitly called out as intentionally not addressed. +- After requested changes are made, rerun focused tests and summarize exactly which reviewer concerns were addressed. +- If Bedevere requests the standard review phrase, use it only after the relevant commits are pushed to the PR branch. + +## Commands + +- Build after C changes: + ```sh + make -j8 + ``` +- Run a focused test module: + ```sh + ./python.exe -m test test_call + ``` +- Check whitespace before committing: + ```sh + git diff --check + ``` +- Inspect recent area-specific history: + ```sh + git log --oneline --max-count=80 -- Lib/test Python Modules Objects Doc Misc + ``` + +## Commits + +- Use CPython-style commit messages when possible: `gh-NNNNNN: Short imperative summary`. +- Keep commits reviewable. A small follow-up commit is fine for review-driven naming, docs wording, or missing coverage. +- Do not add promotional or generated-by footers to commits. +- Stage only files intended for the PR. Leave local planning notes and scratch files untracked unless explicitly requested. From 5519866eff4718a26d4b01be0492f2444b4bd740 Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Thu, 2 Jul 2026 22:01:46 -0700 Subject: [PATCH 17/18] gh-152315: Remove agent guidance --- AGENTS.md | 66 ------------------------------------------------------- 1 file changed, 66 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c9c8356dcd8422..00000000000000 --- a/AGENTS.md +++ /dev/null @@ -1,66 +0,0 @@ -# AGENTS.md - -Guidance for future agent work in this CPython checkout. - -## Work From Repo Evidence - -- Start by reading the local code, tests, and recent commits touching the same area. CPython patches are usually narrow and issue-scoped. -- Prefer existing local patterns over new abstractions. If nearby code uses a helper, macro, assertion style, or test support utility, follow it. -- Keep unrelated cleanup out of the patch. Reviewers expect a clear line from issue to code to tests to NEWS/docs. -- Before changing behavior, make the current failure visible with a focused reproducer or a targeted test. - -## C Code Changes - -- Treat every new object-producing call as an error path. If a call can fail, either propagate the exception or deliberately avoid allocation. -- Be explicit about reference ownership. Pair new references with cleanup on every path, and avoid converting borrowed/constant data into owned objects unless needed. -- For OOM or NULL-handling fixes, preserve the original exception semantics and add only the minimum logic required. -- Do not rely on string formatting, `__qualname__`, or naming conventions when an exact runtime check is available. -- Keep comments sparse and useful. Add comments for non-obvious heuristics and ownership decisions, not for code that reads directly. - -## Tests - -- Add regression tests next to existing tests for the affected behavior, and keep assertions exact when CPython already has stable wording. -- Include both the positive case and the closest false-positive cases. Descriptor and binding changes usually need instance, classmethod, staticmethod, and metaclass coverage. -- Use descriptive fixtures and method names that make failure output understandable. -- When changing error handling, test the precise error message or exception path rather than only testing that an exception was raised. -- For test support improvements, prefer shared helpers over repeating skip or environment logic in many files. - -## Docs And NEWS - -- User-visible behavior changes normally need a `Misc/NEWS.d/next/...` entry. -- If a reviewer asks for a What's New note, keep its wording aligned with the NEWS entry, with contributor credit in What's New only. -- Use CPython reST roles such as `:exc:`, `:func:`, and `:gh:` where appropriate. -- Documentation changes should describe the behavior, not the implementation detail. - -## Reviews And PR Hygiene - -- Fetch and read the full PR conversation before responding to review feedback. Inline review threads may contain follow-up comments that change the desired fix. -- Group review feedback by behavior, not by file. Make sure each requested edge case is either covered by code/tests or explicitly called out as intentionally not addressed. -- After requested changes are made, rerun focused tests and summarize exactly which reviewer concerns were addressed. -- If Bedevere requests the standard review phrase, use it only after the relevant commits are pushed to the PR branch. - -## Commands - -- Build after C changes: - ```sh - make -j8 - ``` -- Run a focused test module: - ```sh - ./python.exe -m test test_call - ``` -- Check whitespace before committing: - ```sh - git diff --check - ``` -- Inspect recent area-specific history: - ```sh - git log --oneline --max-count=80 -- Lib/test Python Modules Objects Doc Misc - ``` - -## Commits - -- Use CPython-style commit messages when possible: `gh-NNNNNN: Short imperative summary`. -- Keep commits reviewable. A small follow-up commit is fine for review-driven naming, docs wording, or missing coverage. -- Do not add promotional or generated-by footers to commits. -- Stage only files intended for the PR. Leave local planning notes and scratch files untracked unless explicitly requested. From a8bf860eb20970445511e2ff178a8ec9d50eca1d Mon Sep 17 00:00:00 2001 From: Suren Nihalani Date: Thu, 2 Jul 2026 22:07:01 -0700 Subject: [PATCH 18/18] gh-152315: Clarify missing self heuristic --- Python/ceval.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Python/ceval.c b/Python/ceval.c index 31d5c11bdacbae..18c61edadfd7de 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1679,10 +1679,13 @@ suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, PyObject *first_argument = PyStackRef_AsPyObjectBorrow(args[0]); if (first_argument == NULL || PyType_Check(first_argument)) { + // When first arg is NULL, it's not really about self + // If its a type object, then its 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 || @@ -1691,7 +1694,7 @@ suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, return 0; } } - + // If the current function matches on the type, its likely worth adding the hint PyTypeObject *self_cls = Py_TYPE(first_argument); PyFunctionObject *possibly_current_function = (PyFunctionObject *)_PyType_Lookup(self_cls, co->co_name);