Skip to content
Open
29 changes: 29 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,35 @@ Improved error messages
^^^^^^^^^^^^^^
AttributeError: 'Container' object has no attribute 'area'. Did you mean '.inner.area' instead of '.area'?
* When an :exc:`AttributeError` on a builtin type has no close match via
Levenshtein distance, the error message now checks a static table of common
method names from other languages (JavaScript, Java, Ruby, C#) and suggests
the Python equivalent:

.. doctest::

>>> [1, 2, 3].push(4) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?

>>> 'hello'.toUpperCase() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'?

When the Python equivalent is a language construct rather than a method,
the hint describes the construct directly:

.. doctest::

>>> {}.put("a", 1) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v.

(Contributed by Matt Van Horn in :gh:`146406`.)


Other language changes
======================
Expand Down
51 changes: 51 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -4564,6 +4564,57 @@ def __init__(self):
actual = self.get_suggestion(Outer(), 'target')
self.assertIn("'.normal.target'", actual)

@force_not_colorized
def test_cross_language(self):
cases = [
# (type, attr, hint_attr)
(list, 'push', 'append'),
(list, 'concat', 'extend'),
(list, 'addAll', 'extend'),
(str, 'toUpperCase', 'upper'),
(str, 'toLowerCase', 'lower'),
(str, 'trimStart', 'lstrip'),
(str, 'trimEnd', 'rstrip'),
(dict, 'keySet', 'keys'),
(dict, 'entrySet', 'items'),
(dict, 'entries', 'items'),
(dict, 'putAll', 'update'),
]
for test_type, attr, hint_attr in cases:
with self.subTest(type=test_type.__name__, attr=attr):
obj = test_type()
actual = self.get_suggestion(obj, attr)
self.assertEndsWith(actual, f"Did you mean '.{hint_attr}'?")

cases = [
# (type, attr, hint)
(list, 'contains', "Use 'x in list'."),
(list, 'add', "Did you mean to use a 'set' object?"),
(dict, 'put', "Use d[k] = v."),
]
for test_type, attr, expected in cases:
with self.subTest(type=test_type, attr=attr):
obj = test_type()
actual = self.get_suggestion(obj, attr)
self.assertEndsWith(actual, expected)

def test_cross_language_levenshtein_takes_priority(self):
# Levenshtein catches trim->strip and indexOf->index before
# the cross-language table is consulted
actual = self.get_suggestion('', 'trim')
self.assertIn("strip", actual)

def test_cross_language_no_hint_for_unknown_attr(self):
actual = self.get_suggestion([], 'completely_unknown_method')
self.assertNotIn("Did you mean", actual)

def test_cross_language_not_triggered_for_subclasses(self):
# Only exact builtin types, not subclasses
class MyList(list):
pass
actual = self.get_suggestion(MyList(), 'push')
self.assertNotIn("append", actual)

def make_module(self, code):
tmpdir = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, tmpdir)
Expand Down
56 changes: 56 additions & 0 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,10 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
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 hasattr(exc_value, 'obj'):
hint = _get_cross_language_hint(exc_value.obj, wrong_name)
if hint:
self._str += f". {hint}"
elif exc_type and issubclass(exc_type, NameError) and \
getattr(exc_value, "name", None) is not None:
wrong_name = getattr(exc_value, "name", None)
Expand Down Expand Up @@ -1649,6 +1653,42 @@ def print(self, *, file=None, chain=True, **kwargs):
_MOVE_COST = 2
_CASE_COST = 1

# Cross-language method suggestions for builtin types.
# Consulted as a fallback when Levenshtein-based suggestions find no match.
#
# Inclusion criteria:
# 1. Must have evidence of real cross-language confusion (Stack Overflow
# traffic, bug reports in production repos, developer survey data).
# 2. Must not be catchable by Levenshtein distance (too different from
# the correct Python method name).
#
# Each entry maps (builtin_type, wrong_name) to a (suggestion, is_raw) tuple.
# If is_raw is False, the suggestion is wrapped in "Did you mean '.X'?".
# If is_raw is True, the suggestion is rendered as-is.
#
# See https://github.com/python/cpython/issues/146406.
_CROSS_LANGUAGE_HINTS = types.MappingProxyType({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking at the new built-in frozendict type:

Suggested change
_CROSS_LANGUAGE_HINTS = types.MappingProxyType({
_CROSS_LANGUAGE_HINTS = frozendict({

# list -- JavaScript/Ruby equivalents
(list, "push"): ("append", False),
(list, "concat"): ("extend", False),
# list -- Java/C# equivalents
(list, "addAll"): ("extend", False),
(list, "contains"): ("Use 'x in list'.", True),
# list -- wrong-type suggestion more likely means the user expected a set
(list, "add"): ("Did you mean to use a 'set' object?", True),
# str -- JavaScript equivalents
(str, "toUpperCase"): ("upper", False),
(str, "toLowerCase"): ("lower", False),
(str, "trimStart"): ("lstrip", False),
(str, "trimEnd"): ("rstrip", False),
# dict -- Java/JavaScript equivalents
(dict, "keySet"): ("keys", False),
(dict, "entrySet"): ("items", False),
(dict, "entries"): ("items", False),
(dict, "putAll"): ("update", False),
(dict, "put"): ("Use d[k] = v.", True),
})


def _substitution_cost(ch_a, ch_b):
if ch_a == ch_b:
Expand Down Expand Up @@ -1711,6 +1751,22 @@ def _check_for_nested_attribute(obj, wrong_name, attrs):
return None


def _get_cross_language_hint(obj, wrong_name):
"""Check if wrong_name is a common method name from another language.

Only checks exact builtin types (list, str, dict) to avoid false
positives on subclasses that may intentionally lack these methods.
Returns a formatted hint string, or None.
"""
entry = _CROSS_LANGUAGE_HINTS.get((type(obj), wrong_name))
if entry is None:
return None
hint, is_raw = entry
if is_raw:
return hint
return f"Did you mean '.{hint}'?"


def _get_safe___dir__(obj):
# Use obj.__dir__() to avoid a TypeError when calling dir(obj).
# See gh-131001 and gh-139933.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Cross-language method suggestions are now shown for :exc:`AttributeError` on
builtin types when the existing Levenshtein-based suggestions find no match.
For example, ``[].push()`` now suggests ``append`` and
``"".toUpperCase()`` suggests ``upper``.
Loading