From 6fb818b700f578432fdd6585344a579eba05ce2a 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 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 f3f03b28112137a..c025f587ba76847 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1604,12 +1604,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); @@ -1647,18 +1648,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, @@ -1751,6 +1805,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; } @@ -1894,7 +1949,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 d43c008b71a014730e64b41295b395aa5b5e7eec 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 c025f587ba76847..0852843409af2b2 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1650,11 +1650,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, @@ -1683,7 +1683,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 8cec83f0559a801373e846c18ddafce5113bef99 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 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 0852843409af2b2..fd845321e2662b7 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1653,8 +1653,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 6446c5a5a8354a9a9560b47a2860caab780b7757 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 fd845321e2662b7..5d29372cb46fa22 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1652,7 +1652,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 eda21d6fe0fdd77d53f5275b45acf6ba44ce64c4 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 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 b8bf56e09345798fe97f5723ad1e5a09b7e96db2 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 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 5d29372cb46fa22..c54e8bb1a993b25 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1605,7 +1605,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; @@ -1648,7 +1648,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) { @@ -1671,44 +1671,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 @@ -1803,7 +1768,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 592f419ac33f187cdfe29035ca937a6fde47ea0a 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 c54e8bb1a993b25..4c313365046eb0a 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1671,9 +1671,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 @@ -1768,7 +1774,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 ef5da0f0e20c343fa6de6543567cfe6e8698eb62 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 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?" From 715b6fac276a9094319c7fc7b64d924297fb21ad 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 c415f6f07c0c974..263b45bbc0f80fc 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 4c313365046eb0a..2d5d5e8b3d30cb6 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1673,9 +1673,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 5ad3b076f482ec75a418d74f66766f8db816dda8 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 | 4 + Lib/test/test_call.py | 80 ++++++++++++++++++- ...-06-26-20-37-22.gh-issue-152315.iVS7u5.rst | 6 +- Python/ceval.c | 36 +++++---- 4 files changed, 103 insertions(+), 23 deletions(-) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 1a73a79a58b78b1..d18b470e992ab9f 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -75,6 +75,10 @@ New features Other language changes ====================== +* :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 263b45bbc0f80fc..a3b1ccc0420c02a 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 eb002b56b0df637..4f901e2606906d0 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 2d5d5e8b3d30cb6..ca4fcbff3e92dce 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1610,7 +1610,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); @@ -1649,14 +1650,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" : "", @@ -1665,31 +1663,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 3299fe9f1b53d1389efd1d16bb267f62d715d796 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 a3b1ccc0420c02a..b15157c45bfdceb 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 ae7720a64045ca066b4f1b95da41cb2c0dc60a26 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 ca4fcbff3e92dce..6a6ed5aa6649ac0 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1671,6 +1671,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; } @@ -1682,6 +1683,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 586c65beaa9856781d5d1390d4d8648726c3ddb5 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 b15157c45bfdceb..a2edd879fc6ed7f 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 9b1d491977b476e77cac514afc3a4cd8c6314b51 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 a2edd879fc6ed7f..a0ad62abdaa3924 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 95039a6dbea2862c3ab27b7099bf0cb0d9c0f388 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 4f901e2606906d0..2f5849afbddb8ad 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 32fb5e8e0a02ebbf646598c6cb4c728693022fc9 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 000000000000000..c9c8356dcd8422c --- /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 f4669fadbd8b0570b7cec3df15c2ebdcfb2250dd 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 c9c8356dcd8422c..000000000000000 --- 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 2b80cb293b51f2fda4709782abddb03dcecef53e 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 6a6ed5aa6649ac0..222c6f11ce42ced 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1678,10 +1678,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 || @@ -1690,7 +1693,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);