Search code examples
pythonpropertiesattributeerrorgetattr

AttributeErrors: undesired interaction between @property and __getattr__


I have a problem with AttributeErrors raised in a @property in combination with __getattr__() in python:

Example code:

>>> def deeply_nested_factory_fn():
...     a = 2
...     return a.invalid_attr
...
>>> class Test(object):
...     def __getattr__(self, name):
...         if name == 'abc':
...             return 'abc'
...         raise AttributeError("'Test' object has no attribute '%s'" % name)
...     @property
...     def my_prop(self):
...         return deeply_nested_factory_fn()
...
>>> test = Test()
>>> test.my_prop
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __getattr__
AttributeError: 'Test' object has no attribute 'my_prop'

In my case, this is a highly misleading error message, because it hides the fact that deeply_nested_factory_fn() has a mistake.


Based on the idea in Tadhg McDonald-Jensen's answer, my currently best solution is the following. Any hints on how to get rid of the __main__. prefix to AttributeError and the reference to attributeErrorCatcher in the traceback would be much appreciated.

>>> def catchAttributeErrors(func):
...     AttributeError_org = AttributeError
...     def attributeErrorCatcher(*args, **kwargs):
...         try:
...             return func(*args, **kwargs)
...         except AttributeError_org as e:
...             import sys
...             class AttributeError(Exception):
...                 pass
...             etype, value, tb = sys.exc_info()
...             raise AttributeError(e).with_traceback(tb.tb_next) from None
...     return attributeErrorCatcher
...
>>> def deeply_nested_factory_fn():
...     a = 2
...     return a.invalid_attr
...
>>> class Test(object):
...     def __getattr__(self, name):
...         if name == 'abc':
...             # computing come other attributes
...             return 'abc'
...         raise AttributeError("'Test' object has no attribute '%s'" % name)
...     @property
...     @catchAttributeErrors
...     def my_prop(self):
...         return deeply_nested_factory_fn()
...
>>> class Test1(object):
...     def __init__(self):
...         test = Test()
...         test.my_prop
...
>>> test1 = Test1()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __init__
  File "<stdin>", line 11, in attributeErrorCatcher
  File "<stdin>", line 10, in my_prop
  File "<stdin>", line 3, in deeply_nested_factory_fn
__main__.AttributeError: 'int' object has no attribute 'invalid_attr'

Solution

  • Just in case others find this: the problem with the example on top is that an AttributeError is raised inside __getattr__. Instead, one should call self.__getattribute__(attr) to let that raise.

    Example

    def deeply_nested_factory_fn():
        a = 2
        return a.invalid_attr
    
    class Test(object):
        def __getattr__(self, name):
            if name == 'abc':
                return 'abc'
            return self.__getattribute__(name)
        @property
        def my_prop(self):
            return deeply_nested_factory_fn()
    
    test = Test()
    test.my_prop
    

    This yields

    AttributeError                            Traceback (most recent call last)
    Cell In [1], line 15
         12         return deeply_nested_factory_fn()
         14 test = Test()
    ---> 15 test.my_prop
    
    Cell In [1], line 9, in Test.__getattr__(self, name)
          7 if name == 'abc':
          8     return 'abc'
    ----> 9 return self.__getattribute__(name)
    
    Cell In [1], line 12, in Test.my_prop(self)
         10 @property
         11 def my_prop(self):
    ---> 12     return deeply_nested_factory_fn()
    
    Cell In [1], line 3, in deeply_nested_factory_fn()
          1 def deeply_nested_factory_fn():
          2     a = 2
    ----> 3     return a.invalid_attr
    
    AttributeError: 'int' object has no attribute 'invalid_attr'