Description
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:
- Attribute lookup can fail due to actual bugs that unintentionally raise
AttributeError
, for example within the code of a@property
- 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