Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
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 @@ -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
===========
Expand Down
148 changes: 133 additions & 15 deletions Lib/test/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,51 +898,169 @@ 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 positional_only(arg, /):
def wag_tail(times):
pass

@staticmethod
def fetch(toy, /):
pass

def bark_with_self(self, sound):
pass

def bark_missing_self(sound):
pass

def wag_missing_self():
pass

def bark_with_self_arg(sound, self):
pass

def sit(self):
pass

@classmethod
def register_breed(dog_type):
pass

class AnimalMeta(type):
def register_pack(dog_type):
pass

def track_pack(cls, scent):
pass

@classmethod
def classify_pack(dog_type):
pass

@staticmethod
def tag_pack(label):
pass

class Poodle(metaclass=AnimalMeta):
pass

@cpython_only
class TestErrorMessagesUseQualifiedName(unittest.TestCase):

@contextlib.contextmanager
def check_raises_type_error(self, message):
with self.assertRaises(TypeError) as cm:
yield
self.assertEqual(str(cm.exception), message)

def test_too_many_positional_with_self_does_not_suggest_missing_self(self):
"""A regular method with self should keep the normal too-many-args error."""
msg = "Dog.bark_with_self() takes 2 positional arguments but 3 were given"
with self.check_raises_type_error(msg):
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 = "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):
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 = "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):
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):
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 = "Dog.bark_with_self_arg() missing 1 required positional argument: 'self'"
with self.check_raises_type_error(msg):
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 = "Dog.sit() missing 1 required positional argument: 'self'"
with self.check_raises_type_error(msg):
Dog.sit()

def test_classmethod_missing_cls_does_not_suggest_missing_self(self):
"""A classmethod missing cls conceptually should not suggest self."""
msg = "Dog.register_breed() takes 1 positional argument but 2 were given"
with self.check_raises_type_error(msg):
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 = "Dog.register_breed() takes 1 positional argument but 2 were given"
with self.check_raises_type_error(msg):
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 = "Dog.wag_tail() takes 1 positional argument but 2 were given"
with self.check_raises_type_error(msg):
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 = "Dog.wag_tail() takes 1 positional argument but 2 were given"
with self.check_raises_type_error(msg):
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 = "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 = "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 = "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 = "AnimalMeta.tag_pack() takes 1 positional argument but 2 were given"
with self.check_raises_type_error(msg):
A().method_two_args("x", "y", x="oops")
Poodle.tag_pack("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.
60 changes: 56 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,56 @@ 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 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 ||
PyUnicode_CompareWithASCIIString(first_parameter_name, "cls") == 0)
{
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);
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 +1774,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 +1791,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 +1943,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 +2004,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
goto fail_post_args;
}
}
Py_XDECREF(first_argument);
return 0;

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

Expand Down
Loading