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]