diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 3775d5ac81a2736..59ba5ca49b491f0 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -215,9 +215,18 @@ The following exceptions are the exceptions that are usually raised. The object that was accessed for the named attribute. + When possible, :attr:`name` and :attr:`obj` are set automatically + for failed attribute lookups. + .. versionchanged:: 3.10 Added the :attr:`name` and :attr:`obj` attributes. + .. versionchanged:: next + The default error message is now generated from :attr:`name` and + :attr:`obj` when both attributes are set and the exception was + constructed with no positional arguments, or with a single positional + argument equal to :attr:`name`. + .. exception:: EOFError Raised when the :func:`input` function hits an end-of-file condition (EOF) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index cc7faef93e1af7c..94b69a9a494551a 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -10,6 +10,7 @@ from codecs import BOM_UTF8 from itertools import product from textwrap import dedent +from types import ModuleType from test.support import (captured_stderr, check_impl_detail, cpython_only, gc_collect, @@ -2047,6 +2048,49 @@ def blech(self): self.assertEqual("bluch", exc.name) self.assertEqual(obj, exc.obj) + def _test_generated_message(self, obj, name): + with self.assertRaises(AttributeError) as cm: + getattr(obj, name) + self.assertEqual(str(cm.exception), + f"{obj!r} has no attribute {name!r}") + + def test_getattr_error_message(self): + class RaiseWithName: + def __getattr__(self, name): + raise AttributeError(name) + + class BareRaise: + def __getattr__(self, name): + raise AttributeError + + class RaiseCustom: + def __getattr__(self, name): + raise AttributeError("custom") + + self._test_generated_message(RaiseWithName(), "missing1") + self._test_generated_message(BareRaise(), "missing2") + self._test_generated_message(RaiseCustom(), "custom") + + def test_module_getattr_error_message(self): + mod1 = ModuleType("raisewithname") + def raise_with_name(name): + raise AttributeError(name) + mod1.__getattr__ = raise_with_name + + mod2 = ModuleType("bareraise") + def bare_raise(name): + raise AttributeError + mod2.__getattr__ = bare_raise + + mod3 = ModuleType("custom") + def raise_custom(name): + raise AttributeError("custom") + mod3.__getattr__ = raise_custom + + self._test_generated_message(mod1, "missing3") + self._test_generated_message(mod2, "missing4") + self._test_generated_message(mod3, "custom") + # Note: name suggestion tests live in `test_traceback`. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst new file mode 100644 index 000000000000000..2852419c6d6e8a7 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst @@ -0,0 +1,4 @@ +The default error message is now generated from ``name`` and ``obj`` attributes +of :exc:`AttributeError` when both are set and the exception was constructed with +no positional arguments, or with a single positional argument equal to ``name``. +Patch by Bartosz Sławecki. diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 149595e64cec144..c460f109c5d2881 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2702,6 +2702,36 @@ AttributeError_dealloc(PyObject *self) Py_TYPE(self)->tp_free(self); } +static PyObject * +AttributeError_str(PyObject *op) +{ + PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); + PyObject *arg, *obj = NULL, *name; + + Py_BEGIN_CRITICAL_SECTION(self); + if ( + self->obj && self->name && PyUnicode_Check(self->name) + && ((PyTuple_GET_SIZE(self->args) == 1 + && PyUnicode_Check(arg = PyTuple_GET_ITEM(self->args, 0)) + && PyUnicode_Equal(arg, self->name)) + || PyTuple_GET_SIZE(self->args) == 0) + ) { + obj = Py_NewRef(self->obj); + name = Py_NewRef(self->name); + } + Py_END_CRITICAL_SECTION(); + + if (!obj) { + return BaseException_str(op); + } + + PyObject *result = PyUnicode_FromFormat("%R has no attribute '%U'", + obj, name); + Py_DECREF(obj); + Py_DECREF(name); + return result; +} + static int AttributeError_traverse(PyObject *op, visitproc visit, void *arg) { @@ -2770,7 +2800,7 @@ static PyMethodDef AttributeError_methods[] = { ComplexExtendsException(PyExc_Exception, AttributeError, AttributeError, 0, AttributeError_methods, AttributeError_members, - 0, BaseException_str, 0, "Attribute not found."); + 0, AttributeError_str, 0, "Attribute not found."); /* * SyntaxError extends Exception