Skip to content

__getattribute__ does not propagate AttributeError error-message if __getattr__ fails as well #103936

Open
@randolf-scholz

Description

@randolf-scholz

This problem only appears when __getattr__ is implemented:

EDIT: Updated MWE

import pandas as pd

class MWE:
    @property
    def foo(self):
        s = pd.Index([1,2,3])
        return s.iloc[0]   # actual bug! pd.Index has no .iloc attribute

MWE().foo  # AttributeError: 'Index' object has no attribute 'iloc'
# without `__getattr__`  implemented we get the right error message!


class MWE_with_getattr:
    def __getattr__(self, key):
        if key == "dummy":
            return "xyz"
        raise AttributeError(f"{self.__class__.__name__} has no attribute {key}!")

    @property
    def foo(self):
        s = pd.Index([1,2,3])
        return s.iloc[0]   # actual bug! pd.Index has no .iloc attribute


MWE_with_getattr().foo  # AttributeError: MWE_with_getattr has no attribute foo!
# Traceback never mentions "AttributeError: 'Index' object has no attribute 'iloc'"
# Implementing `__getattr__` gobbles up the error message!

Expectation

The traceback should include "AttributeError: 'Index' object has no attribute 'iloc". The issue seems to be that object.__getattribute__ does not re-raise the AttributeError in case the fallback __getattr__ fails as well. According to the documentation, one might expect attribute lookup to work like:

class object:
    def __getattribute__(self, key):
        try:
            attr = self._default_attribute_lookup(key)
        except AttributeError as E:
            try:  # alternative lookup via __getattr__
                attr = self.__getattr__(key)
            except Exception as F:
                raise F from E   # re-raise if __getattr__ fails as well
            else:
                return attr
        else:
            return attr

But instead, the original error gets swallowed if __getattr__ fails as well, i.e. it works more like:

class object:
    def __getattribute__(self, key):
        try:
            attr = self._default_attribute_lookup(key)
        except AttributeError as E:
            # if __getattr__ fails, we are left in the dark as to why default lookup failed.
            return self.__getattr__(key)  
        else:
            return attr

This can be problematic, because:

  1. Attribute lookup can fail due to actual bugs that unintentionally raise AttributeError, for example within the code of a @property
  2. Attributes/Properties might only exist conditionally, and with the current behavior, as the traceback does not include the original AttributeError in case the fallback __getattr__ fails as well, the error message explaining the conditional failure is not passed along.

Actual Output

The code above produces the following output:

Traceback (most recent call last):
  File "/home/rscholz/Projects/KIWI/tsdm/bugs/python/__getattr__attributeerror.py", line 25, in <module>
    MWE_with_getattr().foo  # AttributeError: MWE_with_getattr has no attribute foo!
    ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/rscholz/Projects/KIWI/tsdm/bugs/python/__getattr__attributeerror.py", line 17, in __getattr__
    raise AttributeError(f"{self.__class__.__name__} has no attribute {key}!")
AttributeError: MWE_with_getattr has no attribute foo!

The problem with non-existent .iloc is never mentioned!

Old MWE
from functools import cached_property

class Foo:
    def __getattr__(self, key):
        if key == "secret":
            return "cake"
        raise AttributeError(f"{self.__class__.__name__} has no attribute {key}!")


class Bar(Foo):
    @property
    def prop(self) -> int:
        raise AttributeError("Invisible Error message")

    @cached_property
    def cached_prop(self) -> int:
        raise AttributeError("Invisible Error message")

    filler: str = "Lorem_ipsum"


obj = Bar()
obj.prop  # ✘ AttributeError: Bar has no attribute prop!
obj.cached_prop  # ✘ AttributeError: Bar has no attribute cached_prop!

Expectation

The traceback should include "Invisible Error message". If we replace the AttributeError with, say, ValueError the problem does not occur.

Actual Output

The code above produces the following output:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[11], line 25
     21     filler: str = "Lorem_ipsum"
     24 obj = Bar()
---> 25 obj.prop  # ✘ AttributeError: Bar has no attribute cached_prop!

Cell In[11], line 8, in Foo.__getattr__(self, key)
      6 if key == "secret":
      7     return "cake"
----> 8 raise AttributeError(f"{self.__class__.__name__} has no attribute {key}!")

AttributeError: Bar has no attribute prop!

Your environment

  • python 3.11.3
  • Ubuntu 22.04

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-featureA feature request or enhancement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions