https://github.com/python/cpython/commit/7acee984e8e2a88bcfb7a83e9c472902e340e5be
commit: 7acee984e8e2a88bcfb7a83e9c472902e340e5be
branch: main
author: Matt Van Horn <[email protected]>
committer: vstinner <[email protected]>
date: 2026-05-04T21:38:07Z
summary:
gh-146406: Add cross-language method suggestions for builtin AttributeError
(#146407)
When Levenshtein-based suggestions find no match for an AttributeError
on list, str, or dict, check a static table of common method names from
JavaScript, Java, C#, and Ruby.
For example, [].push() now suggests .append(), "".toUpperCase() suggests
.upper(), and {}.keySet() suggests .keys().
The list.add() case suggests using a set instead of suggesting .append(),
since .add() is a set method and the user may have passed a list where
a set was expected (per discussion with Serhiy Storchaka, Terry Reedy,
and Paul Moore).
Design: flat (type, attr) -> suggestion text table, no runtime
introspection. Only exact builtin types are matched to avoid false
positives on subclasses.
Discussion: https://discuss.python.org/t/106632
Co-authored-by: Matt Van Horn <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
files:
A Misc/NEWS.d/next/Library/2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst
M Doc/whatsnew/3.15.rst
M Lib/test/test_traceback.py
M Lib/traceback.py
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index b215c56408503a..9409b41f574222 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -492,6 +492,47 @@ 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.
+
+ When a mutable method is called on an immutable type, the hint suggests
+ the mutable counterpart:
+
+ .. doctest::
+
+ >>> (1, 2, 3).append(4) # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ AttributeError: 'tuple' object has no attribute 'append'. Did you mean to
use a 'list' object?
+
+ These hints also work for subclasses of builtin types.
+
+ (Contributed by Matt Van Horn in :gh:`146406`.)
+
* The interpreter now tries to provide a suggestion when
:func:`delattr` fails due to a missing attribute.
When an attribute name that closely resembles an existing attribute is used,
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 909808825f055e..6624191f164bc1 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -4565,6 +4565,95 @@ 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)
+
+ @force_not_colorized
+ def test_cross_language_levenshtein_fallback(self):
+ # When no cross-language entry exists, Levenshtein still works
+ # (e.g., trim->strip is not in the table but Levenshtein catches it)
+ actual = self.get_suggestion('', 'trim')
+ self.assertIn("strip", actual)
+
+ @force_not_colorized
+ def test_cross_language_no_hint_for_unknown_attr(self):
+ actual = self.get_suggestion([], 'completely_unknown_method')
+ self.assertNotIn("Did you mean", actual)
+
+ @force_not_colorized
+ def test_cross_language_works_for_subclasses(self):
+ # isinstance() check means subclasses also get hints
+ class MyList(list):
+ pass
+ actual = self.get_suggestion(MyList(), 'push')
+ self.assertEndsWith(actual, "Did you mean '.append'?")
+
+ class MyDict(dict):
+ pass
+ actual = self.get_suggestion(MyDict(), 'keySet')
+ self.assertEndsWith(actual, "Did you mean '.keys'?")
+
+ @force_not_colorized
+ def test_cross_language_mutable_on_immutable(self):
+ # Mutable method on immutable type suggests the mutable counterpart
+ cases = [
+ (tuple, 'append', "Did you mean to use a 'list' object?"),
+ (tuple, 'extend', "Did you mean to use a 'list' object?"),
+ (tuple, 'insert', "Did you mean to use a 'list' object?"),
+ (tuple, 'remove', "Did you mean to use a 'list' object?"),
+ (frozenset, 'add', "Did you mean to use a 'set' object?"),
+ (frozenset, 'discard', "Did you mean to use a 'set' object?"),
+ (frozenset, 'remove', "Did you mean to use a 'set' object?"),
+ (frozenset, 'update', "Did you mean to use a 'set' object?"),
+ (frozendict, 'update', "Did you mean to use a 'dict' object?"),
+ ]
+ for test_type, attr, expected in cases:
+ with self.subTest(type=test_type.__name__, attr=attr):
+ obj = test_type()
+ actual = self.get_suggestion(obj, attr)
+ self.assertEndsWith(actual, expected)
+
+ @force_not_colorized
+ def test_cross_language_float_bitwise(self):
+ # Bitwise operators on float suggest using int
+ cases = ['__or__', '__and__', '__xor__', '__lshift__', '__rshift__']
+ for attr in cases:
+ with self.subTest(attr=attr):
+ actual = self.get_suggestion(1.0, attr)
+ self.assertIn("'int'", actual)
+ self.assertIn("Bitwise operators", actual)
+
def make_module(self, code):
tmpdir = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, tmpdir)
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 343d0e5f108c35..66e88d0a588af3 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -1187,12 +1187,20 @@ def __init__(self, exc_type, exc_value, exc_traceback,
*, limit=None,
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})?"
+ # Check cross-language/wrong-type hints first (more specific),
+ # then fall back to Levenshtein distance suggestions.
+ hint = None
+ if hasattr(exc_value, 'obj'):
+ hint = _get_cross_language_hint(exc_value.obj, wrong_name)
+ if hint:
+ self._str += f". {hint}"
+ else:
+ 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)
@@ -1689,6 +1697,62 @@ 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 a wrong method name to a list of (type, suggestion, is_raw)
+# tuples. The lookup checks isinstance() so subclasses are also matched.
+# 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 = frozendict({
+ # list -- JavaScript/Ruby equivalents
+ "push": ((list, "append", False),),
+ "concat": ((list, "extend", False),),
+ # list -- Java/C# equivalents
+ "addAll": ((list, "extend", False),),
+ "contains": ((list, "Use 'x in list'.", True),),
+ # list -- wrong-type suggestion (user expected a set)
+ "add": ((list, "Did you mean to use a 'set' object?", True),
+ (frozenset, "Did you mean to use a 'set' object?", True)),
+ # str -- JavaScript equivalents
+ "toUpperCase": ((str, "upper", False),),
+ "toLowerCase": ((str, "lower", False),),
+ "trimStart": ((str, "lstrip", False),),
+ "trimEnd": ((str, "rstrip", False),),
+ # dict -- Java/JavaScript equivalents
+ "keySet": ((dict, "keys", False),),
+ "entrySet": ((dict, "items", False),),
+ "entries": ((dict, "items", False),),
+ "putAll": ((dict, "update", False),),
+ "put": ((dict, "Use d[k] = v.", True),),
+ # tuple -- mutable method on immutable type (user expected a list)
+ "append": ((tuple, "Did you mean to use a 'list' object?", True),),
+ "extend": ((tuple, "Did you mean to use a 'list' object?", True),),
+ "insert": ((tuple, "Did you mean to use a 'list' object?", True),),
+ "remove": ((tuple, "Did you mean to use a 'list' object?", True),
+ (frozenset, "Did you mean to use a 'set' object?", True)),
+ # frozenset -- mutable method on immutable type (user expected a set)
+ "discard": ((frozenset, "Did you mean to use a 'set' object?", True),),
+ # frozendict -- mutable method on immutable type (user expected a dict)
+ "update": ((frozenset, "Did you mean to use a 'set' object?", True),
+ (frozendict, "Did you mean to use a 'dict' object?", True)),
+ # float -- bitwise operators belong to int
+ "__or__": ((float, "Did you mean to use an 'int' object? Bitwise operators
are not supported by 'float'.", True),),
+ "__and__": ((float, "Did you mean to use an 'int' object? Bitwise
operators are not supported by 'float'.", True),),
+ "__xor__": ((float, "Did you mean to use an 'int' object? Bitwise
operators are not supported by 'float'.", True),),
+ "__lshift__": ((float, "Did you mean to use an 'int' object? Bitwise
operators are not supported by 'float'.", True),),
+ "__rshift__": ((float, "Did you mean to use an 'int' object? Bitwise
operators are not supported by 'float'.", True),),
+})
+
def _substitution_cost(ch_a, ch_b):
if ch_a == ch_b:
@@ -1751,6 +1815,24 @@ 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,
+ a mutable method on an immutable type, or a method tried on None.
+
+ Uses isinstance() so subclasses of builtin types also get hints.
+ Returns a formatted hint string, or None.
+ """
+ entries = _CROSS_LANGUAGE_HINTS.get(wrong_name)
+ if entries is None:
+ return None
+ for check_type, hint, is_raw in entries:
+ if isinstance(obj, check_type):
+ if is_raw:
+ return hint
+ return f"Did you mean '.{hint}'?"
+ return None
+
+
def _get_safe___dir__(obj):
# Use obj.__dir__() to avoid a TypeError when calling dir(obj).
# See gh-131001 and gh-139933.
diff --git
a/Misc/NEWS.d/next/Library/2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst
b/Misc/NEWS.d/next/Library/2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst
new file mode 100644
index 00000000000000..0f8107d2383ba9
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst
@@ -0,0 +1,6 @@
+Cross-language method suggestions are now shown for :exc:`AttributeError` on
+builtin types and their subclasses.
+For example, ``[].push()`` suggests ``append``,
+``(1,2).append(3)`` suggests using a ``list``,
+``None.keys()`` suggests expecting a ``dict``,
+and ``1.0.__or__`` suggests using an ``int``.
_______________________________________________
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]