From 944c632cc41196c26be3f2e445584cb55ab88089 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 13 Jan 2026 19:53:15 +0100 Subject: [PATCH 1/3] Add helpful default error messages to `AttributeError` and `NameError` --- Lib/test/test_exceptions.py | 37 +++++++++++++++++++ Lib/test/test_traceback.py | 14 +++++++ Lib/traceback.py | 10 ++++- ...-01-13-19-51-37.gh-issue-143811.PLHsyK.rst | 3 ++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-01-13-19-51-37.gh-issue-143811.PLHsyK.rst diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 6f212d2f91efb1..a57a31b4269008 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2003,6 +2003,25 @@ class TestClass: self.assertRaises(NameError, f) + def test_gh_143811(self): + def f(): + span = 42 + try: + spam + except NameError as exc: + # Clear the message. + exc.args = () + raise + + try: + f() + except NameError: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + # 'spam' should appear even when message was empty. + self.assertIn("'spam'", err.getvalue()) + # Note: name suggestion tests live in `test_traceback`. @@ -2046,6 +2065,24 @@ def blech(self): self.assertEqual("bluch", exc.name) self.assertEqual(obj, exc.obj) + def test_gh_143811(self): + def f(): + class A: + def __getattr__(self, attr): + # Provide no message. + raise AttributeError + + A.bluch + + try: + f() + except AttributeError: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + # 'bluch' should appear even when message was empty. + self.assertIn("'bluch'", err.getvalue()) + # Note: name suggestion tests live in `test_traceback`. diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 96510eeec54640..a322ad3e09e386 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4259,6 +4259,7 @@ def __getattr__(self, attr): raise AttributeError() actual = self.get_suggestion(A(), 'bluch') + self.assertIn("'A' object has no attribute 'bluch'.", actual) self.assertIn("blech", actual) class A: @@ -4267,6 +4268,7 @@ def __getattr__(self, attr): raise AttributeError actual = self.get_suggestion(A(), 'bluch') + self.assertIn("'A' object has no attribute 'bluch'.", actual) self.assertIn("blech", actual) def test_suggestions_invalid_args(self): @@ -4930,6 +4932,18 @@ def func(): actual = self.get_suggestion(func) self.assertIn("forget to import '_io'", actual) + def test_name_error_empty(self): + """See GH-143811.""" + def func(): + span = 42 + try: + spam + except NameError as exc: + exc.args = () + raise + actual = self.get_suggestion(func) + self.assertIn("'spam'", actual) + self.assertIn("'span'?", actual) class PurePythonSuggestionFormattingTests( diff --git a/Lib/traceback.py b/Lib/traceback.py index f95d6bdbd016ac..cc5b7e8452afe1 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1125,9 +1125,15 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, + "add the site-packages directory to sys.path " + "or to enable your virtual environment?") elif exc_type and issubclass(exc_type, (NameError, AttributeError)) and \ - getattr(exc_value, "name", None) is not None: - wrong_name = getattr(exc_value, "name", None) + (wrong_name := getattr(exc_value, "name", None)) is not None: suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) + if not self._str: + if issubclass(exc_type, AttributeError): + if (obj_type := type(getattr(exc_value, "obj", None))) is not type(None): + obj_type_name = object.__getattribute__(obj_type, "__name__") + self._str = f"{obj_type_name!r} object has no attribute {wrong_name!r}" + else: # NameError + self._str = repr(wrong_name) if suggestion: self._str += f". Did you mean: '{suggestion}'?" if issubclass(exc_type, NameError): diff --git a/Misc/NEWS.d/next/Library/2026-01-13-19-51-37.gh-issue-143811.PLHsyK.rst b/Misc/NEWS.d/next/Library/2026-01-13-19-51-37.gh-issue-143811.PLHsyK.rst new file mode 100644 index 00000000000000..1854597be17c14 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-13-19-51-37.gh-issue-143811.PLHsyK.rst @@ -0,0 +1,3 @@ +:exc:`AttributeError` and :exc:`NameError` exceptions raised with no message +now get helpful default messages when displaying traceback. Patch by Bartosz +Sławecki. From 0bebeec5cd6c79b3981d436934b8f14f86df40d7 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 13 Jan 2026 20:16:29 +0100 Subject: [PATCH 2/3] Fix `AttributeError` test in `test_exceptions` --- Lib/test/test_exceptions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index a57a31b4269008..2374eb33bdd596 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2072,7 +2072,7 @@ def __getattr__(self, attr): # Provide no message. raise AttributeError - A.bluch + A().bluch try: f() @@ -2080,8 +2080,8 @@ def __getattr__(self, attr): with support.captured_stderr() as err: sys.__excepthook__(*sys.exc_info()) - # 'bluch' should appear even when message was empty. - self.assertIn("'bluch'", err.getvalue()) + # Should appear even when no message was provided. + self.assertIn("'A' object has no attribute 'bluch'", err.getvalue()) # Note: name suggestion tests live in `test_traceback`. From 530c2e8ace5b153ec953a122df3d68f67a98f8ff Mon Sep 17 00:00:00 2001 From: johnslavik Date: Wed, 14 Jan 2026 14:26:01 +0100 Subject: [PATCH 3/3] Change default message for `NameError` to "name '...' is not defined" Co-authored-by: Aaron Wieczorek --- Lib/test/test_exceptions.py | 2 +- Lib/test/test_traceback.py | 4 ++-- Lib/traceback.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 2374eb33bdd596..05ff5ac8653c2f 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2020,7 +2020,7 @@ def f(): sys.__excepthook__(*sys.exc_info()) # 'spam' should appear even when message was empty. - self.assertIn("'spam'", err.getvalue()) + self.assertIn("name 'spam' is not defined", err.getvalue()) # Note: name suggestion tests live in `test_traceback`. diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index a322ad3e09e386..f36bf532cb9f95 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4942,8 +4942,8 @@ def func(): exc.args = () raise actual = self.get_suggestion(func) - self.assertIn("'spam'", actual) - self.assertIn("'span'?", actual) + self.assertIn("name 'spam' is not defined", actual) + self.assertIn("Did you mean: 'span'?", actual) class PurePythonSuggestionFormattingTests( diff --git a/Lib/traceback.py b/Lib/traceback.py index cc5b7e8452afe1..404c47090bdcf9 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1133,7 +1133,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, obj_type_name = object.__getattribute__(obj_type, "__name__") self._str = f"{obj_type_name!r} object has no attribute {wrong_name!r}" else: # NameError - self._str = repr(wrong_name) + self._str = f"name {wrong_name!r} is not defined" if suggestion: self._str += f". Did you mean: '{suggestion}'?" if issubclass(exc_type, NameError):