https://github.com/python/cpython/commit/ae72d0c92bf57448e256cdf8cfac7d4fd345b1a4
commit: ae72d0c92bf57448e256cdf8cfac7d4fd345b1a4
branch: main
author: Bartosz SÅ‚awecki <[email protected]>
committer: vstinner <[email protected]>
date: 2026-07-24T14:14:47+02:00
summary:

gh-153785: Generate `AttributeError` messages from context (#153786)

Co-authored-by: Victor Stinner <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst
M Doc/library/exceptions.rst
M Lib/test/test_exceptions.py
M Objects/exceptions.c

diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst
index 3775d5ac81a273..ecf62fb6391b1b 100644
--- a/Doc/library/exceptions.rst
+++ b/Doc/library/exceptions.rst
@@ -215,6 +215,8 @@ 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.
+
    .. versionchanged:: 3.10
       Added the :attr:`name` and :attr:`obj` attributes.
 
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index ee4888c18598f3..0cee756958f3ad 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,
@@ -2054,6 +2055,129 @@ def blech(self):
             self.assertEqual("bluch", exc.name)
             self.assertEqual(obj, exc.obj)
 
+    def test_getattr_error_message(self):
+        def fqn(type):
+            return f'{type.__module__}.{type.__qualname__}'
+
+        class RaiseWithName:
+            def __getattr__(self, name):
+                raise AttributeError(name)
+        obj = RaiseWithName()
+        with self.assertRaises(AttributeError) as cm:
+            getattr(obj, "missing1")
+        self.assertEqual(str(cm.exception),
+                         f"'{fqn(RaiseWithName)}' object has no attribute 
'missing1'")
+        self.assertIs(cm.exception.obj, obj)
+        self.assertEqual(cm.exception.name, "missing1")
+
+        class BareRaise:
+            def __getattr__(self, name):
+                raise AttributeError
+        obj = BareRaise()
+        with self.assertRaises(AttributeError) as cm:
+            getattr(obj, "missing2")
+        self.assertEqual(str(cm.exception),
+                         f"'{fqn(BareRaise)}' object has no attribute 
'missing2'")
+        self.assertIs(cm.exception.obj, obj)
+        self.assertEqual(cm.exception.name, "missing2")
+
+        class RaiseCustom:
+            def __getattr__(self, name):
+                raise AttributeError("custom")
+        obj = RaiseCustom()
+        with self.assertRaises(AttributeError) as cm:
+            getattr(obj, "missing3")
+        self.assertEqual(str(cm.exception), "custom")
+        self.assertIs(cm.exception.obj, obj)
+        self.assertEqual(cm.exception.name, "missing3")
+
+    def test_class_getattr_error_message(self):
+        def fqn(type):
+            return f'{type.__module__}.{type.__qualname__}'
+
+        class MetaclassRaiseWithName(type):
+            def __getattr__(self, name):
+                raise AttributeError(name)
+        cls = MetaclassRaiseWithName("spam", (), {})
+        with self.assertRaises(AttributeError) as cm:
+            getattr(cls, "missing1")
+        self.assertEqual(str(cm.exception),
+                         f"type object '{fqn(cls)}' has no attribute 
'missing1'")
+        self.assertIs(cm.exception.obj, cls)
+        self.assertEqual(cm.exception.name, "missing1")
+
+        class MetaclassBareRaise(type):
+            def __getattr__(self, name):
+                raise AttributeError
+        cls = MetaclassBareRaise("eggs", (), {})
+        with self.assertRaises(AttributeError) as cm:
+            getattr(cls, "missing2")
+        self.assertEqual(str(cm.exception),
+                         f"type object '{fqn(cls)}' has no attribute 
'missing2'")
+        self.assertIs(cm.exception.obj, cls)
+        self.assertEqual(cm.exception.name, "missing2")
+
+        class MetaclassRaiseCustom(type):
+            def __getattr__(self, name):
+                raise AttributeError("custom")
+        cls = MetaclassRaiseCustom("ham", (), {})
+        with self.assertRaises(AttributeError) as cm:
+            getattr(cls, "missing3")
+        self.assertEqual(str(cm.exception), "custom")
+        self.assertIs(cm.exception.obj, cls)
+        self.assertEqual(cm.exception.name, "missing3")
+
+    def test_module_getattr_error_message(self):
+        raisewithname_mod = ModuleType("raisewithname")
+        def raise_with_name(name):
+            raise AttributeError(name)
+        raisewithname_mod.__getattr__ = raise_with_name
+        with self.assertRaises(AttributeError) as cm:
+            getattr(raisewithname_mod, "missing1")
+        self.assertEqual(str(cm.exception),
+                         "module 'raisewithname' has no attribute 'missing1'")
+        self.assertIs(cm.exception.obj, raisewithname_mod)
+        self.assertEqual(cm.exception.name, "missing1")
+
+        bareraise_mod = ModuleType("bareraise")
+        def bare_raise(name):
+            raise AttributeError
+        bareraise_mod.__getattr__ = bare_raise
+        with self.assertRaises(AttributeError) as cm:
+            getattr(bareraise_mod, "missing2")
+        self.assertEqual(str(cm.exception),
+                         "module 'bareraise' has no attribute 'missing2'")
+        self.assertIs(cm.exception.obj, bareraise_mod)
+        self.assertEqual(cm.exception.name, "missing2")
+
+        custom_mod = ModuleType("custom")
+        def raise_custom(name):
+            raise AttributeError("custom")
+        custom_mod.__getattr__ = raise_custom
+        with self.assertRaises(AttributeError) as cm:
+            getattr(custom_mod, "missing3")
+        self.assertEqual(str(cm.exception), "custom")
+        self.assertIs(cm.exception.obj, custom_mod)
+        self.assertEqual(cm.exception.name, "missing3")
+
+        nameless_mod = ModuleType("forgettable")
+        del nameless_mod.__dict__["__name__"]
+        nameless_mod.__getattr__ = raise_with_name
+        with self.assertRaises(AttributeError) as cm:
+            getattr(nameless_mod, "missing4")
+        self.assertEqual(str(cm.exception), "module has no attribute 
'missing4'")
+        self.assertIs(cm.exception.obj, nameless_mod)
+        self.assertEqual(cm.exception.name, "missing4")
+
+        nameless_mod = ModuleType("broken")
+        nameless_mod.__dict__["__name__"] = 10j
+        nameless_mod.__getattr__ = raise_with_name
+        with self.assertRaises(AttributeError) as cm:
+            getattr(nameless_mod, "missing4")
+        self.assertEqual(str(cm.exception), "module has no attribute 
'missing4'")
+        self.assertIs(cm.exception.obj, nameless_mod)
+        self.assertEqual(cm.exception.name, "missing4")
+
     # 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 00000000000000..40981acb37bbd7
--- /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 @@
+:exc:`AttributeError`: The default error message is now generated from ``name``
+and ``obj`` attributes 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 149595e64cec14..fb546ad2673576 100644
--- a/Objects/exceptions.c
+++ b/Objects/exceptions.c
@@ -11,9 +11,11 @@
 #include "pycore_exceptions.h"    // struct _Py_exc_state
 #include "pycore_initconfig.h"
 #include "pycore_modsupport.h"    // _PyArg_NoKeywords()
+#include "pycore_moduleobject.h"  // _PyModule_CAST()
 #include "pycore_object.h"
 #include "pycore_pyerrors.h"      // struct _PyErr_SetRaisedException
 #include "pycore_tuple.h"         // _PyTuple_FromPair
+#include "pycore_unicodeobject.h" // _PyUnicode_Equal()
 
 #include "osdefs.h"               // SEP
 #include "clinic/exceptions.c.h"
@@ -2702,6 +2704,65 @@ AttributeError_dealloc(PyObject *self)
     Py_TYPE(self)->tp_free(self);
 }
 
+static PyObject *
+AttributeError_str(PyObject *op)
+{
+    PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op);
+    PyObject *arg;  // borrowed ref
+    PyObject *obj = NULL, *name = NULL;
+
+     /* .name and .obj are set automatically when attribute lookup fails, so
+        synthesize a more informative message from them when the caller
+        didn't supply a meaningful one of their own -- that is, when args is
+        empty, or contains only the attribute name.  Otherwise, use the
+        message the caller gave. */
+
+    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) {
+        assert(!name);
+        return BaseException_str(op);  /* re-acquires lock */
+    }
+
+    PyObject *result = NULL;
+    if (PyModule_Check(obj)) {
+        PyModuleObject *mod = _PyModule_CAST(obj);
+        PyObject *modname;
+        if (PyDict_GetItemRef(mod->md_dict, &_Py_ID(__name__), &modname) < 0) {
+            goto done;
+        }
+        if (modname && PyUnicode_Check(modname)) {
+            result = PyUnicode_FromFormat("module %R has no attribute %R",
+                                          modname, name);
+            Py_DECREF(modname);
+        } else {
+            Py_XDECREF(modname);
+            result = PyUnicode_FromFormat("module has no attribute %R", name);
+        }
+    } else if (PyType_Check(obj)) {
+        result = PyUnicode_FromFormat("type object '%N' has no attribute %R",
+                                      obj, name);
+    } else {
+        result = PyUnicode_FromFormat("'%T' object has no attribute %R",
+                                      obj, name);
+    }
+done:
+    Py_DECREF(obj);
+    Py_DECREF(name);
+    return result;
+}
+
 static int
 AttributeError_traverse(PyObject *op, visitproc visit, void *arg)
 {
@@ -2770,7 +2831,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

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to