Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ac555bc
Implement `AttributeError_str()`
johnslavik Jul 16, 2026
4f22a16
Add tests
johnslavik Jul 16, 2026
b30d676
Check name too
johnslavik Jul 16, 2026
28e04d3
Add docs
johnslavik Jul 16, 2026
e5c4922
Add news entry
johnslavik Jul 16, 2026
3854563
Improve formatting
johnslavik Jul 16, 2026
1e88331
Simplify?
johnslavik Jul 16, 2026
2fd216a
Cooler formatting of the news entry
johnslavik Jul 16, 2026
356f56e
Test custom messages aren't overwritten
johnslavik Jul 16, 2026
8db0394
Fix news problem
johnslavik Jul 16, 2026
ebcc795
Shorter test name
johnslavik Jul 16, 2026
b6fda10
Fix inconsistent style
johnslavik Jul 16, 2026
d624835
Make thread-safe, presumably
johnslavik Jul 16, 2026
c50a356
Rarest case goes last
johnslavik Jul 16, 2026
7ad5204
} else {
johnslavik Jul 16, 2026
b65a7fd
Remove unnecessary whitespace
johnslavik Jul 16, 2026
e99d875
Fix style
johnslavik Jul 16, 2026
8c29de3
Avoid nesting critical sections!
johnslavik Jul 16, 2026
6faf358
Fix error handling, crete strong refs for interpolation
johnslavik Jul 16, 2026
086a0c9
Multiline if properly done
johnslavik Jul 16, 2026
e5bb17e
Fix small text mistake in tests
johnslavik Jul 16, 2026
f2507a5
Fix tests to actually validate custom message
johnslavik Jul 17, 2026
9c81069
Shorten docs
johnslavik Jul 17, 2026
e905ab2
Truncate `AttributeError.obj` representation to 200 chars
johnslavik Jul 17, 2026
7d720ac
Remove extra empty line
johnslavik Jul 17, 2026
337cef4
Add "re-acquires lock" comment
johnslavik Jul 17, 2026
6aa47db
I desperately need a formatter
johnslavik Jul 17, 2026
4ff6a48
Missing5.
johnslavik Jul 17, 2026
e00db5f
.
johnslavik Jul 17, 2026
b5ce3c9
Use `_PyUnicode_Equal`
johnslavik Jul 17, 2026
da8c723
Use typename or module name
johnslavik Jul 17, 2026
c941657
Rearrange tests
johnslavik Jul 17, 2026
9ea6c8a
Cover nameless module branch
johnslavik Jul 17, 2026
f825f27
Clearer variable names
johnslavik Jul 17, 2026
7b73fb8
Localize tests visually
johnslavik Jul 17, 2026
7378e30
Use `%T`
johnslavik Jul 17, 2026
faf6e53
Revert "Use `%T`"
johnslavik Jul 17, 2026
a86fde5
Use '%T'
johnslavik Jul 18, 2026
9b00321
Test for fully qualified names
johnslavik Jul 18, 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
2 changes: 2 additions & 0 deletions Doc/library/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change surprised me but it's correct.

This addition surprised me since the PR doesn't change that. The change documents that _PyObject_SetAttributeErrorContext() is called by many functions such as PyObject_GetAttr() to set name and obj attributes of an AttributeError.


.. versionchanged:: 3.10

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We dropped the .. versionchanged:: next note that documented the generated message. This is a user-visible change to str(), so I think we should keep a versionchanged here, no?

Added the :attr:`name` and :attr:`obj` attributes.

Expand Down
61 changes: 61 additions & 0 deletions Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2047,6 +2048,66 @@ def blech(self):
self.assertEqual("bluch", exc.name)
self.assertEqual(obj, exc.obj)

def test_getattr_error_message(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check also obj and name attributes of AttributeError:

Details
    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.assertIs(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.assertIs(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.assertIs(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.assertIs(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.assertIs(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.assertIs(cm.exception.name, "missing4")

        nameless_mod = ModuleType.__new__(ModuleType)
        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.assertIs(cm.exception.name, "missing4")

def fqn(type):
return f'{type.__module__}.{type.__qualname__}'

class RaiseWithName:
def __getattr__(self, name):
raise AttributeError(name)
with self.assertRaises(AttributeError) as cm:
getattr(RaiseWithName(), "missing1")
self.assertEqual(str(cm.exception),
f"'{fqn(RaiseWithName)}' object has no attribute 'missing1'")

class BareRaise:
def __getattr__(self, name):
raise AttributeError
with self.assertRaises(AttributeError) as cm:
getattr(BareRaise(), "missing2")
self.assertEqual(str(cm.exception),
f"'{fqn(BareRaise)}' object has no attribute 'missing2'")

class RaiseCustom:
def __getattr__(self, name):
raise AttributeError("custom")
with self.assertRaises(AttributeError) as cm:
getattr(RaiseCustom(), "missing3")
self.assertEqual(str(cm.exception), "custom")

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'")

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'")

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")

nameless_mod = ModuleType.__new__(ModuleType)
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'")

# Note: name suggestion tests live in `test_traceback`.


Expand Down
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +1 to +2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
: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.
46 changes: 45 additions & 1 deletion Objects/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#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"
Expand Down Expand Up @@ -2702,6 +2703,49 @@ 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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike the arg = ... assignment in the middle of the big if condition, but you can keep it if you like it.

Suggested change
PyObject *arg, *obj = NULL, *name;
PyObject *arg; // borrowed ref
PyObject *obj = NULL, *name = NULL;


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)
Comment on lines +2715 to +2718

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a comment explaining why you use exc.name only if it's equal to exc.args[0] or if exc.args is empty? IMO it's a good thing to implement this change, but it wasn't obvious the first time that I read this code.

) {
obj = Py_NewRef(self->obj);
name = Py_NewRef(self->name);
Comment thread
serhiy-storchaka marked this conversation as resolved.
}
Py_END_CRITICAL_SECTION();

if (!obj) {
return BaseException_str(op); /* re-acquires lock */
}

PyObject *result;
if (PyModule_Check(obj)) {
PyModuleObject *mod = _PyModule_CAST(obj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we use _PyModule_CAST and md_name here but only get pycore_moduleobject.h transitively through pycore_object.h. Can we include it explicitly like the other headers?

/* In a typical case, module's __name__ is examined instead. */
if (mod->md_name) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses cached md_name, so assigning module.__name__ makes the generated message report the old name. We should read the current module name here.

result = PyUnicode_FromFormat("module '%U' has no attribute '%U'",
mod->md_name,
name);
Comment on lines +2735 to +2736

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mod->md_name,
name);
mod->md_name, name);

} else {
result = PyUnicode_FromFormat("module has no attribute '%U'", name);
}
} else {
result = PyUnicode_FromFormat("'%T' object has no attribute '%U'",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formats class objects as instances of their metaclass. Can we preserve the standard type object 'C' has no attribute ... form here?

obj, name);
}
Py_DECREF(obj);
Py_DECREF(name);
return result;
}

static int
AttributeError_traverse(PyObject *op, visitproc visit, void *arg)
{
Expand Down Expand Up @@ -2770,7 +2814,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
Expand Down
Loading