https://github.com/python/cpython/commit/b6b72e766338490305f756e25b0e4725e1b31cd7
commit: b6b72e766338490305f756e25b0e4725e1b31cd7
branch: main
author: Bartosz Sławecki <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-02-12T14:12:49Z
summary:
gh-144285: Improve `AttributeError` attribute suggestions (#144299)
files:
A Misc/NEWS.d/next/Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst
M Lib/idlelib/idle_test/test_run.py
M Lib/test/test_traceback.py
M Lib/traceback.py
diff --git a/Lib/idlelib/idle_test/test_run.py
b/Lib/idlelib/idle_test/test_run.py
index 83ecbffa2a197e..9a9d3b7b4e219c 100644
--- a/Lib/idlelib/idle_test/test_run.py
+++ b/Lib/idlelib/idle_test/test_run.py
@@ -44,7 +44,7 @@ def __eq__(self, other):
"Or did you forget to import 'abc'?\n"),
('int.reel', AttributeError,
"type object 'int' has no attribute 'reel'. "
- "Did you mean: 'real'?\n"),
+ "Did you mean '.real' instead of '.reel'?\n"),
)
@force_not_colorized
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index eaca62b12d3eb1..99ac7fd83d91cb 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -4176,25 +4176,25 @@ class CaseChangeOverSubstitution:
BLuch = None
for cls, suggestion in [
- (Addition, "'bluchin'?"),
- (Substitution, "'blech'?"),
- (Elimination, "'blch'?"),
- (Addition, "'bluchin'?"),
- (SubstitutionOverElimination, "'blach'?"),
- (SubstitutionOverAddition, "'blach'?"),
- (EliminationOverAddition, "'bluc'?"),
- (CaseChangeOverSubstitution, "'BLuch'?"),
+ (Addition, "'.bluchin'"),
+ (Substitution, "'.blech'"),
+ (Elimination, "'.blch'"),
+ (Addition, "'.bluchin'"),
+ (SubstitutionOverElimination, "'.blach'"),
+ (SubstitutionOverAddition, "'.blach'"),
+ (EliminationOverAddition, "'.bluc'"),
+ (CaseChangeOverSubstitution, "'.BLuch'"),
]:
actual = self.get_suggestion(cls(), 'bluch')
- self.assertIn(suggestion, actual)
+ self.assertIn('Did you mean ' + suggestion, actual)
def test_suggestions_underscored(self):
class A:
bluch = None
- self.assertIn("'bluch'", self.get_suggestion(A(), 'blach'))
- self.assertIn("'bluch'", self.get_suggestion(A(), '_luch'))
- self.assertIn("'bluch'", self.get_suggestion(A(), '_bluch'))
+ self.assertIn("'.bluch'", self.get_suggestion(A(), 'blach'))
+ self.assertIn("'.bluch'", self.get_suggestion(A(), '_luch'))
+ self.assertIn("'.bluch'", self.get_suggestion(A(), '_bluch'))
attr_function = self.attr_function
class B:
@@ -4202,13 +4202,13 @@ class B:
def method(self, name):
attr_function(self, name)
- self.assertIn("'_bluch'", self.get_suggestion(B(), '_blach'))
- self.assertIn("'_bluch'", self.get_suggestion(B(), '_luch'))
- self.assertNotIn("'_bluch'", self.get_suggestion(B(), 'bluch'))
+ self.assertIn("'._bluch'", self.get_suggestion(B(), '_blach'))
+ self.assertIn("'._bluch'", self.get_suggestion(B(), '_luch'))
+ self.assertNotIn("'._bluch'", self.get_suggestion(B(), 'bluch'))
- self.assertIn("'_bluch'", self.get_suggestion(partial(B().method,
'_blach')))
- self.assertIn("'_bluch'", self.get_suggestion(partial(B().method,
'_luch')))
- self.assertIn("'_bluch'", self.get_suggestion(partial(B().method,
'bluch')))
+ self.assertIn("'._bluch'", self.get_suggestion(partial(B().method,
'_blach')))
+ self.assertIn("'._bluch'", self.get_suggestion(partial(B().method,
'_luch')))
+ self.assertIn("'._bluch'", self.get_suggestion(partial(B().method,
'bluch')))
def test_do_not_trigger_for_long_attributes(self):
@@ -4256,7 +4256,7 @@ class A:
fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None
suggestion = self.get_suggestion(A(), 'fiⁿₐˡᵢᶻₐᵗᵢᵒₙ')
- self.assertIn("'finalization'", suggestion)
+ self.assertIn("'.finalization'", suggestion)
self.assertNotIn("analization", suggestion)
class B:
@@ -4264,8 +4264,10 @@ class B:
attr_µ = None # attr_\xb5
suggestion = self.get_suggestion(B(), 'attr_\xb5')
- self.assertIn("'attr_\u03bc'", suggestion)
- self.assertIn(r"'attr_\u03bc'", suggestion)
+ self.assertIn(
+ "'.attr_\u03bc' ('attr_\\u03bc') "
+ "instead of '.attr_\xb5' ('attr_\\xb5')",
+ suggestion)
self.assertNotIn("attr_a", suggestion)
@@ -4371,11 +4373,11 @@ def __init__(self):
# Should suggest 'inner.value'
actual = self.get_suggestion(Outer(), 'value')
- self.assertIn("Did you mean: 'inner.value'", actual)
+ self.assertIn("Did you mean '.inner.value' instead of '.value'",
actual)
# Should suggest 'inner.data'
actual = self.get_suggestion(Outer(), 'data')
- self.assertIn("Did you mean: 'inner.data'", actual)
+ self.assertIn("Did you mean '.inner.data' instead of '.data'", actual)
def test_getattr_nested_prioritizes_direct_matches(self):
# Test that direct attribute matches are prioritized over nested ones
@@ -4390,7 +4392,7 @@ def __init__(self):
# Should suggest 'fooo' (direct) not 'inner.foo' (nested)
actual = self.get_suggestion(Outer(), 'foo')
- self.assertIn("Did you mean: 'fooo'", actual)
+ self.assertIn("Did you mean '.fooo'", actual)
self.assertNotIn("inner.foo", actual)
def test_getattr_nested_with_property(self):
@@ -4487,7 +4489,7 @@ def __init__(self):
# Should suggest only the first match (alphabetically)
actual = self.get_suggestion(Outer(), 'value')
- self.assertIn("'a_inner.value'", actual)
+ self.assertIn("'.a_inner.value'", actual)
# Verify it's a single suggestion, not multiple
self.assertEqual(actual.count("Did you mean"), 1)
@@ -4510,10 +4512,10 @@ def __init__(self):
self.exploder = ExplodingProperty() # Accessing attributes
will raise
self.safe_inner = SafeInner()
- # Should still suggest 'safe_inner.target' without crashing
+ # Should still suggest '.safe_inner.target' without crashing
# even though accessing exploder.target would raise an exception
actual = self.get_suggestion(Outer(), 'target')
- self.assertIn("'safe_inner.target'", actual)
+ self.assertIn("'.safe_inner.target'", actual)
def test_getattr_nested_handles_hasattr_exceptions(self):
# Test that exceptions in hasattr don't crash the system
@@ -4534,7 +4536,7 @@ def __init__(self):
# Should still find 'normal.target' even though weird.target check
fails
actual = self.get_suggestion(Outer(), 'target')
- self.assertIn("'normal.target'", actual)
+ self.assertIn("'.normal.target'", actual)
def make_module(self, code):
tmpdir = Path(tempfile.mkdtemp())
diff --git a/Lib/traceback.py b/Lib/traceback.py
index b121733c27fd8c..42453b4867ce99 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -1128,7 +1128,16 @@ def __init__(self, exc_type, exc_value, exc_traceback,
*, limit=None,
self._str += (". Site initialization is disabled, did you
forget to "
+ "add the site-packages directory to sys.path "
+ "or to enable your virtual environment?")
- elif exc_type and issubclass(exc_type, (NameError, AttributeError))
and \
+ elif exc_type and issubclass(exc_type, AttributeError) and \
+ getattr(exc_value, "name", None) is not None:
+ wrong_name = getattr(exc_value, "name", None)
+ suggestion = _compute_suggestion_error(exc_value, exc_traceback,
wrong_name)
+ if suggestion:
+ if suggestion.isascii():
+ self._str += f". Did you mean '.{suggestion}' instead of
'.{wrong_name}'?"
+ else:
+ self._str += f". Did you mean '.{suggestion}'
({suggestion!a}) instead of '.{wrong_name}' ({wrong_name!a})?"
+ elif exc_type and issubclass(exc_type, NameError) and \
getattr(exc_value, "name", None) is not None:
wrong_name = getattr(exc_value, "name", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback,
wrong_name)
@@ -1137,13 +1146,11 @@ def __init__(self, exc_type, exc_value, exc_traceback,
*, limit=None,
self._str += f". Did you mean: '{suggestion}'?"
else:
self._str += f". Did you mean: '{suggestion}'
({suggestion!a})?"
- if issubclass(exc_type, NameError):
- wrong_name = getattr(exc_value, "name", None)
- if wrong_name is not None and wrong_name in
sys.stdlib_module_names:
- if suggestion:
- self._str += f" Or did you forget to import
'{wrong_name}'?"
- else:
- self._str += f". Did you forget to import
'{wrong_name}'?"
+ if wrong_name is not None and wrong_name in
sys.stdlib_module_names:
+ if suggestion:
+ self._str += f" Or did you forget to import
'{wrong_name}'?"
+ else:
+ self._str += f". Did you forget to import '{wrong_name}'?"
if lookup_lines:
self._load_lines()
self.__suppress_context__ = \
diff --git
a/Misc/NEWS.d/next/Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst
b/Misc/NEWS.d/next/Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst
new file mode 100644
index 00000000000000..e1119a85e9c1f3
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst
@@ -0,0 +1,3 @@
+Attribute suggestions in :exc:`AttributeError` tracebacks are now formatted
differently
+to make them easier to understand, for example: ``Did you mean '.datetime.now'
instead of '.now'``.
+Contributed by Bartosz Sławecki.
_______________________________________________
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]