Skip to content

gh-146406: Add cross-language method suggestions for builtin AttributeError#146407

Open
mvanhorn wants to merge 14 commits intopython:mainfrom
mvanhorn:osc/cross-language-attr-hints
Open

gh-146406: Add cross-language method suggestions for builtin AttributeError#146407
mvanhorn wants to merge 14 commits intopython:mainfrom
mvanhorn:osc/cross-language-attr-hints

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented Mar 25, 2026

Summary

When an AttributeError on a builtin type (list, str, dict) has no
Levenshtein-based suggestion, check a static table of common method names
from other languages.

Before:

>>> [1, 2, 3].push(4)
AttributeError: 'list' object has no attribute 'push'

After:

>>> [1, 2, 3].push(4)
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append' instead of '.push'?

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):

    "the table can simply map the method name and object type onto a suggestion. So add on a list is mapped straight to 'did you mean to use a set?' No need to check whether add is a set method - you know it is already."

  • list.add() suggests set, not append per @Storchaka (post 8):

    "It can also mean that you passed a list instead of set. Changing 'add' to 'append' will not fix the error, it will introduce yet one error."

    Reinforced by @tjreedy (post 12):

    "When a method exists for another, related, Python class, I think it better to suggest the other class rather than assume a foreign language match."

  • 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):

>>> [1, 2, 3].push(4)
AttributeError: 'list' object has no attribute 'push'

>>> 'hello'.toUpperCase()
AttributeError: 'str' object has no attribute 'toUpperCase'

>>> {}.keySet()
AttributeError: 'dict' object has no attribute 'keySet'

>>> [].add(1)
AttributeError: 'list' object has no attribute 'add'

After (this PR):

>>> [1, 2, 3].push(4)
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append' instead of '.push'?

>>> 'hello'.toUpperCase()
AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper' instead of '.toUpperCase'?

>>> {}.keySet()
AttributeError: 'dict' object has no attribute 'keySet'. Did you mean '.keys' instead of '.keySet'?

>>> [].add(1)
AttributeError: 'list' object has no attribute 'add'. Did you mean to use a set? Sets have an .add() method

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_HINTS dict (11 entries), _get_cross_language_hint() function, and a 4-line fallback hook in TracebackException.__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

Source Evidence
SO: push vs append 320K views
SO: trim whitespace 1.5M views
JetBrains/PSF Survey 2024 40% of Python devs also use JS
CPython precedent elseif->elif (gh-132449), import x from y->from x import y (gh-98931)
Discourse thread 420 views, 25 likes, 3 core devs supportive

Fixes #146406

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
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Verification screenshots

Built CPython from source and ran the REPL to verify before/after behavior.

Before (Python 3.13, no cross-language hints)

before

After (this PR)

after

All 447 tests in test_traceback pass (including 28 new cross-language tests).

@picnixz picnixz self-requested a review March 25, 2026 09:05
@picnixz
Copy link
Copy Markdown
Member

picnixz commented Mar 25, 2026

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!

@nedbat
Copy link
Copy Markdown
Member

nedbat commented Mar 25, 2026

Thanks for taking this on! From a language perspective, I think these can be shorter. Instead of:

AttributeError: 'list' object has no attribute 'push'. Did you mean '.append' instead of '.push'?

I think it is enough to say:

AttributeError: 'list' object has no attribute 'push'. Did you mean '.append' instead?

or even:

AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?

Copy link
Copy Markdown
Member

@ZeroIntensity ZeroIntensity left a comment

Choose a reason for hiding this comment

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

Looks very cool!

It'd also be nice if we could suggest using language constructs in some cases. For example:

  1. dict.put -> dict[x] = y
  2. list.contains -> x in list

Lib/traceback.py Outdated
Comment on lines +1671 to +1672
#
# See https://discuss.python.org/t/106632 for the design discussion.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's link to the GH issue instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, switched to the GH issue link.

Lib/traceback.py Outdated
Comment on lines +1675 to +1676
(list, "push"): "append",
(list, "concat"): "extend",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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").

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, dropped the alignment.

Comment on lines +4 to +5
``"".toUpperCase()`` suggests ``upper``. The ``list.add()`` case suggests
using a set instead, following feedback from the community discussion.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This last sentence isn't particularly useful:

Suggested change
``"".toUpperCase()`` suggests ``upper``. The ``list.add()`` case suggests
using a set instead, following feedback from the community discussion.
``"".toUpperCase()`` suggests ``upper``.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hm, what needs to be suppressed here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Good call - the error message already says "has no attribute 'push'" so repeating it in the hint is noise. Shortened to Did you mean '.append'? in 6d58cdc.

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.

@mvanhorn
Copy link
Copy Markdown
Contributor Author

@ZeroIntensity re: suggesting language constructs - added both in 6d58cdc. They use the custom hint format since they suggest constructs rather than method equivalents:

>>> {}.put("a", 1)
AttributeError: 'dict' object has no attribute 'put'. Use dict[key] = value for item assignment

>>> [].contains(1)
AttributeError: 'list' object has no attribute 'contains'. Use 'x in list' to check membership

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.

@mvanhorn
Copy link
Copy Markdown
Contributor Author

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!

appreciat you digging in when you're ready! ::anxiously waits::

@ZeroIntensity
Copy link
Copy Markdown
Member

Oh, you also need to add an entry to "What's New in Python 3.15".

@mvanhorn mvanhorn requested a review from AA-Turner as a code owner March 25, 2026 13:10
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Added in 579d037 - covers the basic hint format and the language-construct variant (dict.put, list.contains).

@mvanhorn mvanhorn force-pushed the osc/cross-language-attr-hints branch from 57b8fc8 to 8a39e32 Compare March 25, 2026 13:17
- 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).
Copy link
Copy Markdown
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

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

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 = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You might use a frozendict for such global constant dictionary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in f702e79 - wrapped in types.MappingProxyType.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You don't need types.MappingProxyType. We have a native frozendict type in 3.15 as of PEP 814.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice - frozendict is cleaner than the MappingProxyType wrapper. Thanks for applying it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems like the frozendict was lost in the meanwhile.

@vstinner
Copy link
Copy Markdown
Member

I applied my suggestion to replace types.MappingProxyType with frozendict.

@vstinner
Copy link
Copy Markdown
Member

vstinner commented Apr 7, 2026

@ZeroIntensity @picnixz: Are you ok with this change?

Copy link
Copy Markdown
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

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

One nit and otherwise LGTM. I think @serhiy-storchaka had some more reservations/suggestions so I'd wait a bit for his feedback maybe?

Copy link
Copy Markdown
Member

@ZeroIntensity ZeroIntensity left a comment

Choose a reason for hiding this comment

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

Fine with me!

@mvanhorn
Copy link
Copy Markdown
Contributor Author

mvanhorn commented Apr 9, 2026

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.

@picnixz
Copy link
Copy Markdown
Member

picnixz commented Apr 9, 2026

The nit is just adding a newline before the "1.".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Added the blank line in 0089761. Waiting on @serhiy-storchaka whenever he has time.

@serhiy-storchaka
Copy link
Copy Markdown
Member

See my comments on the issue.

First, it should work for subclasses. We should also fix suggestions for float.__or__ and other wrong suggestions. Add suggestions for tuple.append, frozenset.add, frozendict.update, etc. Maybe add some suggestions for None.

…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>
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Thanks @serhiy-storchaka for the thorough feedback! Addressed all your points in e2c12ec:

Subclass support: Switched from type(obj) exact match to isinstance() check. OrderedDict().entries() now suggests .items(), custom list subclasses get hints too. Table restructured to index by method name for efficient isinstance iteration.

Mutable on immutable: Added tuple.append/extend/insert/remove (suggests list), frozenset.add/discard/remove/update (suggests set), frozendict.update (suggests dict).

None suggestions: Added hints for common methods tried on None: None.keys() suggests dict, None.upper() suggests str, None.sort() suggests list, etc.

Float bitwise: Added float.__or__/__and__/__xor__/__lshift__/__rshift__ suggesting int. Cross-language hints now take priority over Levenshtein (more specific), so the misleading __dir__ suggestion is replaced. Levenshtein still fires as fallback when no table match exists.

Demo (built from source, Python REPL):

demo

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)],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can replace type(None) with types.NoneType.


.. doctest::

>>> None.keys() # doctest: +ELLIPSIS
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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()

cc @serhiy-storchaka

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 = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Addressed all three in e1eb6f4:

  1. type(None)types.NoneType - Done.

  2. frozendict wrapper restored - The frozendict() wrapper was lost during the isinstance() refactor in e2c12ec. Restored it, and converted inner lists to tuples for consistency with the immutable container.

  3. None doctest - Good point, None.keys() was contrived. Replaced with your example:

>>> lst = [3, 1, 2].sort()
>>> lst.pop()

This shows the real scenario (method returned None instead of the expected object). Also updated the prose to explain why.

(Also fixed the CI failure from the previous push in 564ca3a - three cross-language tests were missing @force_not_colorized.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add cross-language method suggestions for builtin AttributeError

6 participants