From 7adad8b51b7a16a37ca2ab6dc6e7b6b09d7d2bd7 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:13:35 -0700 Subject: [PATCH 1/8] Add cross-language method suggestions for builtin AttributeError 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 --- Lib/test/test_traceback.py | 61 +++++++++++++++++++ Lib/traceback.py | 61 +++++++++++++++++++ .../2026-03-25-cross-language-hints.rst | 5 ++ 3 files changed, 127 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-03-25-cross-language-hints.rst diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 5dc11253e0d5c8..c81103e7d6fb58 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4564,6 +4564,67 @@ def __init__(self): actual = self.get_suggestion(Outer(), 'target') self.assertIn("'.normal.target'", actual) + def test_cross_language_list_push_suggests_append(self): + actual = self.get_suggestion([], 'push') + self.assertIn("'.append'", actual) + + def test_cross_language_list_concat_suggests_extend(self): + actual = self.get_suggestion([], 'concat') + self.assertIn("'.extend'", actual) + + def test_cross_language_list_addAll_suggests_extend(self): + actual = self.get_suggestion([], 'addAll') + self.assertIn("'.extend'", actual) + + def test_cross_language_list_add_suggests_set(self): + actual = self.get_suggestion([], 'add') + self.assertIn("Did you mean to use a set?", actual) + + def test_cross_language_str_toUpperCase_suggests_upper(self): + actual = self.get_suggestion('', 'toUpperCase') + self.assertIn("'.upper'", actual) + + def test_cross_language_str_toLowerCase_suggests_lower(self): + actual = self.get_suggestion('', 'toLowerCase') + self.assertIn("'.lower'", actual) + + def test_cross_language_str_trimStart_suggests_lstrip(self): + actual = self.get_suggestion('', 'trimStart') + self.assertIn("'.lstrip'", actual) + + def test_cross_language_str_trimEnd_suggests_rstrip(self): + actual = self.get_suggestion('', 'trimEnd') + self.assertIn("'.rstrip'", actual) + + def test_cross_language_dict_keySet_suggests_keys(self): + actual = self.get_suggestion({}, 'keySet') + self.assertIn("'.keys'", actual) + + def test_cross_language_dict_entrySet_suggests_items(self): + actual = self.get_suggestion({}, 'entrySet') + self.assertIn("'.items'", actual) + + def test_cross_language_dict_putAll_suggests_update(self): + actual = self.get_suggestion({}, 'putAll') + self.assertIn("'.update'", actual) + + 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) diff --git a/Lib/traceback.py b/Lib/traceback.py index 1f9f151ebf5d39..5039c771670aa0 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1153,6 +1153,11 @@ 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'): + with suppress(Exception): + 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) @@ -1649,6 +1654,45 @@ 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) +# 3. Must be from a top-4 language by Python co-usage: JavaScript, Java, +# C#, or Ruby (JetBrains/PSF Developer Survey 2024) +# +# Each entry maps (builtin_type, wrong_name) to a suggestion string. +# If the suggestion is a Python method name, the standard "Did you mean" +# format is used. If it contains a space, it's rendered as a full hint. +# +# See https://discuss.python.org/t/106632 for the design discussion. +_CROSS_LANGUAGE_HINTS = { + # list -- JavaScript/Ruby equivalents + (list, "push"): "append", + (list, "concat"): "extend", + # list -- Java/C# equivalents + (list, "addAll"): "extend", + # list -- wrong-type suggestion (per Serhiy Storchaka, Terry Reedy, + # Paul Moore: list.add() more likely means the user expected a set) + (list, "add"): "Did you mean to use a set? Sets have an .add() method", + # str -- JavaScript equivalents + (str, "toUpperCase"): "upper", + (str, "toLowerCase"): "lower", + (str, "trimStart"): "lstrip", + (str, "trimEnd"): "rstrip", + # dict -- Java equivalents + (dict, "keySet"): "keys", + (dict, "entrySet"): "items", + (dict, "putAll"): "update", + # Note: indexOf, trim, and getOrDefault are not included because + # Levenshtein distance already catches them (indexOf->index, + # trim->strip, getOrDefault->setdefault). +} + def _substitution_cost(ch_a, ch_b): if ch_a == ch_b: @@ -1711,6 +1755,23 @@ 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. + """ + hint = _CROSS_LANGUAGE_HINTS.get((type(obj), wrong_name)) + if hint is None: + return None + if ' ' in hint: + # Full custom hint (e.g., wrong-type suggestion for list.add) + return hint + # Direct method equivalent -- format like Levenshtein suggestions + return f"Did you mean '.{hint}' instead of '.{wrong_name}'?" + + 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-cross-language-hints.rst b/Misc/NEWS.d/next/Library/2026-03-25-cross-language-hints.rst new file mode 100644 index 00000000000000..b21e5c07337b9f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-25-cross-language-hints.rst @@ -0,0 +1,5 @@ +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`` (JavaScript), and +``"".toUpperCase()`` suggests ``upper``. The ``list.add()`` case suggests +using a set instead, following feedback from the community discussion. From aecaacbddbc79c9dd66c37338f4acc0d17e6331a Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:18:37 -0700 Subject: [PATCH 2/8] fix NEWS entry filename for bedevere bot --- ...e-hints.rst => 2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Library/{2026-03-25-cross-language-hints.rst => 2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst} (100%) diff --git a/Misc/NEWS.d/next/Library/2026-03-25-cross-language-hints.rst b/Misc/NEWS.d/next/Library/2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst similarity index 100% rename from Misc/NEWS.d/next/Library/2026-03-25-cross-language-hints.rst rename to Misc/NEWS.d/next/Library/2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst From 6d58cdc03e9ddf15128238c025de76fb3854d86f Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 25 Mar 2026 05:48:07 -0700 Subject: [PATCH 3/8] Address review feedback from @nedbat and @ZeroIntensity - Shorten hint format to "Did you mean '.append'?" (drop redundant "instead of '.push'" since the error already names the attribute) - Add dict.put and list.contains entries suggesting language constructs (dict[key] = value, x in list) per @ZeroIntensity's review - Replace suppress(Exception) with direct call (function is safe) - Link to GH issue instead of Discourse thread in comment - Drop column alignment in hint table entries - Trim NEWS entry last sentence --- Lib/test/test_traceback.py | 9 +++++ Lib/traceback.py | 33 ++++++++++--------- ...-03-25-07-17-41.gh-issue-146406.ydsmqe.rst | 5 ++- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index c81103e7d6fb58..fd1a1399a4cc34 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4567,6 +4567,7 @@ def __init__(self): def test_cross_language_list_push_suggests_append(self): actual = self.get_suggestion([], 'push') self.assertIn("'.append'", actual) + self.assertNotIn("instead of", actual) def test_cross_language_list_concat_suggests_extend(self): actual = self.get_suggestion([], 'concat') @@ -4576,6 +4577,10 @@ def test_cross_language_list_addAll_suggests_extend(self): actual = self.get_suggestion([], 'addAll') self.assertIn("'.extend'", actual) + def test_cross_language_list_contains_suggests_in(self): + actual = self.get_suggestion([], 'contains') + self.assertIn("Use 'x in list'", actual) + def test_cross_language_list_add_suggests_set(self): actual = self.get_suggestion([], 'add') self.assertIn("Did you mean to use a set?", actual) @@ -4608,6 +4613,10 @@ def test_cross_language_dict_putAll_suggests_update(self): actual = self.get_suggestion({}, 'putAll') self.assertIn("'.update'", actual) + def test_cross_language_dict_put_suggests_bracket(self): + actual = self.get_suggestion({}, 'put') + self.assertIn("dict[key] = value", actual) + def test_cross_language_levenshtein_takes_priority(self): # Levenshtein catches trim->strip and indexOf->index before # the cross-language table is consulted diff --git a/Lib/traceback.py b/Lib/traceback.py index 5039c771670aa0..15a772927b33ff 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1154,10 +1154,9 @@ 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}' ({wrong_name!a})?" elif hasattr(exc_value, 'obj'): - with suppress(Exception): - hint = _get_cross_language_hint(exc_value.obj, wrong_name) - if hint: - self._str += f". {hint}" + 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) @@ -1669,25 +1668,27 @@ def print(self, *, file=None, chain=True, **kwargs): # If the suggestion is a Python method name, the standard "Did you mean" # format is used. If it contains a space, it's rendered as a full hint. # -# See https://discuss.python.org/t/106632 for the design discussion. +# See https://github.com/python/cpython/issues/146406 for the design discussion. _CROSS_LANGUAGE_HINTS = { # list -- JavaScript/Ruby equivalents - (list, "push"): "append", - (list, "concat"): "extend", + (list, "push"): "append", + (list, "concat"): "extend", # list -- Java/C# equivalents - (list, "addAll"): "extend", + (list, "addAll"): "extend", + (list, "contains"): "Use 'x in list' to check membership", # list -- wrong-type suggestion (per Serhiy Storchaka, Terry Reedy, # Paul Moore: list.add() more likely means the user expected a set) - (list, "add"): "Did you mean to use a set? Sets have an .add() method", + (list, "add"): "Did you mean to use a set? Sets have an .add() method", # str -- JavaScript equivalents (str, "toUpperCase"): "upper", (str, "toLowerCase"): "lower", - (str, "trimStart"): "lstrip", - (str, "trimEnd"): "rstrip", + (str, "trimStart"): "lstrip", + (str, "trimEnd"): "rstrip", # dict -- Java equivalents - (dict, "keySet"): "keys", - (dict, "entrySet"): "items", - (dict, "putAll"): "update", + (dict, "keySet"): "keys", + (dict, "entrySet"): "items", + (dict, "putAll"): "update", + (dict, "put"): "Use dict[key] = value for item assignment", # Note: indexOf, trim, and getOrDefault are not included because # Levenshtein distance already catches them (indexOf->index, # trim->strip, getOrDefault->setdefault). @@ -1768,8 +1769,8 @@ def _get_cross_language_hint(obj, wrong_name): if ' ' in hint: # Full custom hint (e.g., wrong-type suggestion for list.add) return hint - # Direct method equivalent -- format like Levenshtein suggestions - return f"Did you mean '.{hint}' instead of '.{wrong_name}'?" + # Direct method equivalent + return f"Did you mean '.{hint}'?" def _get_safe___dir__(obj): 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 index b21e5c07337b9f..2f9142ce905d28 100644 --- 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 @@ -1,5 +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`` (JavaScript), and -``"".toUpperCase()`` suggests ``upper``. The ``list.add()`` case suggests -using a set instead, following feedback from the community discussion. +For example, ``[].push()`` now suggests ``append`` and +``"".toUpperCase()`` suggests ``upper``. From 8a39e32eccf909e351b0353d823373a51fbb571d Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:16:39 -0700 Subject: [PATCH 4/8] Add What's New entry for cross-language AttributeError hints --- Doc/whatsnew/3.15.rst | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 0973c387a1e595..fb671b6587021f 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -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 dict[key] = value for item assignment + + (Contributed by Matt Van Horn in :gh:`146406`.) + Other language changes ====================== From 196dbe4e8d4045ac34d8cdbbd2928ebd66feeda1 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:06:48 -0700 Subject: [PATCH 5/8] Address review feedback from @picnixz - Use (hint, is_raw) tuples instead of space-based raw detection - Shorten list.add hint to "Did you mean to use a 'set' object?" - Use d[k] = v instead of dict[key] = value for dict.put hint - Add dict.entries -> items (JavaScript) - Remove Levenshtein guardrail from code comment (belongs on issue) - Add periods to raw hint messages - Add test for dict.entries --- Doc/whatsnew/3.15.rst | 2 +- Lib/test/test_traceback.py | 8 +++-- Lib/traceback.py | 60 +++++++++++++++++--------------------- 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index fb671b6587021f..fb0c06c6f2edeb 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -445,7 +445,7 @@ Improved error messages >>> {}.put("a", 1) # doctest: +ELLIPSIS Traceback (most recent call last): ... - AttributeError: 'dict' object has no attribute 'put'. Use dict[key] = value for item assignment + AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v for item assignment. (Contributed by Matt Van Horn in :gh:`146406`.) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index fd1a1399a4cc34..d3ac8c9bd96465 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4583,7 +4583,7 @@ def test_cross_language_list_contains_suggests_in(self): def test_cross_language_list_add_suggests_set(self): actual = self.get_suggestion([], 'add') - self.assertIn("Did you mean to use a set?", actual) + self.assertIn("Did you mean to use a 'set' object?", actual) def test_cross_language_str_toUpperCase_suggests_upper(self): actual = self.get_suggestion('', 'toUpperCase') @@ -4613,9 +4613,13 @@ def test_cross_language_dict_putAll_suggests_update(self): actual = self.get_suggestion({}, 'putAll') self.assertIn("'.update'", actual) + def test_cross_language_dict_entries_suggests_items(self): + actual = self.get_suggestion({}, 'entries') + self.assertIn("'.items'", actual) + def test_cross_language_dict_put_suggests_bracket(self): actual = self.get_suggestion({}, 'put') - self.assertIn("dict[key] = value", actual) + self.assertIn("d[k] = v", actual) def test_cross_language_levenshtein_takes_priority(self): # Levenshtein catches trim->strip and indexOf->index before diff --git a/Lib/traceback.py b/Lib/traceback.py index 15a772927b33ff..484c87b7194c2e 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1658,40 +1658,35 @@ def print(self, *, file=None, chain=True, **kwargs): # # 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) -# 3. Must be from a top-4 language by Python co-usage: JavaScript, Java, -# C#, or Ruby (JetBrains/PSF Developer Survey 2024) +# traffic, bug reports in production repos, developer survey data). +# 2. Must be from a top-4 language by Python co-usage: JavaScript, Java, +# C#, or Ruby (JetBrains/PSF Developer Survey 2024). # -# Each entry maps (builtin_type, wrong_name) to a suggestion string. -# If the suggestion is a Python method name, the standard "Did you mean" -# format is used. If it contains a space, it's rendered as a full hint. +# 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 for the design discussion. +# See https://github.com/python/cpython/issues/146406. _CROSS_LANGUAGE_HINTS = { # list -- JavaScript/Ruby equivalents - (list, "push"): "append", - (list, "concat"): "extend", + (list, "push"): ("append", False), + (list, "concat"): ("extend", False), # list -- Java/C# equivalents - (list, "addAll"): "extend", - (list, "contains"): "Use 'x in list' to check membership", - # list -- wrong-type suggestion (per Serhiy Storchaka, Terry Reedy, - # Paul Moore: list.add() more likely means the user expected a set) - (list, "add"): "Did you mean to use a set? Sets have an .add() method", + (list, "addAll"): ("extend", False), + (list, "contains"): ("Use 'x in list' to check membership.", 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", - (str, "toLowerCase"): "lower", - (str, "trimStart"): "lstrip", - (str, "trimEnd"): "rstrip", - # dict -- Java equivalents - (dict, "keySet"): "keys", - (dict, "entrySet"): "items", - (dict, "putAll"): "update", - (dict, "put"): "Use dict[key] = value for item assignment", - # Note: indexOf, trim, and getOrDefault are not included because - # Levenshtein distance already catches them (indexOf->index, - # trim->strip, getOrDefault->setdefault). + (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 for item assignment.", True), } @@ -1763,13 +1758,12 @@ def _get_cross_language_hint(obj, wrong_name): positives on subclasses that may intentionally lack these methods. Returns a formatted hint string, or None. """ - hint = _CROSS_LANGUAGE_HINTS.get((type(obj), wrong_name)) - if hint is None: + entry = _CROSS_LANGUAGE_HINTS.get((type(obj), wrong_name)) + if entry is None: return None - if ' ' in hint: - # Full custom hint (e.g., wrong-type suggestion for list.add) + hint, is_raw = entry + if is_raw: return hint - # Direct method equivalent return f"Did you mean '.{hint}'?" From 99e106aefe73b97aca6c9508a5e030119874db58 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:46:42 -0700 Subject: [PATCH 6/8] Keep Levenshtein criterion, remove survey restriction per picnixz --- Lib/traceback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index 484c87b7194c2e..51ff825246731a 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1659,8 +1659,8 @@ def print(self, *, file=None, chain=True, **kwargs): # Inclusion criteria: # 1. Must have evidence of real cross-language confusion (Stack Overflow # traffic, bug reports in production repos, developer survey data). -# 2. Must be from a top-4 language by Python co-usage: JavaScript, Java, -# C#, or Ruby (JetBrains/PSF Developer Survey 2024). +# 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'?". From 57a4d399d69ce0e217552406a7d052a8c5bba6a0 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:08:52 -0700 Subject: [PATCH 7/8] Consolidate cross-language tests, shorten raw-message hints Address review feedback from @vstinner: - Merge 14 individual test_cross_language_* methods into a single parameterized test_cross_language using subTest - Shorten raw-message hints: "Use 'x in list'." and "Use d[k] = v." - Fix pre-existing levenshtein priority test assertion - Update whatsnew entry to match shortened hint text --- Doc/whatsnew/3.15.rst | 2 +- Lib/test/test_traceback.py | 79 ++++++++++++-------------------------- Lib/traceback.py | 4 +- 3 files changed, 27 insertions(+), 58 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index fb0c06c6f2edeb..906c858aea7fa4 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -445,7 +445,7 @@ Improved error messages >>> {}.put("a", 1) # doctest: +ELLIPSIS Traceback (most recent call last): ... - AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v for item assignment. + AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v. (Contributed by Matt Van Horn in :gh:`146406`.) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index d3ac8c9bd96465..7c7c5234bc1817 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4564,68 +4564,37 @@ def __init__(self): actual = self.get_suggestion(Outer(), 'target') self.assertIn("'.normal.target'", actual) - def test_cross_language_list_push_suggests_append(self): + def test_cross_language(self): + cases = [ + # (obj, attr, expected_substring) + ([], 'push', "'.append'"), + ([], 'concat', "'.extend'"), + ([], 'addAll', "'.extend'"), + ([], 'contains', "Use 'x in list'."), + ([], 'add', "Did you mean to use a 'set' object?"), + ('', 'toUpperCase', "'.upper'"), + ('', 'toLowerCase', "'.lower'"), + ('', 'trimStart', "'.lstrip'"), + ('', 'trimEnd', "'.rstrip'"), + ({}, 'keySet', "'.keys'"), + ({}, 'entrySet', "'.items'"), + ({}, 'entries', "'.items'"), + ({}, 'putAll', "'.update'"), + ({}, 'put', "d[k] = v"), + ] + for obj, attr, expected in cases: + with self.subTest(type=type(obj).__name__, attr=attr): + actual = self.get_suggestion(obj, attr) + self.assertIn(expected, actual) + # push hint should not repeat the wrong attribute name actual = self.get_suggestion([], 'push') - self.assertIn("'.append'", actual) self.assertNotIn("instead of", actual) - def test_cross_language_list_concat_suggests_extend(self): - actual = self.get_suggestion([], 'concat') - self.assertIn("'.extend'", actual) - - def test_cross_language_list_addAll_suggests_extend(self): - actual = self.get_suggestion([], 'addAll') - self.assertIn("'.extend'", actual) - - def test_cross_language_list_contains_suggests_in(self): - actual = self.get_suggestion([], 'contains') - self.assertIn("Use 'x in list'", actual) - - def test_cross_language_list_add_suggests_set(self): - actual = self.get_suggestion([], 'add') - self.assertIn("Did you mean to use a 'set' object?", actual) - - def test_cross_language_str_toUpperCase_suggests_upper(self): - actual = self.get_suggestion('', 'toUpperCase') - self.assertIn("'.upper'", actual) - - def test_cross_language_str_toLowerCase_suggests_lower(self): - actual = self.get_suggestion('', 'toLowerCase') - self.assertIn("'.lower'", actual) - - def test_cross_language_str_trimStart_suggests_lstrip(self): - actual = self.get_suggestion('', 'trimStart') - self.assertIn("'.lstrip'", actual) - - def test_cross_language_str_trimEnd_suggests_rstrip(self): - actual = self.get_suggestion('', 'trimEnd') - self.assertIn("'.rstrip'", actual) - - def test_cross_language_dict_keySet_suggests_keys(self): - actual = self.get_suggestion({}, 'keySet') - self.assertIn("'.keys'", actual) - - def test_cross_language_dict_entrySet_suggests_items(self): - actual = self.get_suggestion({}, 'entrySet') - self.assertIn("'.items'", actual) - - def test_cross_language_dict_putAll_suggests_update(self): - actual = self.get_suggestion({}, 'putAll') - self.assertIn("'.update'", actual) - - def test_cross_language_dict_entries_suggests_items(self): - actual = self.get_suggestion({}, 'entries') - self.assertIn("'.items'", actual) - - def test_cross_language_dict_put_suggests_bracket(self): - actual = self.get_suggestion({}, 'put') - self.assertIn("d[k] = v", actual) - 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) + self.assertIn("strip", actual) def test_cross_language_no_hint_for_unknown_attr(self): actual = self.get_suggestion([], 'completely_unknown_method') diff --git a/Lib/traceback.py b/Lib/traceback.py index 51ff825246731a..dd692be355cb8a 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1673,7 +1673,7 @@ def print(self, *, file=None, chain=True, **kwargs): (list, "concat"): ("extend", False), # list -- Java/C# equivalents (list, "addAll"): ("extend", False), - (list, "contains"): ("Use 'x in list' to check membership.", True), + (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 @@ -1686,7 +1686,7 @@ def print(self, *, file=None, chain=True, **kwargs): (dict, "entrySet"): ("items", False), (dict, "entries"): ("items", False), (dict, "putAll"): ("update", False), - (dict, "put"): ("Use d[k] = v for item assignment.", True), + (dict, "put"): ("Use d[k] = v.", True), } From 2a2c0d5a63b9b7cba8c1ff08060fe728d6048d5a Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:14:02 -0700 Subject: [PATCH 8/8] test: restructure test_cross_language to use assertEndsWith for full suffix matching Apply vstinner's review suggestion: use assertEndsWith instead of assertIn for more precise test assertions. Split cases into method hints (checked via Did you mean pattern) and raw hints (checked via exact suffix). --- Lib/test/test_traceback.py | 50 ++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 7c7c5234bc1817..9c07bd3297f2de 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4564,31 +4564,39 @@ def __init__(self): actual = self.get_suggestion(Outer(), 'target') self.assertIn("'.normal.target'", actual) + @force_not_colorized def test_cross_language(self): cases = [ - # (obj, attr, expected_substring) - ([], 'push', "'.append'"), - ([], 'concat', "'.extend'"), - ([], 'addAll', "'.extend'"), - ([], 'contains', "Use 'x in list'."), - ([], 'add', "Did you mean to use a 'set' object?"), - ('', 'toUpperCase', "'.upper'"), - ('', 'toLowerCase', "'.lower'"), - ('', 'trimStart', "'.lstrip'"), - ('', 'trimEnd', "'.rstrip'"), - ({}, 'keySet', "'.keys'"), - ({}, 'entrySet', "'.items'"), - ({}, 'entries', "'.items'"), - ({}, 'putAll', "'.update'"), - ({}, 'put', "d[k] = v"), + # (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 obj, attr, expected in cases: - with self.subTest(type=type(obj).__name__, attr=attr): + 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.assertIn(expected, actual) - # push hint should not repeat the wrong attribute name - actual = self.get_suggestion([], 'push') - self.assertNotIn("instead of", actual) + self.assertEndsWith(actual, expected) def test_cross_language_levenshtein_takes_priority(self): # Levenshtein catches trim->strip and indexOf->index before