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 14 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
- 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
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
…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).
vstinner
left a comment
There was a problem hiding this comment.
LGTM. The implementation is correct and well tested. IMO the feature makes sense and is useful.
Lib/traceback.py
Outdated
| # If is_raw is True, the suggestion is rendered as-is. | ||
| # | ||
| # See https://github.com/python/cpython/issues/146406. | ||
| _CROSS_LANGUAGE_HINTS = { |
There was a problem hiding this comment.
You might use a frozendict for such global constant dictionary.
There was a problem hiding this comment.
Applied in f702e79 - wrapped in types.MappingProxyType.
There was a problem hiding this comment.
You don't need types.MappingProxyType. We have a native frozendict type in 3.15 as of PEP 814.
There was a problem hiding this comment.
Nice - frozendict is cleaner than the MappingProxyType wrapper. Thanks for applying it.
There was a problem hiding this comment.
It seems like the frozendict was lost in the meanwhile.
|
I applied my suggestion to replace |
|
@ZeroIntensity @picnixz: Are you ok with this change? |
picnixz
left a comment
There was a problem hiding this comment.
One nit and otherwise LGTM. I think @serhiy-storchaka had some more reservations/suggestions so I'd wait a bit for his feedback maybe?
|
Thanks @picnixz for the review and approval! Happy to wait for @serhiy-storchaka's feedback. Is the nit you mentioned something I should address now, or was it from the earlier round (the Levenshtein point and the raw hint formatting)? Those should be fixed in the latest commits but let me know if I missed something. |
|
The nit is just adding a newline before the "1.". |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Added the blank line in 0089761. Waiting on @serhiy-storchaka whenever he has time. |
|
See my comments on the issue. First, it should work for subclasses. We should also fix suggestions for |
…e, float - Use isinstance() instead of type(obj) for hint lookup, so subclasses of builtin types (e.g. OrderedDict, custom list subclasses) also get cross-language hints. - Restructure table to index by method name for efficient isinstance iteration. - Add mutable-on-immutable hints: tuple.append/extend/insert/remove suggest list, frozenset.add/discard/remove/update suggest set, frozendict.update suggests dict. - Add NoneType hints: common methods (keys, upper, sort, etc.) tried on None suggest the type the user likely expected. - Add float bitwise hints: __or__/__and__/__xor__/__lshift__/__rshift__ suggest using int, fixing the misleading __dir__ Levenshtein suggestion. - Cross-language hints now take priority over Levenshtein (they are more specific; Levenshtein still fires as fallback when no table match). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Thanks @serhiy-storchaka for the thorough feedback! Addressed all your points in e2c12ec: Subclass support: Switched from Mutable on immutable: Added None suggestions: Added hints for common methods tried on None: Float bitwise: Added Demo (built from source, Python REPL): All 433 tests in test_traceback pass. |
Lib/traceback.py
Outdated
| "__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)], | ||
| # NoneType -- common methods tried on None (got None instead of expected type) | ||
| "keys": [(type(None), "Did you expect a 'dict'?", True)], |
There was a problem hiding this comment.
You can replace type(None) with types.NoneType.
Doc/whatsnew/3.15.rst
Outdated
|
|
||
| .. doctest:: | ||
|
|
||
| >>> None.keys() # doctest: +ELLIPSIS |
There was a problem hiding this comment.
I don't understand the None suggestions. Yes, None has no keys() method, but I don't see how I'm supposed to replace it with a dict in this example.
I expected an example where a variable is set to None by mistake, such as:
>>> lst=[1,2,3].sort()
>>> lst.pop()
Lib/traceback.py
Outdated
| # If is_raw is True, the suggestion is rendered as-is. | ||
| # | ||
| # See https://github.com/python/cpython/issues/146406. | ||
| _CROSS_LANGUAGE_HINTS = { |
There was a problem hiding this comment.
It seems like the frozendict was lost in the meanwhile.
Add missing @force_not_colorized decorator to three cross-language test methods. The CPython C-level traceback path includes ANSI color codes in error output, causing assertEndsWith to fail when the expected string doesn't account for trailing escape sequences. Also add the decorator to test_cross_language_levenshtein_fallback and test_cross_language_no_hint_for_unknown_attr for consistency with all other cross-language tests.
1. Restore frozendict() wrapper for _CROSS_LANGUAGE_HINTS table (lost during the isinstance() refactor in e2c12ec). Convert inner lists to tuples for consistency with immutable container. 2. Replace type(None) with types.NoneType per review. 3. Replace contrived None.keys() doctest with a realistic example: lst = [3, 1, 2].sort() followed by lst.pop(), showing the common "method returned None" mistake vstinner suggested.
|
Addressed all three in e1eb6f4:
>>> lst = [3, 1, 2].sort()
>>> lst.pop()This shows the real scenario (method returned (Also fixed the CI failure from the previous push in 564ca3a - three cross-language tests were missing |



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