gh-146406: Add cross-language method suggestions for builtin AttributeError#146407
gh-146406: Add cross-language method suggestions for builtin AttributeError#146407mvanhorn wants to merge 7 commits intopython:mainfrom
Conversation
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
|
I will make a review of this PR when I have time (by the end of the week), so fellow core devs, please hold off any merge, TiA! |
|
Thanks for taking this on! From a language perspective, I think these can be shorter. Instead of: I think it is enough to say: or even: |
ZeroIntensity
left a comment
There was a problem hiding this comment.
Looks very cool!
It'd also be nice if we could suggest using language constructs in some cases. For example:
dict.put->dict[x] = ylist.contains->x in list
Lib/traceback.py
Outdated
| # | ||
| # See https://discuss.python.org/t/106632 for the design discussion. |
There was a problem hiding this comment.
Let's link to the GH issue instead.
There was a problem hiding this comment.
Done, switched to the GH issue link.
Lib/traceback.py
Outdated
| (list, "push"): "append", | ||
| (list, "concat"): "extend", |
There was a problem hiding this comment.
I'd prefer to keep standard convention and just use a single space after the :. Alignment adds extra maintenance (because we need to change each entry if we add something that breaks it) and also creates a false-symmetry between each entry (e.g., for our purposes (list, "push"): "append" has no functional relation to (list, "concat"): "extend").
There was a problem hiding this comment.
Makes sense, dropped the alignment.
| ``"".toUpperCase()`` suggests ``upper``. The ``list.add()`` case suggests | ||
| using a set instead, following feedback from the community discussion. |
There was a problem hiding this comment.
This last sentence isn't particularly useful:
| ``"".toUpperCase()`` suggests ``upper``. The ``list.add()`` case suggests | |
| using a set instead, following feedback from the community discussion. | |
| ``"".toUpperCase()`` suggests ``upper``. |
There was a problem hiding this comment.
Applied your suggestion.
Lib/traceback.py
Outdated
| 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): |
There was a problem hiding this comment.
Hm, what needs to be suppressed here?
There was a problem hiding this comment.
Nothing realistic - _get_cross_language_hint is just a dict lookup on (type(obj), wrong_name), both of which are already validated by the time we get here. I had it as a defensive measure since we're inside traceback formatting, but on reflection it's unnecessary given how simple the function is. Removed it.
- 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
|
Good call - the error message already says "has no attribute 'push'" so repeating it in the hint is noise. Shortened to I kept the existing Levenshtein format as-is (that's a separate code path and probably a separate discussion), so the cross-language hints now have their own slighty shorter style. |
|
@ZeroIntensity re: suggesting language constructs - added both in 6d58cdc. They use the custom hint format since they suggest constructs rather than method equivalents: The existing architecture already handles this - entries with a space in the hint string are rendered as-is rather than wrapped in "Did you mean" format. |
appreciat you digging in when you're ready! ::anxiously waits:: |
|
Oh, you also need to add an entry to "What's New in Python 3.15". |
|
Added in 579d037 - covers the basic hint format and the language-construct variant ( |
57b8fc8 to
8a39e32
Compare
Lib/traceback.py
Outdated
| # 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://github.com/python/cpython/issues/146406 for the design discussion. |
There was a problem hiding this comment.
| # See https://github.com/python/cpython/issues/146406 for the design discussion. | |
| # See https://github.com/python/cpython/issues/146406. |
Lib/traceback.py
Outdated
| # 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) |
There was a problem hiding this comment.
| # 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) | |
| # 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). |
The last point should be mentioned on the issue but not in the code if we want to update the list.
There was a problem hiding this comment.
Removed - moved that note to the issue instead.
Lib/traceback.py
Outdated
| # list -- wrong-type suggestion (per Serhiy Storchaka, Terry Reedy, | ||
| # Paul Moore: list.add() more likely means the user expected a set) |
There was a problem hiding this comment.
| # list -- wrong-type suggestion (per Serhiy Storchaka, Terry Reedy, | |
| # Paul Moore: list.add() more likely means the user expected a set) | |
| # list -- wrong-type suggestion more likely means the user expected a set |
There was a problem hiding this comment.
Applied your suggestion.
Lib/traceback.py
Outdated
| (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", |
There was a problem hiding this comment.
I don't really find the suggestion for list.add really helpful and I find the "Sets have ..." part redundant. I'm not saying we should not have it but I would have maybe said:
"Did you mean to use a 'set' object?"
While it could appear redundant, the full message would be "'list' objects have no 'add' attribute. Did you mean to use a 'set' object?"
There was a problem hiding this comment.
Shortened to "Did you mean to use a 'set' object?"
Lib/traceback.py
Outdated
| (dict, "keySet"): "keys", | ||
| (dict, "entrySet"): "items", | ||
| (dict, "putAll"): "update", | ||
| (dict, "put"): "Use dict[key] = value for item assignment", |
There was a problem hiding this comment.
Having the full message
'dict' object [...]. Use dict[key] = value ...
would be confusing IMO. So I suggest that we rename dict to d[k] = v instead because the docs for dict use those symbols.
There was a problem hiding this comment.
Good catch, switched to d[k] = v.
Lib/traceback.py
Outdated
| (str, "trimStart"): "lstrip", | ||
| (str, "trimEnd"): "rstrip", | ||
| # dict -- Java equivalents | ||
| (dict, "keySet"): "keys", |
There was a problem hiding this comment.
Let's also add dict.entries for JavaScript. IDK if it's already caught though.
There was a problem hiding this comment.
Added. Levenshtein doesn't catch it (too far from 'items').
Lib/traceback.py
Outdated
| if ' ' in hint: | ||
| # Full custom hint (e.g., wrong-type suggestion for list.add) | ||
| return hint |
There was a problem hiding this comment.
Instead of this fragile check, I suggest we register tuples instead: (hint_message, use_raw_message).
If we add an API for updating the known suggestions, then users can decide how to handle their spaces. Note that it's technically possible to support spaces in attribute names, though very unlikely to happen.
There was a problem hiding this comment.
Switched to (hint, is_raw) tuples. Much cleaner than the space check.
Doc/whatsnew/3.15.rst
Outdated
| >>> {}.put("a", 1) # doctest: +ELLIPSIS | ||
| Traceback (most recent call last): | ||
| ... | ||
| AttributeError: 'dict' object has no attribute 'put'. Use dict[key] = value for item assignment |
There was a problem hiding this comment.
Grand question. Should we end the message with a period or not?
There was a problem hiding this comment.
I think a period would be nice.
There was a problem hiding this comment.
Added periods to the raw hints.
- 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
Lib/traceback.py
Outdated
| # 2. Must be from a top-4 language by Python co-usage: JavaScript, Java, | ||
| # C#, or Ruby (JetBrains/PSF Developer Survey 2024). |
There was a problem hiding this comment.
You erased the wrong point,. I wanted to keep the Levenshtein mention but ignore the survey one (maybe my suggestion was wrong?)
There was a problem hiding this comment.
sorry let me dig in!
There was a problem hiding this comment.
You're right, I mixed up which point to drop. Fixed in 99e106a - kept the Levenshtein criterion, removed the survey restriction.
Lib/test/test_traceback.py
Outdated
| actual = self.get_suggestion(Outer(), 'target') | ||
| self.assertIn("'.normal.target'", actual) | ||
|
|
||
| def test_cross_language_list_push_suggests_append(self): |
There was a problem hiding this comment.
Hum, it's kind of verbose to have one test per method. What about having a single test_cross_language() which runs all these tests?
There was a problem hiding this comment.
Consolidated into a single test_cross_language with subTest in 57a4d39. Also fixed a pre-existing assertion in the levenshtein priority test.
Lib/traceback.py
Outdated
| (list, "concat"): ("extend", False), | ||
| # list -- Java/C# equivalents | ||
| (list, "addAll"): ("extend", False), | ||
| (list, "contains"): ("Use 'x in list' to check membership.", True), |
There was a problem hiding this comment.
Hum, maybe make it shorter:
| (list, "contains"): ("Use 'x in list' to check membership.", True), | |
| (list, "contains"): ("Use 'x in list'.", True), |
There was a problem hiding this comment.
Shortened in 57a4d39.
Lib/traceback.py
Outdated
| (dict, "entrySet"): ("items", False), | ||
| (dict, "entries"): ("items", False), | ||
| (dict, "putAll"): ("update", False), | ||
| (dict, "put"): ("Use d[k] = v for item assignment.", True), |
There was a problem hiding this comment.
Same suggestion here:
| (dict, "put"): ("Use d[k] = v for item assignment.", True), | |
| (dict, "put"): ("Use d[k] = v.", True), |
There was a problem hiding this comment.
Done in 57a4d39. Also updated the whatsnew entry.
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


Summary
When an
AttributeErroron a builtin type (list,str,dict) has noLevenshtein-based suggestion, check a static table of common method names
from other languages.
Before:
After:
Discourse discussion
Discussed at https://discuss.python.org/t/106632 (420 views, 25 likes, 15 posts, 3 core devs).
Design decisions from the thread:
Flat table per @pf_moore (post 14, 4 likes):
list.add() suggests set, not append per @Storchaka (post 8):
Reinforced by @tjreedy (post 12):
Scope guardrails per @dr_carlos (post 3): only add entries backed by real confusion evidence, not just because methods are similar.
Static table for builtins only (Option 1) - community consensus. 11 entries covering JavaScript, Java, C#, and Ruby.
Verification
Before (system Python 3.13, no cross-language hints):
After (this PR):
Levenshtein still takes priority (trim->strip, indexOf->index already work and are not in the table). Only exact builtin types are matched - subclasses are not affected.
Changes
Lib/traceback.py: Added_CROSS_LANGUAGE_HINTSdict (11 entries),_get_cross_language_hint()function, and a 4-line fallback hook inTracebackException.__init__that runs only when Levenshtein found nothing.Lib/test/test_traceback.py: 15 test cases covering all entries, priority ordering, unknown attrs, and subclass exclusion.Misc/NEWS.d/: NEWS entry.Evidence
elseif->elif(gh-132449),import x from y->from x import y(gh-98931)Fixes #146406