Skip to content

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

Open
mvanhorn wants to merge 6 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 6 commits intopython:mainfrom
mvanhorn:osc/cross-language-attr-hints

Conversation

@mvanhorn
Copy link
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# See https://github.com/python/cpython/issues/146406 for the design discussion.
# See https://github.com/python/cpython/issues/146406.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Lib/traceback.py Outdated
Comment on lines +1660 to +1665
# 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)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed - moved that note to the issue instead.

Lib/traceback.py Outdated
Comment on lines +1679 to +1680
# list -- wrong-type suggestion (per Serhiy Storchaka, Terry Reedy,
# Paul Moore: list.add() more likely means the user expected a set)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# 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

Copy link
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
(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",
Copy link
Member

Choose a reason for hiding this comment

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

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?"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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",
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, switched to d[k] = v.

Lib/traceback.py Outdated
(str, "trimStart"): "lstrip",
(str, "trimEnd"): "rstrip",
# dict -- Java equivalents
(dict, "keySet"): "keys",
Copy link
Member

Choose a reason for hiding this comment

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

Let's also add dict.entries for JavaScript. IDK if it's already caught though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added. Levenshtein doesn't catch it (too far from 'items').

Lib/traceback.py Outdated
Comment on lines +1769 to +1771
if ' ' in hint:
# Full custom hint (e.g., wrong-type suggestion for list.add)
return hint
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched to (hint, is_raw) tuples. Much cleaner than the space check.

>>> {}.put("a", 1) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError: 'dict' object has no attribute 'put'. Use dict[key] = value for item assignment
Copy link
Member

Choose a reason for hiding this comment

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

Grand question. Should we end the message with a period or not?

Copy link
Member

Choose a reason for hiding this comment

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

I think a period would be nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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
Comment on lines +1662 to +1663
# 2. Must be from a top-4 language by Python co-usage: JavaScript, Java,
# C#, or Ruby (JetBrains/PSF Developer Survey 2024).
Copy link
Member

Choose a reason for hiding this comment

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

You erased the wrong point,. I wanted to keep the Levenshtein mention but ignore the survey one (maybe my suggestion was wrong?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry let me dig in!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, I mixed up which point to drop. Fixed in 99e106a - kept the Levenshtein criterion, removed the survey restriction.

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

4 participants