https://github.com/python/cpython/commit/dd0fde58cce1e664c095949404d91807e1b45c55
commit: dd0fde58cce1e664c095949404d91807e1b45c55
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-02-04T17:23:09Z
summary:

gh-143962: Improve name suggestions for not normalized names (GH-144154)

Suggest the normalized name or the closest name to the normalized name.
If the suggested name is not ASCII, include also its ASCII representation.

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-01-22-17-04-30.gh-issue-143962.dQR1a9.rst
M Lib/test/test_traceback.py
M Lib/traceback.py

diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 96510eeec54640..a4a49fd44bb2e0 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -4250,6 +4250,24 @@ def __dir__(self):
         actual = self.get_suggestion(A(), 'blech')
         self.assertNotIn("Did you mean", actual)
 
+    def test_suggestions_not_normalized(self):
+        class A:
+            analization = None
+            fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None
+
+        suggestion = self.get_suggestion(A(), 'fiⁿₐˡᵢᶻₐᵗᵢᵒₙ')
+        self.assertIn("'finalization'", suggestion)
+        self.assertNotIn("analization", suggestion)
+
+        class B:
+            attr_a = None
+            attr_µ = None  # attr_\xb5
+
+        suggestion = self.get_suggestion(B(), 'attr_\xb5')
+        self.assertIn("'attr_\u03bc'", suggestion)
+        self.assertIn(r"'attr_\u03bc'", suggestion)
+        self.assertNotIn("attr_a", suggestion)
+
 
 class GetattrSuggestionTests(BaseSuggestionTests):
     def test_suggestions_no_args(self):
@@ -4872,6 +4890,34 @@ def foo(self):
         actual = self.get_suggestion(instance.foo)
         self.assertIn("self.blech", actual)
 
+    def test_name_error_with_instance_not_normalized(self):
+        class A:
+            def __init__(self):
+                self.fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None
+            def foo(self):
+                analization = 1
+                x = fiⁿₐˡᵢᶻₐᵗᵢᵒₙ
+
+        instance = A()
+        actual = self.get_suggestion(instance.foo)
+        self.assertIn("self.finalization", actual)
+        self.assertNotIn("fiⁿₐˡᵢᶻₐᵗᵢᵒₙ", actual)
+        self.assertNotIn("analization", actual)
+
+        class B:
+            def __init__(self):
+                self.attr_µ = None  # attr_\xb5
+            def foo(self):
+                attr_a = 1
+                x = attr_µ  # attr_\xb5
+
+        instance = B()
+        actual = self.get_suggestion(instance.foo)
+        self.assertIn("self.attr_\u03bc", actual)
+        self.assertIn(r"self.attr_\u03bc", actual)
+        self.assertNotIn("attr_\xb5", actual)
+        self.assertNotIn("attr_a", actual)
+
     def test_unbound_local_error_with_instance(self):
         class A:
             def __init__(self):
diff --git a/Lib/traceback.py b/Lib/traceback.py
index f95d6bdbd016ac..97d83f3ddd3297 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -1111,7 +1111,10 @@ def __init__(self, exc_type, exc_value, exc_traceback, 
*, limit=None,
             wrong_name = getattr(exc_value, "name_from", None)
             suggestion = _compute_suggestion_error(exc_value, exc_traceback, 
wrong_name)
             if suggestion:
-                self._str += f". Did you mean: '{suggestion}'?"
+                if suggestion.isascii():
+                    self._str += f". Did you mean: '{suggestion}'?"
+                else:
+                    self._str += f". Did you mean: '{suggestion}' 
({suggestion!a})?"
         elif exc_type and issubclass(exc_type, ModuleNotFoundError):
             module_name = getattr(exc_value, "name", None)
             if module_name in sys.stdlib_module_names:
@@ -1129,7 +1132,10 @@ 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}'?"
+                if suggestion.isascii():
+                    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:
@@ -1654,6 +1660,13 @@ def _check_for_nested_attribute(obj, wrong_name, attrs):
 def _compute_suggestion_error(exc_value, tb, wrong_name):
     if wrong_name is None or not isinstance(wrong_name, str):
         return None
+    not_normalized = False
+    if not wrong_name.isascii():
+        from unicodedata import normalize
+        normalized_name = normalize('NFKC', wrong_name)
+        if normalized_name != wrong_name:
+            not_normalized = True
+            wrong_name = normalized_name
     if isinstance(exc_value, AttributeError):
         obj = exc_value.obj
         try:
@@ -1699,6 +1712,8 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
             + list(frame.f_builtins)
         )
         d = [x for x in d if isinstance(x, str)]
+        if not_normalized and wrong_name in d:
+            return wrong_name
 
         # Check first if we are in a method and the instance
         # has the wrong name as attribute
@@ -1711,6 +1726,8 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
             if has_wrong_name:
                 return f"self.{wrong_name}"
 
+    if not_normalized and wrong_name in d:
+        return wrong_name
     try:
         import _suggestions
     except ImportError:
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-01-22-17-04-30.gh-issue-143962.dQR1a9.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-22-17-04-30.gh-issue-143962.dQR1a9.rst
new file mode 100644
index 00000000000000..71c2476c02b89d
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-22-17-04-30.gh-issue-143962.dQR1a9.rst
@@ -0,0 +1,3 @@
+Name suggestion for not normalized name suggests now the normalized name or
+the closest name to the normalized name. If the suggested name is not ASCII,
+include also its ASCII representation.

_______________________________________________
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