Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
87ffa90
Suggest missing 'self' when a method is called with too many position…
SurenNihalani Jun 26, 2026
b73f5cd
Respond to comments about giving typeerror without hint and changing …
SurenNihalani Jun 27, 2026
dfa5f6f
Add test for happy path
SurenNihalani Jun 27, 2026
02dfa50
Missing semicolon
SurenNihalani Jun 27, 2026
6165cd1
Fix tests
SurenNihalani Jun 27, 2026
118a24c
Be simpler
SurenNihalani Jun 27, 2026
0a52b0b
Change methodology
SurenNihalani Jun 27, 2026
aaf51cf
Add misc
SurenNihalani Jun 27, 2026
887456a
Dont give hints when first arg is already named self
SurenNihalani Jul 3, 2026
b9c2e70
gh-152315: Refine missing self TypeError hint
SurenNihalani Jul 3, 2026
f61cf66
gh-152315: Use descriptive TypeError test fixtures
SurenNihalani Jul 3, 2026
a67750f
gh-152315: Document missing self heuristic
SurenNihalani Jul 3, 2026
db5f2a4
gh-152315: Cover metaclass descriptors
SurenNihalani Jul 3, 2026
6721c67
gh-152315: Rename metaclass test fixture
SurenNihalani Jul 3, 2026
c246bad
gh-152315: Align missing self docs wording
SurenNihalani Jul 3, 2026
5cd94fe
gh-152315: Add agent guidance
SurenNihalani Jul 3, 2026
5519866
gh-152315: Remove agent guidance
SurenNihalani Jul 3, 2026
a8bf860
gh-152315: Clarify missing self heuristic
SurenNihalani Jul 3, 2026
f9fd4da
gh-152315: Remove redundant argcount check
SurenNihalani Aug 23, 2026
57aca39
gh-152315: Compute missing self hint only when error is raised
SurenNihalani Sep 9, 2026
6ec11c7
gh-152315: Pin first argument for missing self hint
SurenNihalani Sep 9, 2026
bf64b96
gh-152315: Initialize first_argument before the fail_pre_positional exit
SurenNihalani Sep 9, 2026
498062e
gh-152315: Drop comment
SurenNihalani Sep 9, 2026
e158625
Merge remote-tracking branch 'origin/main' into missing_self
SurenNihalani Sep 9, 2026
97bbc04
Undo calls
SurenNihalani Sep 13, 2026
587703c
Update tests
SurenNihalani Sep 13, 2026
2e5ee8d
Rename for consistency
SurenNihalani Sep 13, 2026
f875f81
Fix all tests
SurenNihalani Sep 13, 2026
20b5767
gh-152315: Fix misleading test names and drop duplicate tests
SurenNihalani Sep 13, 2026
7914cdd
gh-152315: Fix comment style and typos in suggest_missing_self
SurenNihalani Sep 13, 2026
725a1fb
gh-152315: Rename TypeError message helper to match assertIn semantics
SurenNihalani Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ Other language changes
they reference) alive indefinitely.
(Contributed by Łukasz Langa in :gh:`102960`.)

* :exc:`TypeError` messages for some instance methods called with too many
positional arguments now suggest checking whether the method definition is
missing the ``self`` parameter. (Contributed by Suren Nihalani in
:gh:`152315`.)


New modules
===========
Expand Down
142 changes: 121 additions & 21 deletions Lib/test/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,50 +947,150 @@ def f(self, *args, **kwargs):
self.assertEqual(kwargs_captured, [{"baz": "bar"}])

class A:
def method_two_args(self, x, y):
def method_two_args(arg1=None, arg2=None):
pass

def method_one_arg(arg1):
pass

def method_zero_arg():
pass

@classmethod
def classmethod_one_arg(arg1):
pass

@staticmethod
def static_no_args():
pass

@staticmethod
def positional_only(arg, /):
def static_positional_only_one_arg(arg1, /):
pass

@cpython_only
class TestErrorMessagesUseQualifiedName(unittest.TestCase):
@staticmethod
def static_one_arg(arg1):
pass


class AMeta(type):

def method_one_arg(arg1):
pass

def method_two_arg(arg1, arg2):
pass

@classmethod
def classmethod_one_arg(arg1):
pass

@staticmethod
def static_one_arg(arg1):
pass

class AClassWithMetaclass(metaclass=AMeta):
pass

@cpython_only
class TestIncorrectNumberOfPositionalArgs(unittest.TestCase):
@contextlib.contextmanager
def check_raises_type_error(self, message):
def assert_type_error_and_msg_in(self, message: str):
with self.assertRaises(TypeError) as cm:
yield
self.assertEqual(str(cm.exception), message)

def test_missing_arguments(self):
msg = "A.method_two_args() missing 1 required positional argument: 'y'"
with self.check_raises_type_error(msg):
A().method_two_args("x")
self.assertIn(message, str(cm.exception))

def test_too_many_positional_with_defaults_suggests_missing_self(self):
"""A method with defaults that omits self should still get the hint."""
msg = ".method_two_args() takes from 0 to 2 positional arguments but 3 were given. Did you forget the 'self' parameter in the function definition?"
with self.assert_type_error_and_msg_in(msg):
A().method_two_args("woof", "loud")

def test_too_many_positional_but_missing_self_no_args(self):
"""A zero-argument method called through an instance should get the hint."""
msg = "takes 0 positional arguments but 1 was given. Did you forget the 'self' parameter in the function definition?"
with self.assert_type_error_and_msg_in(msg):
A().method_zero_arg()

def test_too_many_positional_but_missing_self(self):
"""A bound instance method missing self should get the targeted hint."""
msg = ".method_one_arg() takes 1 positional argument but 2 were given. Did you forget the 'self' parameter in the function definition?"
with self.assert_type_error_and_msg_in(msg):
A().method_one_arg("quiet")

def test_too_many_positional(self):
msg = "A.static_no_args() takes 0 positional arguments but 1 was given"
with self.check_raises_type_error(msg):
msg = "takes 0 positional arguments but 1 was given"
with self.assert_type_error_and_msg_in(msg):
A.static_no_args("oops it's an arg")

def test_positional_only_passed_as_keyword(self):
msg = "A.positional_only() got some positional-only arguments passed as keyword arguments: 'arg'"
with self.check_raises_type_error(msg):
A.positional_only(arg="x")
msg = ".static_positional_only_one_arg() got some positional-only arguments passed as keyword arguments: 'arg1'"
with self.assert_type_error_and_msg_in(msg):
A.static_positional_only_one_arg(arg1="ball")

def test_unexpected_keyword(self):
msg = "A.method_two_args() got an unexpected keyword argument 'bad'"
with self.check_raises_type_error(msg):
msg = "method_two_args() got an unexpected keyword argument 'bad'"
with self.assert_type_error_and_msg_in(msg):
A().method_two_args(bad="x")

def test_multiple_values(self):
msg = "A.method_two_args() got multiple values for argument 'x'"
with self.check_raises_type_error(msg):
A().method_two_args("x", "y", x="oops")
msg = ".method_two_args() got multiple values for argument 'arg1'"
with self.assert_type_error_and_msg_in(msg):
A().method_two_args("quiet", "low", arg1="oops")

def test_unbound_method_with_self_keeps_missing_argument_error(self):
"""Calling an unbound method without self should keep the missing-arg error."""
msg = ".method_one_arg() missing 1 required positional argument: 'arg1'"
with self.assert_type_error_and_msg_in(msg):
A.method_one_arg()

def test_classmethod_missing_cls_does_not_suggest_missing_self(self):
"""A classmethod missing cls conceptually should not suggest self."""
msg = ".classmethod_one_arg() takes 1 positional argument but 2 were given"
with self.assert_type_error_and_msg_in(msg):
A.classmethod_one_arg("poodle")

def test_classmethod_missing_cls_via_instance_does_not_suggest_missing_self(self):
"""A classmethod called through an instance should not suggest self."""
msg = ".classmethod_one_arg() takes 1 positional argument but 2 were given"
with self.assert_type_error_and_msg_in(msg):
A().classmethod_one_arg("poodle")

def test_staticmethod_too_many_args_does_not_suggest_missing_self(self):
"""A staticmethod with too many arguments should not suggest self."""
msg = ".static_one_arg() takes 1 positional argument but 2 were given"
with self.assert_type_error_and_msg_in(msg):
A.static_one_arg(1, 2)

def test_staticmethod_too_many_args_via_instance_does_not_suggest_missing_self(self):
"""A staticmethod called through an instance should not suggest self."""
msg = ".static_one_arg() takes 1 positional argument but 2 were given"
with self.assert_type_error_and_msg_in(msg):
A().static_one_arg(1, 2)

def test_metaclass_missing_receiver_does_not_suggest_missing_self(self):
"""A metaclass receiver error should not suggest an instance self."""
msg = "AMeta.method_one_arg() takes 1 positional argument but 2 were given"
with self.assert_type_error_and_msg_in(msg):
AClassWithMetaclass.method_one_arg("standard")

def test_metaclass_method_too_many_args_does_not_suggest_missing_self(self):
"""A metaclass method with too many arguments should not suggest self."""
msg = "AMeta.method_two_arg() takes 2 positional arguments but 3 were given"
with self.assert_type_error_and_msg_in(msg):
AClassWithMetaclass.method_two_arg("trail", "river")

def test_metaclass_classmethod_does_not_suggest_missing_self(self):
"""A classmethod on a metaclass should not suggest instance self."""
msg = "AMeta.classmethod_one_arg() takes 1 positional argument but 2 were given"
with self.assert_type_error_and_msg_in(msg):
AClassWithMetaclass.classmethod_one_arg("standard")

def test_metaclass_staticmethod_does_not_suggest_missing_self(self):
"""A staticmethod on a metaclass should not suggest instance self."""
msg = "AMeta.static_one_arg() takes 1 positional argument but 2 were given"
with self.assert_type_error_and_msg_in(msg):
AClassWithMetaclass.static_one_arg("show", "working")

@cpython_only
class TestErrorMessagesSuggestions(unittest.TestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:exc:`TypeError` messages for some instance methods called with too many
positional arguments now suggest checking whether the method definition is
missing the ``self`` parameter.
61 changes: 57 additions & 4 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -1605,12 +1605,14 @@ missing_arguments(PyThreadState *tstate, PyCodeObject *co,
static void
too_many_positional(PyThreadState *tstate, PyCodeObject *co,
Py_ssize_t given, PyObject *defaults,
_PyStackRef *localsplus, PyObject *qualname)
_PyStackRef *localsplus, PyObject *qualname,
int should_suggest_missing_self)
{
int plural;
Py_ssize_t kwonly_given = 0;
Py_ssize_t i;
PyObject *sig, *kwonly_sig;
const char *self_hint = "";
Py_ssize_t co_argcount = co->co_argcount;

assert((co->co_flags & CO_VARARGS) == 0);
Expand Down Expand Up @@ -1648,18 +1650,57 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co,
kwonly_sig = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
assert(kwonly_sig != NULL);
}
if (should_suggest_missing_self) {
self_hint = ". Did you forget the 'self' parameter "
"in the function definition?";
}
_PyErr_Format(tstate, PyExc_TypeError,
"%U() takes %U positional argument%s but %zd%U %s given",
"%U() takes %U positional argument%s but %zd%U %s given%s",
qualname,
sig,
plural ? "s" : "",
given,
kwonly_sig,
given == 1 && !kwonly_given ? "was" : "were");
given == 1 && !kwonly_given ? "was" : "were",
self_hint
);
Py_DECREF(sig);
Py_DECREF(kwonly_sig);
}

static int
suggest_missing_self(PyFunctionObject *func, PyCodeObject *co,
PyObject *first_argument, Py_ssize_t argcount)
{
/* Missing self shows up as exactly one extra positional argument. */
if ((co->co_argcount + 1) != argcount) {
return 0;
}

if (first_argument == NULL || PyType_Check(first_argument)) {
/* When first arg is NULL, it's not really about self.
If it's a type object, then it's a classmethod. */
return 0;
}

if (co->co_argcount > 0) {
/* Don't confuse the user when they've already declared a
common convention of cls/self. */
PyObject *first_parameter_name = PyTuple_GET_ITEM(co->co_localsplusnames, 0);
/* If the receiver parameter is already declared, another hint would be misleading. */
if (PyUnicode_CompareWithASCIIString(first_parameter_name, "self") == 0 ||
PyUnicode_CompareWithASCIIString(first_parameter_name, "cls") == 0)
{
return 0;
}
}
/* If the current function matches on the type, it's likely worth adding the hint. */
PyTypeObject *self_cls = Py_TYPE(first_argument);
PyFunctionObject *possibly_current_function =
(PyFunctionObject *)_PyType_Lookup(self_cls, co->co_name);
return possibly_current_function == func;
}

static int
positional_only_passed_as_keyword(PyThreadState *tstate, PyCodeObject *co,
Py_ssize_t kwcount, PyObject* kwnames,
Expand Down Expand Up @@ -1734,6 +1775,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
/* Create a dictionary for keyword parameters (**kwags) */
PyObject *kwdict;
Py_ssize_t i;
PyObject *first_argument = NULL;
if (co->co_flags & CO_VARKEYWORDS) {
kwdict = PyDict_New();
if (kwdict == NULL) {
Expand All @@ -1750,6 +1792,14 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
kwdict = NULL;
}

/* Pin the first argument for the "missing self" hint: the surplus
argument cleanup below may close args[0] before the hint is computed.
The pin is only needed when the "too many positional arguments"
error is about to be raised. */
if (argcount > co->co_argcount && !(co->co_flags & CO_VARARGS)) {
first_argument = Py_NewRef(PyStackRef_AsPyObjectBorrow(args[0]));
}

/* Copy all positional arguments into local variables */
Py_ssize_t j, n;
if (argcount > co->co_argcount) {
Expand Down Expand Up @@ -1894,8 +1944,9 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,

/* Check the number of positional arguments */
if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) {
int missing_self_hint = suggest_missing_self(func, co, first_argument, argcount);
too_many_positional(tstate, co, argcount, func->func_defaults, localsplus,
func->func_qualname);
func->func_qualname, missing_self_hint);
goto fail_post_args;
}

Expand Down Expand Up @@ -1954,6 +2005,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
goto fail_post_args;
}
}
Py_XDECREF(first_argument);
return 0;

fail_pre_positional:
Expand All @@ -1970,6 +2022,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
}
/* fall through */
fail_post_args:
Py_XDECREF(first_argument);
return -1;
}

Expand Down
Loading