From ac555bc90352e6da424129a8a51f43545c81731c Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 02:15:55 +0200 Subject: [PATCH 01/37] Implement `AttributeError_str()` --- Objects/exceptions.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 149595e64cec144..0833791d5c707aa 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2702,6 +2702,23 @@ AttributeError_dealloc(PyObject *self) Py_TYPE(self)->tp_free(self); } +static PyObject * +AttributeError_str(PyObject *op) +{ + PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); + if ( + self->obj + && (!PyTuple_GET_SIZE(self->args) + || (PyTuple_GET_SIZE(self->args) == 1 + && PyUnicode_Equal(PyTuple_GET_ITEM(self->args, 0), self->name)) + ) + ) { + return PyUnicode_FromFormat("%R has no attribute '%U'", + self->obj, self->name); + } + return BaseException_str(op); +} + static int AttributeError_traverse(PyObject *op, visitproc visit, void *arg) { @@ -2770,7 +2787,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 From 4f22a162a15bc7cab7892b2dd96d8eb611128e08 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 02:34:00 +0200 Subject: [PATCH 02/37] Add tests --- Lib/test/test_exceptions.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index cc7faef93e1af7c..36a6d718f06ece0 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,38 @@ 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 + + self._test_generated_message(RaiseWithName(), "missing1") + self._test_generated_message(BareRaise(), "missing2") + + def test_module_getattr_attribute_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 + + self._test_generated_message(mod1, "missing3") + self._test_generated_message(mod2, "missing4") + # Note: name suggestion tests live in `test_traceback`. From b30d6768cb96bc35f4d8e4d1983e91fec51c09b1 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 02:41:04 +0200 Subject: [PATCH 03/37] Check name too --- Objects/exceptions.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 0833791d5c707aa..6fa2c2f8f149ec0 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2708,6 +2708,7 @@ AttributeError_str(PyObject *op) PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); if ( self->obj + && self->name && (!PyTuple_GET_SIZE(self->args) || (PyTuple_GET_SIZE(self->args) == 1 && PyUnicode_Equal(PyTuple_GET_ITEM(self->args, 0), self->name)) From 28e04d3e4213189e44ba539df01aab57ccdbb1fb Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 02:48:03 +0200 Subject: [PATCH 04/37] Add docs --- Doc/library/exceptions.rst | 9 +++++++++ 1 file changed, 9 insertions(+) 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) From e5c49220c73dcf84efed95ee15fded200e9c1e97 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 02:53:16 +0200 Subject: [PATCH 05/37] Add news entry --- .../2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst 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..e3bcea972ca4cbd --- /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 :attr:`name`. Patch by Bartosz Sławecki. From 38545635df41c999dd9d0ef64ab33ca074a0f2ff Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 02:55:13 +0200 Subject: [PATCH 06/37] Improve formatting --- Objects/exceptions.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 6fa2c2f8f149ec0..d3f4eba3ed9a442 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2710,8 +2710,8 @@ AttributeError_str(PyObject *op) self->obj && self->name && (!PyTuple_GET_SIZE(self->args) - || (PyTuple_GET_SIZE(self->args) == 1 - && PyUnicode_Equal(PyTuple_GET_ITEM(self->args, 0), self->name)) + || (PyTuple_GET_SIZE(self->args) == 1 + && PyUnicode_Equal(PyTuple_GET_ITEM(self->args, 0), self->name)) ) ) { return PyUnicode_FromFormat("%R has no attribute '%U'", From 1e88331004cdbb0aa90e9e01233adaa55787880d Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 03:01:05 +0200 Subject: [PATCH 07/37] Simplify? --- Objects/exceptions.c | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index d3f4eba3ed9a442..dd55642130c46fc 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2706,18 +2706,14 @@ static PyObject * AttributeError_str(PyObject *op) { PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); - if ( - self->obj - && self->name - && (!PyTuple_GET_SIZE(self->args) - || (PyTuple_GET_SIZE(self->args) == 1 - && PyUnicode_Equal(PyTuple_GET_ITEM(self->args, 0), self->name)) - ) - ) { - return PyUnicode_FromFormat("%R has no attribute '%U'", - self->obj, self->name); + if (!self->obj || !self->name || PyTuple_GET_SIZE(self->args) > 1 + || (PyTuple_GET_SIZE(self->args) == 1 && + !PyUnicode_Equal(PyTuple_GET_ITEM(self->args, 0), self->name))) + { + return BaseException_str(op); } - return BaseException_str(op); + return PyUnicode_FromFormat("%R has no attribute '%U'", + self->obj, self->name); } static int From 2fd216a0255da824db4c4e91d2f8cb562bd29961 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 03:06:59 +0200 Subject: [PATCH 08/37] Cooler formatting of the news entry --- .../2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index e3bcea972ca4cbd..278bc2ac64abeba 100644 --- 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 @@ -1,4 +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 :attr:`name`. Patch by Bartosz Sławecki. +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 :attr:`name`. +Patch by Bartosz Sławecki. From 356f56eeac4fdd2dc5724541ad1e70fc0a711ee5 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 03:13:38 +0200 Subject: [PATCH 09/37] Test custom messages aren't overwritten --- Lib/test/test_exceptions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 36a6d718f06ece0..7d08369549206e6 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2063,8 +2063,13 @@ 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_attribute_error_message(self): mod1 = ModuleType("raisewithname") @@ -2077,8 +2082,14 @@ def bare_raise(name): raise AttributeError mod2.__getattr__ = bare_raise + mod3 = ModuleType("bareraise") + 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`. From 8db03948be57346ea359b79dccbdb86165f02c2c Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 03:17:49 +0200 Subject: [PATCH 10/37] Fix news problem --- .../2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 278bc2ac64abeba..2852419c6d6e8a7 100644 --- 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 @@ -1,4 +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 :attr:`name`. +no positional arguments, or with a single positional argument equal to ``name``. Patch by Bartosz Sławecki. From ebcc7958b59acdbb751a9c777d7b071aa7e756f1 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 03:20:19 +0200 Subject: [PATCH 11/37] Shorter test name --- Lib/test/test_exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 7d08369549206e6..5c04a02adf29f59 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2071,7 +2071,7 @@ def __getattr__(self, name): self._test_generated_message(BareRaise(), "missing2") self._test_generated_message(RaiseCustom(), "custom") - def test_module_getattr_attribute_error_message(self): + def test_module_getattr_error_message(self): mod1 = ModuleType("raisewithname") def raise_with_name(name): raise AttributeError(name) From b6fda10e9d3b1084b5f3ba6b2f5140448c6644f8 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 03:21:18 +0200 Subject: [PATCH 12/37] Fix inconsistent style --- Objects/exceptions.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index dd55642130c46fc..50056635d0afdbe 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2707,8 +2707,8 @@ AttributeError_str(PyObject *op) { PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); if (!self->obj || !self->name || PyTuple_GET_SIZE(self->args) > 1 - || (PyTuple_GET_SIZE(self->args) == 1 && - !PyUnicode_Equal(PyTuple_GET_ITEM(self->args, 0), self->name))) + || (PyTuple_GET_SIZE(self->args) == 1 + && !PyUnicode_Equal(PyTuple_GET_ITEM(self->args, 0), self->name))) { return BaseException_str(op); } From d624835c7f72a6fcd1a61a43bec957d2c6eff51d Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 04:20:38 +0200 Subject: [PATCH 13/37] Make thread-safe, presumably --- Objects/exceptions.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 50056635d0afdbe..aff5c19c578b52c 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2706,14 +2706,24 @@ static PyObject * AttributeError_str(PyObject *op) { PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); - if (!self->obj || !self->name || PyTuple_GET_SIZE(self->args) > 1 + PyObject *ret, *arg; + Py_BEGIN_CRITICAL_SECTION(self); + if ( + !self->obj || !self->name || !PyUnicode_Check(self->name) + || PyTuple_GET_SIZE(self->args) > 1 || (PyTuple_GET_SIZE(self->args) == 1 - && !PyUnicode_Equal(PyTuple_GET_ITEM(self->args, 0), self->name))) - { - return BaseException_str(op); + && PyUnicode_Check(arg = PyTuple_GET_ITEM(self->args, 0)) + && !PyUnicode_Equal(arg, self->name)) + ) { + ret = BaseException_str(op); + } + else { + ret = PyUnicode_FromFormat("%R has no attribute '%U'", + self->obj, self->name); } - return PyUnicode_FromFormat("%R has no attribute '%U'", - self->obj, self->name); + Py_END_CRITICAL_SECTION(); + return ret; + } static int From c50a356e4e3cff2363cb130d6eec20dd68aee6c4 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 04:23:31 +0200 Subject: [PATCH 14/37] Rarest case goes last --- Objects/exceptions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index aff5c19c578b52c..a339d848a8a2ade 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2710,10 +2710,10 @@ AttributeError_str(PyObject *op) Py_BEGIN_CRITICAL_SECTION(self); if ( !self->obj || !self->name || !PyUnicode_Check(self->name) - || PyTuple_GET_SIZE(self->args) > 1 || (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 ) { ret = BaseException_str(op); } From 7ad5204f0e29ca83fdc87516c693bd6c3f87e926 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 04:24:10 +0200 Subject: [PATCH 15/37] } else { --- Objects/exceptions.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index a339d848a8a2ade..6bab5e706db8205 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2716,8 +2716,7 @@ AttributeError_str(PyObject *op) || PyTuple_GET_SIZE(self->args) != 0 ) { ret = BaseException_str(op); - } - else { + } else { ret = PyUnicode_FromFormat("%R has no attribute '%U'", self->obj, self->name); } From b65a7fddb074fa40274cbffae728cf1189d13436 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 04:26:44 +0200 Subject: [PATCH 16/37] Remove unnecessary whitespace --- Objects/exceptions.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 6bab5e706db8205..ad1cc9a77ca21a2 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2722,7 +2722,6 @@ AttributeError_str(PyObject *op) } Py_END_CRITICAL_SECTION(); return ret; - } static int From e99d87553694bafe9ea623e5db3e8fcdc2e483ff Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 04:27:02 +0200 Subject: [PATCH 17/37] Fix style --- Objects/exceptions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index ad1cc9a77ca21a2..64f6a8cc948bae9 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2718,7 +2718,7 @@ AttributeError_str(PyObject *op) ret = BaseException_str(op); } else { ret = PyUnicode_FromFormat("%R has no attribute '%U'", - self->obj, self->name); + self->obj, self->name); } Py_END_CRITICAL_SECTION(); return ret; From 8c29de3460602e9bd4260df966f4faa174d6a1b7 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 04:37:36 +0200 Subject: [PATCH 18/37] Avoid nesting critical sections! --- Objects/exceptions.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 64f6a8cc948bae9..786f57bbc6c0b37 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2706,21 +2706,22 @@ static PyObject * AttributeError_str(PyObject *op) { PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); - PyObject *ret, *arg; + PyObject *ret = NULL, *arg; 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 + 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) ) { - ret = BaseException_str(op); - } else { ret = PyUnicode_FromFormat("%R has no attribute '%U'", self->obj, self->name); } Py_END_CRITICAL_SECTION(); + if (!ret) { + ret = BaseException_str(op); + } return ret; } From 6faf358eb22dd52a3fcb054921b1ad34bd952bc4 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 05:48:55 +0200 Subject: [PATCH 19/37] Fix error handling, crete strong refs for interpolation --- Objects/exceptions.c | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 786f57bbc6c0b37..3760b17facd0d44 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2706,23 +2706,29 @@ static PyObject * AttributeError_str(PyObject *op) { PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); - PyObject *ret = NULL, *arg; + PyObject *arg, *obj = NULL, *name; + Py_BEGIN_CRITICAL_SECTION(self); - if ( - self->obj && self->name && PyUnicode_Check(self->name) + 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) - ) { - ret = PyUnicode_FromFormat("%R has no attribute '%U'", - self->obj, self->name); + || PyTuple_GET_SIZE(self->args) == 0)) + { + obj = Py_NewRef(self->obj); + name = Py_NewRef(self->name); } Py_END_CRITICAL_SECTION(); - if (!ret) { - ret = BaseException_str(op); + + if (!obj) { + return BaseException_str(op); } - return ret; + + PyObject *result = PyUnicode_FromFormat("%R has no attribute '%U'", + obj, name); + Py_DECREF(obj); + Py_DECREF(name); + return result; } static int From 086a0c947bf4cde5942691015bd8bbe54f856c08 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Thu, 16 Jul 2026 05:52:11 +0200 Subject: [PATCH 20/37] Multiline if properly done --- Objects/exceptions.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 3760b17facd0d44..c460f109c5d2881 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2709,12 +2709,13 @@ AttributeError_str(PyObject *op) PyObject *arg, *obj = NULL, *name; Py_BEGIN_CRITICAL_SECTION(self); - if (self->obj && self->name && PyUnicode_Check(self->name) + 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)) - { + || PyTuple_GET_SIZE(self->args) == 0) + ) { obj = Py_NewRef(self->obj); name = Py_NewRef(self->name); } From e5bb17e5b1555b955d01568f500f9b447b6bdcd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Fri, 17 Jul 2026 01:25:53 +0200 Subject: [PATCH 21/37] Fix small text mistake in tests --- Lib/test/test_exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 5c04a02adf29f59..94b69a9a494551a 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2082,7 +2082,7 @@ def bare_raise(name): raise AttributeError mod2.__getattr__ = bare_raise - mod3 = ModuleType("bareraise") + mod3 = ModuleType("custom") def raise_custom(name): raise AttributeError("custom") mod3.__getattr__ = raise_custom From f2507a58e438a159db394cdfff4955b8474555e3 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 10:52:34 +0200 Subject: [PATCH 22/37] Fix tests to actually validate custom message --- Lib/test/test_exceptions.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 94b69a9a494551a..b107bd3a99b4a8d 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2069,7 +2069,11 @@ def __getattr__(self, name): self._test_generated_message(RaiseWithName(), "missing1") self._test_generated_message(BareRaise(), "missing2") - self._test_generated_message(RaiseCustom(), "custom") + + with self.assertRaises(AttributeError) as cm: + getattr(RaiseCustom(), "missing3") + self.assertEqual(str(cm.exception), "custom") + def test_module_getattr_error_message(self): mod1 = ModuleType("raisewithname") @@ -2089,7 +2093,10 @@ def raise_custom(name): self._test_generated_message(mod1, "missing3") self._test_generated_message(mod2, "missing4") - self._test_generated_message(mod3, "custom") + + with self.assertRaises(AttributeError) as cm: + getattr(mod3, "missing3") + self.assertEqual(str(cm.exception), "custom") # Note: name suggestion tests live in `test_traceback`. From 9c81069bdb366488a7f82f19adaa3910bc46d2eb Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 11:03:02 +0200 Subject: [PATCH 23/37] Shorten docs --- Doc/library/exceptions.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 59ba5ca49b491f0..ea9e90aa9dfdfb9 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -215,17 +215,11 @@ 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. + When possible, :attr:`name` and :attr:`obj` are set automatically. .. 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 From e905ab292d3802e977b7cd219be7c2f147aa43b4 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 11:11:36 +0200 Subject: [PATCH 24/37] Truncate `AttributeError.obj` representation to 200 chars --- Objects/exceptions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index c460f109c5d2881..eae3484c5233c5a 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2725,7 +2725,7 @@ AttributeError_str(PyObject *op) return BaseException_str(op); } - PyObject *result = PyUnicode_FromFormat("%R has no attribute '%U'", + PyObject *result = PyUnicode_FromFormat("%.200R has no attribute '%U'", obj, name); Py_DECREF(obj); Py_DECREF(name); From 7d720ac6b521c01c49fabeb29e16e2ae9d31064b Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 11:17:17 +0200 Subject: [PATCH 25/37] Remove extra empty line --- Doc/library/exceptions.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index ea9e90aa9dfdfb9..ecf62fb6391b1b1 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -220,7 +220,6 @@ The following exceptions are the exceptions that are usually raised. .. versionchanged:: 3.10 Added the :attr:`name` and :attr:`obj` attributes. - .. exception:: EOFError Raised when the :func:`input` function hits an end-of-file condition (EOF) From 337cef41d4e0f5e099deebac580450c0aeaa21d9 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 11:21:07 +0200 Subject: [PATCH 26/37] Add "re-acquires lock" comment --- Objects/exceptions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index eae3484c5233c5a..a5eed05fda474bd 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2722,7 +2722,7 @@ AttributeError_str(PyObject *op) Py_END_CRITICAL_SECTION(); if (!obj) { - return BaseException_str(op); + return BaseException_str(op); /* re-acquires lock */ } PyObject *result = PyUnicode_FromFormat("%.200R has no attribute '%U'", From 6aa47db5f1192db8d0679de6ef1f6fc2b98f33fb Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 11:24:57 +0200 Subject: [PATCH 27/37] I desperately need a formatter --- Lib/test/test_exceptions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index b107bd3a99b4a8d..17fc0940f2b23e5 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2074,7 +2074,6 @@ def __getattr__(self, name): getattr(RaiseCustom(), "missing3") self.assertEqual(str(cm.exception), "custom") - def test_module_getattr_error_message(self): mod1 = ModuleType("raisewithname") def raise_with_name(name): From 4ff6a4817be01be3eb24b7dc313b1f0706db6ae9 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 11:25:55 +0200 Subject: [PATCH 28/37] Missing5. --- Lib/test/test_exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 17fc0940f2b23e5..d50da7c0b54a2ab 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2094,7 +2094,7 @@ def raise_custom(name): self._test_generated_message(mod2, "missing4") with self.assertRaises(AttributeError) as cm: - getattr(mod3, "missing3") + getattr(mod3, "missing5") self.assertEqual(str(cm.exception), "custom") # Note: name suggestion tests live in `test_traceback`. From e00db5fc6c1e9d2820549fd1bd5136b0424f4f8e Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 11:26:15 +0200 Subject: [PATCH 29/37] . --- 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 d50da7c0b54a2ab..ec59b31115bf3fa 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2090,11 +2090,11 @@ 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(mod1, "missing4") + self._test_generated_message(mod2, "missing5") with self.assertRaises(AttributeError) as cm: - getattr(mod3, "missing5") + getattr(mod3, "missing6") self.assertEqual(str(cm.exception), "custom") # Note: name suggestion tests live in `test_traceback`. From b5ce3c91d717f2e3f790e38bc137cdc8f6e4dc60 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 11:35:13 +0200 Subject: [PATCH 30/37] Use `_PyUnicode_Equal` --- Objects/exceptions.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index a5eed05fda474bd..cbe9a3ecb3a1075 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -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" @@ -2713,7 +2714,7 @@ AttributeError_str(PyObject *op) 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)) + && _PyUnicode_Equal(arg, self->name)) || PyTuple_GET_SIZE(self->args) == 0) ) { obj = Py_NewRef(self->obj); From da8c7235cd12531bb56b89f9eefe15211cafa48c Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 11:58:57 +0200 Subject: [PATCH 31/37] Use typename or module name --- Lib/test/test_exceptions.py | 28 ++++++++++++++++++---------- Objects/exceptions.c | 17 +++++++++++++++-- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index ec59b31115bf3fa..b002248ea6c5836 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2048,12 +2048,6 @@ 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): @@ -2067,8 +2061,15 @@ class RaiseCustom: def __getattr__(self, name): raise AttributeError("custom") - self._test_generated_message(RaiseWithName(), "missing1") - self._test_generated_message(BareRaise(), "missing2") + with self.assertRaises(AttributeError) as cm: + getattr(RaiseWithName(), "missing1") + self.assertEqual(str(cm.exception), + "'RaiseWithName' object has no attribute 'missing1'") + + with self.assertRaises(AttributeError) as cm: + getattr(BareRaise(), "missing2") + self.assertEqual(str(cm.exception), + "'BareRaise' object has no attribute 'missing2'") with self.assertRaises(AttributeError) as cm: getattr(RaiseCustom(), "missing3") @@ -2090,8 +2091,15 @@ def raise_custom(name): raise AttributeError("custom") mod3.__getattr__ = raise_custom - self._test_generated_message(mod1, "missing4") - self._test_generated_message(mod2, "missing5") + with self.assertRaises(AttributeError) as cm: + getattr(mod1, "missing4") + self.assertEqual(str(cm.exception), + "module 'raisewithname' has no attribute 'missing4'") + + with self.assertRaises(AttributeError) as cm: + getattr(mod2, "missing5") + self.assertEqual(str(cm.exception), + "module 'bareraise' has no attribute 'missing5'") with self.assertRaises(AttributeError) as cm: getattr(mod3, "missing6") diff --git a/Objects/exceptions.c b/Objects/exceptions.c index cbe9a3ecb3a1075..f91cc85bdb0e246 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2726,8 +2726,21 @@ AttributeError_str(PyObject *op) return BaseException_str(op); /* re-acquires lock */ } - PyObject *result = PyUnicode_FromFormat("%.200R has no attribute '%U'", - obj, name); + PyObject *result; + if (PyModule_Check(obj)) { + PyModuleObject *mod = _PyModule_CAST(obj); + /* In a typical case, module's __name__ is examined instead. */ + if (mod->md_name) { + result = PyUnicode_FromFormat("module '%U' has no attribute '%U'", + mod->md_name, + name); + } else { + result = PyUnicode_FromFormat("module has no attribute '%U'", name); + } + } else { + result = PyUnicode_FromFormat("'%.200s' object has no attribute '%U'", + Py_TYPE(obj)->tp_name, name); + } Py_DECREF(obj); Py_DECREF(name); return result; From c9416575564f1b14e2dc42c4c851728bebc6ebb0 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 12:09:11 +0200 Subject: [PATCH 32/37] Rearrange tests I'm not sure how to create a nameless module at runtime --- Lib/test/test_exceptions.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index b002248ea6c5836..1b770313cf560da 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2053,24 +2053,24 @@ 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") - with self.assertRaises(AttributeError) as cm: getattr(RaiseWithName(), "missing1") self.assertEqual(str(cm.exception), "'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), "'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") @@ -2080,29 +2080,26 @@ def test_module_getattr_error_message(self): def raise_with_name(name): raise AttributeError(name) mod1.__getattr__ = raise_with_name + with self.assertRaises(AttributeError) as cm: + getattr(mod1, "missing1") + self.assertEqual(str(cm.exception), + "module 'raisewithname' has no attribute 'missing1'") mod2 = ModuleType("bareraise") def bare_raise(name): raise AttributeError mod2.__getattr__ = bare_raise + with self.assertRaises(AttributeError) as cm: + getattr(mod2, "missing2") + self.assertEqual(str(cm.exception), + "module 'bareraise' has no attribute 'missing2'") mod3 = ModuleType("custom") def raise_custom(name): raise AttributeError("custom") mod3.__getattr__ = raise_custom - - with self.assertRaises(AttributeError) as cm: - getattr(mod1, "missing4") - self.assertEqual(str(cm.exception), - "module 'raisewithname' has no attribute 'missing4'") - - with self.assertRaises(AttributeError) as cm: - getattr(mod2, "missing5") - self.assertEqual(str(cm.exception), - "module 'bareraise' has no attribute 'missing5'") - with self.assertRaises(AttributeError) as cm: - getattr(mod3, "missing6") + getattr(mod3, "missing3") self.assertEqual(str(cm.exception), "custom") # Note: name suggestion tests live in `test_traceback`. From 9ea6c8a58963e229cefaafcc36445af96ca34e17 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 12:10:39 +0200 Subject: [PATCH 33/37] Cover nameless module branch --- Lib/test/test_exceptions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 1b770313cf560da..08374f72429325c 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2102,6 +2102,12 @@ def raise_custom(name): getattr(mod3, "missing3") self.assertEqual(str(cm.exception), "custom") + mod4 = ModuleType.__new__(ModuleType) + mod4.__getattr__ = raise_with_name + with self.assertRaises(AttributeError) as cm: + getattr(mod4, "missing4") + self.assertEqual(str(cm.exception), "module has no attribute 'missing4'") + # Note: name suggestion tests live in `test_traceback`. From f825f27878794a6d910a375d584ad8d488fe63b7 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 12:13:11 +0200 Subject: [PATCH 34/37] Clearer variable names --- Lib/test/test_exceptions.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 08374f72429325c..64a3cbc6b9b069a 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2076,36 +2076,36 @@ def __getattr__(self, name): self.assertEqual(str(cm.exception), "custom") def test_module_getattr_error_message(self): - mod1 = ModuleType("raisewithname") + raisewithname_mod = ModuleType("raisewithname") def raise_with_name(name): raise AttributeError(name) - mod1.__getattr__ = raise_with_name + raisewithname_mod.__getattr__ = raise_with_name with self.assertRaises(AttributeError) as cm: - getattr(mod1, "missing1") + getattr(raisewithname_mod, "missing1") self.assertEqual(str(cm.exception), "module 'raisewithname' has no attribute 'missing1'") - mod2 = ModuleType("bareraise") + bareraise_mod = ModuleType("bareraise") def bare_raise(name): raise AttributeError - mod2.__getattr__ = bare_raise + bareraise_mod.__getattr__ = bare_raise with self.assertRaises(AttributeError) as cm: - getattr(mod2, "missing2") + getattr(bareraise_mod, "missing2") self.assertEqual(str(cm.exception), "module 'bareraise' has no attribute 'missing2'") - mod3 = ModuleType("custom") + custom_mod = ModuleType("custom") def raise_custom(name): raise AttributeError("custom") - mod3.__getattr__ = raise_custom + custom_mod.__getattr__ = raise_custom with self.assertRaises(AttributeError) as cm: - getattr(mod3, "missing3") + getattr(custom_mod, "missing3") self.assertEqual(str(cm.exception), "custom") - mod4 = ModuleType.__new__(ModuleType) - mod4.__getattr__ = raise_with_name + nameless_mod = ModuleType.__new__(ModuleType) + nameless_mod.__getattr__ = raise_with_name with self.assertRaises(AttributeError) as cm: - getattr(mod4, "missing4") + getattr(nameless_mod, "missing4") self.assertEqual(str(cm.exception), "module has no attribute 'missing4'") # Note: name suggestion tests live in `test_traceback`. From 7b73fb8bac8b93eaad73a4dd710666104e2871cd Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 12:14:17 +0200 Subject: [PATCH 35/37] Localize tests visually --- Lib/test/test_exceptions.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 64a3cbc6b9b069a..b62034c1ac419be 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2052,7 +2052,6 @@ def test_getattr_error_message(self): class RaiseWithName: def __getattr__(self, name): raise AttributeError(name) - with self.assertRaises(AttributeError) as cm: getattr(RaiseWithName(), "missing1") self.assertEqual(str(cm.exception), @@ -2061,7 +2060,6 @@ def __getattr__(self, name): class BareRaise: def __getattr__(self, name): raise AttributeError - with self.assertRaises(AttributeError) as cm: getattr(BareRaise(), "missing2") self.assertEqual(str(cm.exception), @@ -2070,7 +2068,6 @@ def __getattr__(self, name): class RaiseCustom: def __getattr__(self, name): raise AttributeError("custom") - with self.assertRaises(AttributeError) as cm: getattr(RaiseCustom(), "missing3") self.assertEqual(str(cm.exception), "custom") From 7378e3080305775564cbd53dbf6c1c63cb830abd Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 17:42:38 +0200 Subject: [PATCH 36/37] Use `%T` --- Objects/exceptions.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index f91cc85bdb0e246..12c72e0f727fcea 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2738,8 +2738,8 @@ AttributeError_str(PyObject *op) result = PyUnicode_FromFormat("module has no attribute '%U'", name); } } else { - result = PyUnicode_FromFormat("'%.200s' object has no attribute '%U'", - Py_TYPE(obj)->tp_name, name); + result = PyUnicode_FromFormat("'%T' object has no attribute '%U'", + obj, name); } Py_DECREF(obj); Py_DECREF(name); From faf6e53fc1f4def9cfc431f748810fd0d8ec195d Mon Sep 17 00:00:00 2001 From: johnslavik Date: Fri, 17 Jul 2026 17:59:46 +0200 Subject: [PATCH 37/37] Revert "Use `%T`" This reverts commit 7378e3080305775564cbd53dbf6c1c63cb830abd. --- Objects/exceptions.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 12c72e0f727fcea..f91cc85bdb0e246 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2738,8 +2738,8 @@ AttributeError_str(PyObject *op) result = PyUnicode_FromFormat("module has no attribute '%U'", name); } } else { - result = PyUnicode_FromFormat("'%T' object has no attribute '%U'", - obj, name); + result = PyUnicode_FromFormat("'%.200s' object has no attribute '%U'", + Py_TYPE(obj)->tp_name, name); } Py_DECREF(obj); Py_DECREF(name);