Skip to content

rlcompleter suggests attributes not accessible on instances (e.g., Enum member __name__) #139819

Closed

Description

@ttw225

Bug report

Bug description:

While looking into gh-139398, I noticed a separate issue: some tab-completion suggestions are offered even though they do not actually exist / are not accessible.

For example, on an Enum member, __name__ is suggested but accessing it raises AttributeError. Tracing the behavior shows rlcompleter includes class-level names even when they aren’t accessible from the instance; adding a guard in rlcompleter fixes it (PR incoming).

Reproduce

from enum import Enum
class Color(Enum):
    BLUE = 1

# In the REPL, press <TAB> after typing:
Color.BLUE.__
# '__name__' appears in the suggestions.

Color.BLUE.__name__  # AttributeError: 'Color' object has no attribute '__name__'

Expected

Only attributes actually accessible on the instance should be suggested.

Actual

Class-level names show up for instances (e.g., Enum members), leading to AttributeError when accessed.

Fix

diff --git a/Lib/rlcompleter.py b/Lib/rlcompleter.py
index 23eb0020f42..521f41a869d 100644
--- a/Lib/rlcompleter.py
+++ b/Lib/rlcompleter.py
@@ -39,6 +39,9 @@

 __all__ = ["Completer"]

+# Sentinel object to distinguish "missing" from "present but None"
+_SENTINEL = object()
+
 class Completer:
     def __init__(self, namespace = None):
         """Create a new completer for the command line.
@@ -188,9 +191,9 @@ def attr_matches(self, text):
                         # property method, which is not desirable.
                         matches.append(match)
                         continue
-                    if (value := getattr(thisobject, word, None)) is not None:
+                    if (value := getattr(thisobject, word, _SENTINEL)) is not _SENTINEL:
                         matches.append(self._callable_postfix(value, match))
-                    else:
+                    elif word in getattr(type(thisobject), '__slots__', ()):
                         matches.append(match)
             if matches or not noprefix:
                 break

Also observed (bogus suggestions)

['Color.BLUE.__iter__',
 'Color.BLUE.__getitem__',
 'Color.BLUE.__members__',
 'Color.BLUE.__contains__',
 'Color.BLUE.__qualname__',
 'Color.BLUE.__len__',
 'Color.BLUE.__name__']
# Additionally seen among bogus suggestions:
'Enum.__abstractmethods__'

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Oct 9, 2025
  2. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Oct 9, 2025
  3. picnixz commented on Oct 9, 2025

    @picnixz
    Member

    I would rather change __dir__ of Enum instead. The reason is that we actually have a custom __dir__ for enums so the reason why __name__ is here may be because of that.

  4. ttw225 commented on Oct 9, 2025

    @ttw225
    ContributorAuthor

    Thanks @picnixz ! Yes, Enum defines a custom __dir__.
    And I previously verified that Enum’s custom dir behaves correctly:

    >>> from enum import Enum
    >>> class Color(Enum):
    ...     BLUE = 1
    ...
    >>> dir(Color)
    ['BLUE', '__class__', '__contains__', '__doc__', '__getitem__', '__init_subclass__', '__iter__', '__len__', '__members__', '__module__', '__name__', '__qualname__']
    >>> dir(Color.BLUE)
    ['__class__', '__doc__', '__eq__', '__hash__', '__module__', 'name', 'value']

    The spurious completions appear because rlcompleter.attr_matches() also considers attributes from the class while completing on an instance, without guarding that those names are actually accessible from the instance. This makes the behavior incorrect in mixed class/instance or inheritance scenarios (e.g., Enum members).

    I think the fix belongs in rlcompleter, but I’m open to alternatives.

  5. ttw225 commented on Oct 9, 2025

    @ttw225
    ContributorAuthor

    For context, the current logic in attr_matches() is:

    if (value := getattr(thisobject, word, None)) is not None:
        matches.append(self._callable_postfix(value, match))
    else:
        matches.append(match)

    The else path is intended to include attributes whose value is None, but it does so without verifying that the attribute actually exists on the instance. This is what lets class-level names leak into instance completions.

  6. picnixz commented on Oct 9, 2025

    @picnixz
    Member

    Ah I see. I can understand why it's an issue. For instance, with int, the reason why __name__ doesn't show up in x.__<TAB> when x is an int is because dir(int) doesn't contain __name__.

    This is what lets class-level names leak into instance completions.

    However, this should be accepted. As I can see on the PR, the tests are failing because we don't want to invoke __getattribute__ more than once per attribute (otherwise it may become VERY slow/or have undesirable side-effects). An hasattr check unfortunately calls it as well.

    An alternative would be to try to get __dict__ of the instance, if any and check for name in __dict__. For instances that do not have a __dict__, we should indeed try to use hasattr. At least, we wouldn't need to trigger many __getattribute__ that could have undesirable side-effects.

  7. ttw225 commented on Oct 9, 2025

    @ttw225
    ContributorAuthor

    Reply to @picnixz

    Thanks a lot for the thoughtful suggestions — I completely agree about avoiding multiple __getattribute__ calls per candidate.

    I’ve updated the PR with a sentinel-based approach so we keep correctness while sticking to a single lookup:

    • Define a module-level _SENTINEL = object() and use
      if (value := getattr(thisobject, word, _SENTINEL)) is not _SENTINEL:
      This treats “present but `None`” as present, so we can drop the old elif hasattr(...) branch and still avoid a second lookup.
    • Keep the early return for @property to avoid evaluating properties.
    • Preserve the slots behavior per the existing tests:
      elif word in getattr(type(thisobject), '__slots__', ()): matches.append(match)
      from test: test_uncreated_attr

      Attributes like properties and slots should be completed even when
      they haven't been created on an instance

    The PR has been updated and tests are passing.

    As a side note on __dict__: it can be a useful positive hint, but it’s not complete. For example, __name__ exists on a type but won’t show up in its __dict__:

    class C:
        x = None
    
    c = C()
    c.__dict__          # {}

    Thanks again for the review and pointers!

  8. added a commit that references this issue on Jun 9, 2026
  9. added a commit that references this issue on Jun 9, 2026
  10. added 3 commits that reference this issue on Jun 10, 2026
  11. added 2 commits that reference this issue on Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions