From 726189cf4c562a65a7a2fc89162fb7c33b209893 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Wed, 28 Jan 2026 12:00:36 +0100 Subject: [PATCH 01/20] Use "instead of" technique for clearer name suggestions --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index f95d6bdbd016ac..aee4c9d3e583f4 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1129,7 +1129,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, wrong_name = getattr(exc_value, "name", None) suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) if suggestion: - self._str += f". Did you mean: '{suggestion}'?" + self._str += f". Did you mean '{suggestion}' instead of '{wrong_name}'?" 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: From 600ede54f9b7842f4c0082199c26b245ce078770 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 15:16:24 +0100 Subject: [PATCH 02/20] Prepend with a dot for better understanding --- Lib/traceback.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index aee4c9d3e583f4..658939f1bce627 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1129,7 +1129,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, wrong_name = getattr(exc_value, "name", None) suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) if suggestion: - self._str += f". Did you mean '{suggestion}' instead of '{wrong_name}'?" + if issubclass(exc_type, AttributeError): + # Prepend with a dot for better understanding. See GH-144285. + self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?" + else: # NameError + self._str += f". Did you mean '{suggestion}' instead of '{wrong_name}'?" 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: From 89242f35725f56601bcb281613bfc6c64734c348 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 15:21:19 +0100 Subject: [PATCH 03/20] Don't change `NameError` messages Benefit isn't as clear as for `AttributeError` -- let's not change it if not necessary --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 658939f1bce627..37d120e20142eb 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1133,7 +1133,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, # Prepend with a dot for better understanding. See GH-144285. self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?" else: # NameError - self._str += f". Did you mean '{suggestion}' instead of '{wrong_name}'?" + self._str += f". Did you mean: '{suggestion}'?" 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: From 5ab231133a36b84e1d4ee8cbc590fcba0a64f754 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 15:56:53 +0100 Subject: [PATCH 04/20] Bring back the TOCTOU for readability --- Lib/traceback.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 8e5ae53d5fa784..e6dd6ab48383fd 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1128,7 +1128,8 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, + "add the site-packages directory to sys.path " + "or to enable your virtual environment?") elif exc_type and issubclass(exc_type, AttributeError) and \ - (wrong_name := getattr(exc_value, "name", None)) is not None: + 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(): @@ -1136,7 +1137,8 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, else: self._str += f". Did you mean: '.{suggestion}' ({suggestion!a}) instead of '.{wrong_name}'?" elif exc_type and issubclass(exc_type, NameError) and \ - (wrong_name := getattr(exc_value, "name", None)) is not None: + 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(): From 03b1f455af72ca0e71fd69f80ae4a2e7777bb4e6 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 16:01:08 +0100 Subject: [PATCH 05/20] Remove `:` in `AttributeError` path --- Lib/traceback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index e6dd6ab48383fd..90c43a8a711c36 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1133,9 +1133,9 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=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}'?" + 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}'?" + self._str += f". Did you mean '.{suggestion}' ({suggestion!a}) instead of '.{wrong_name}'?" elif exc_type and issubclass(exc_type, NameError) and \ getattr(exc_value, "name", None) is not None: wrong_name = getattr(exc_value, "name", None) From d0f68a067e0e5ac8ebdce5276c68de3c66613b03 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 16:28:53 +0100 Subject: [PATCH 06/20] Don't prepend with dots in non-ASCII case This is extremely unlikely Keeping parity with this case would just be unhelpful --- Lib/traceback.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 90c43a8a711c36..21baa7f6ae0447 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1133,9 +1133,12 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) if suggestion: if suggestion.isascii(): + # Prepending attribute accesseses with a dot makes the message much + # easier to understand in the most common cases. + # See GH-144285. 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}'?" + self._str += f". Did you mean: '{suggestion}' ({suggestion!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) From fce786788f424fda1bf572b93ecfa35ffc5b8799 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 16:29:11 +0100 Subject: [PATCH 07/20] Fix typo --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 21baa7f6ae0447..032d3342efd9fe 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1133,7 +1133,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) if suggestion: if suggestion.isascii(): - # Prepending attribute accesseses with a dot makes the message much + # Prepending attribute accesses with a dot makes the message much # easier to understand in the most common cases. # See GH-144285. self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?" From 6dc66129a589ad7c46d52e8417e2c579383b5f97 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 16:29:23 +0100 Subject: [PATCH 08/20] Update tests --- Lib/test/test_traceback.py | 50 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index a4a49fd44bb2e0..6e4a06d4f15305 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4176,14 +4176,14 @@ 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) @@ -4192,9 +4192,9 @@ 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: @@ -4371,11 +4371,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 '.value'", actual) def test_getattr_nested_prioritizes_direct_matches(self): # Test that direct attribute matches are prioritized over nested ones @@ -4390,7 +4390,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 +4487,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 +4510,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 +4534,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()) From 79539b0cc2c61a2d93df9abab56d171c607b40e2 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 16:31:48 +0100 Subject: [PATCH 09/20] Add news entry --- .../Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst 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..c8e9fb0da08940 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst @@ -0,0 +1,3 @@ +Attribute access suggestions in :exc:`AttributeError` tracebacks are now +prepended with a dot (e.g. ``'.datetime.now'``) to make them easier to +understand. Contributed by Bartosz Sławecki. From 605435248c5890e70f90cd1b9fe2ae0f153a6911 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 16:34:11 +0100 Subject: [PATCH 10/20] Less words, more meaning --- Lib/traceback.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 032d3342efd9fe..5c2de0606be9a2 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1133,8 +1133,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) if suggestion: if suggestion.isascii(): - # Prepending attribute accesses with a dot makes the message much - # easier to understand in the most common cases. + # Prepending attribute accesses with a dot makes the message much clearer. # See GH-144285. self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?" else: From f80d3da474dd1e9836b96f54539645e55750f19c Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 19:30:47 +0100 Subject: [PATCH 11/20] Fix more tests --- Lib/idlelib/idle_test/test_run.py | 2 +- Lib/test/test_traceback.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 6e4a06d4f15305..28c20089f57b09 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4375,7 +4375,7 @@ def __init__(self): # Should suggest 'inner.data' actual = self.get_suggestion(Outer(), 'data') - self.assertIn("Did you mean '.inner.data' instead of '.value'", 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 From c973a52756c1ef370890725e0ca147fcea9cd2b7 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 20:07:51 +0100 Subject: [PATCH 12/20] Handle non-ASCII cases correctly --- Lib/test/test_traceback.py | 2 +- Lib/traceback.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 28c20089f57b09..511bad30c2d1c6 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4264,7 +4264,7 @@ class B: attr_µ = None # attr_\xb5 suggestion = self.get_suggestion(B(), 'attr_\xb5') - self.assertIn("'attr_\u03bc'", suggestion) + self.assertIn("'.attr_\u03bc'", suggestion) self.assertIn(r"'attr_\u03bc'", suggestion) self.assertNotIn("attr_a", suggestion) diff --git a/Lib/traceback.py b/Lib/traceback.py index 5c2de0606be9a2..1bf9a914a04027 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1137,7 +1137,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, # See GH-144285. self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?" else: - self._str += f". Did you mean: '{suggestion}' ({suggestion!a})?" + 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) From 01337cfbeb9eefc8fdb5dbdeacd421a801ad9fe0 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 20:16:20 +0100 Subject: [PATCH 13/20] Extend the normalized suggestions test --- Lib/test/test_traceback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 511bad30c2d1c6..1deb728a0545a6 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4264,8 +4264,8 @@ 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')", suggestion) + self.assertIn("'.attr_\xb5' ('attr_\\xb5')", suggestion) self.assertNotIn("attr_a", suggestion) From 97e075b2738171260931d7ec85c5c12dccb9dc03 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 20:19:33 +0100 Subject: [PATCH 14/20] Make the extended test more strict --- Lib/test/test_traceback.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 1deb728a0545a6..ece2e77601ff7e 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4264,8 +4264,10 @@ class B: attr_µ = None # attr_\xb5 suggestion = self.get_suggestion(B(), 'attr_\xb5') - self.assertIn("'.attr_\u03bc' ('attr_\\u03bc')", suggestion) - self.assertIn("'.attr_\xb5' ('attr_\\xb5')", suggestion) + self.assertIn( + "'.attr_\u03bc' ('attr_\\u03bc') " + "instead of '.attr_\xb5' ('attr_\\xb5')", + suggestion) self.assertNotIn("attr_a", suggestion) From 107e7c0af46142fd0bc78db4955febd106d84a34 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 20:21:30 +0100 Subject: [PATCH 15/20] Update news entry to reflect what happened --- .../Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index c8e9fb0da08940..9de5eac8d97ed8 100644 --- 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 @@ -1,3 +1,4 @@ Attribute access suggestions in :exc:`AttributeError` tracebacks are now -prepended with a dot (e.g. ``'.datetime.now'``) to make them easier to -understand. Contributed by Bartosz Sławecki. +collated with the original attribute access and prepended with a dot +(e.g. ``'.datetime.now'``) to make them easier to understand. +Contributed by Bartosz Sławecki. From 57f08a6d10152efc35aad6803ff583f4f8e9f9c7 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 20:22:06 +0100 Subject: [PATCH 16/20] Fix wording --- .../next/Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 9de5eac8d97ed8..0113ee44325511 100644 --- 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 @@ -1,4 +1,4 @@ Attribute access suggestions in :exc:`AttributeError` tracebacks are now -collated with the original attribute access and prepended with a dot +collated with the original attribute accesses and prepended with a dot (e.g. ``'.datetime.now'``) to make them easier to understand. Contributed by Bartosz Sławecki. From 5bfdc3efcb5c63428031bf039e6c091627cdc585 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 20:25:22 +0100 Subject: [PATCH 17/20] Final plumbing --- Lib/test/test_traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index ece2e77601ff7e..b12da52b2a841b 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4186,7 +4186,7 @@ class CaseChangeOverSubstitution: (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: From 5e14b5f9e41445f2fc227ebabb2363295fd8c340 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 7 Feb 2026 20:29:30 +0100 Subject: [PATCH 18/20] Make the news entry easier --- .../Library/2026-02-07-16-31-42.gh-issue-144285.iyH9iL.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 0113ee44325511..e1119a85e9c1f3 100644 --- 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 @@ -1,4 +1,3 @@ -Attribute access suggestions in :exc:`AttributeError` tracebacks are now -collated with the original attribute accesses and prepended with a dot -(e.g. ``'.datetime.now'``) to make them easier to understand. +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. From b253b1c22fb908d304f1d2089309736ffb838e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Sat, 7 Feb 2026 21:03:27 +0100 Subject: [PATCH 19/20] Remove unneeded comment (code is self-explanatory) --- Lib/traceback.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 1bf9a914a04027..aa7b6759bc8c13 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1133,8 +1133,6 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) if suggestion: if suggestion.isascii(): - # Prepending attribute accesses with a dot makes the message much clearer. - # See GH-144285. 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})?" From 5a25fea816e00c30138bece4c1fb786918159517 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 8 Feb 2026 16:58:28 +0100 Subject: [PATCH 20/20] Fix colon usage inconsistency --- Lib/traceback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index aa7b6759bc8c13..cb3ba6940fd230 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1135,7 +1135,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, 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})?" + 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)