Why is getattr() throwing an exception when an attribute doesn't exist?

Nathan Osman picture Nathan Osman · Jul 17, 2014 · Viewed 10.4k times · Source

This one has me baffled. Consider the following Django models - representing zookeepers and the cages at the zoo that they are responsible for cleaning:

class Zookeeper(moodels.Model):
    name = models.CharField(max_length=40)

class Cage(models.Model):
    zookeeper = models.ForeignKey(Zookeeper)

Now suppose I want to connect a receiver to the Cage's post_init signal:

@receiver(models.signals.post_init, sender=Cage)
def on_cage_init(instance, **kwargs):
    print instance.zookeeper

As expected, this raises an exception since the Cage has not yet been assigned to a Zookeeper. Consider the following modification to the body of the receiver:

print getattr(instance, 'zookeeper', 'No Zookeeper')

One would expect this to print "No Zookeeper" since one has not been assigned to the instance. Instead, an exception is raised:

Traceback (most recent call last):
  File "../zoo/models.py", line 185, in on_cage_init
    print getattr(instance, 'zookeeper', 'No Zookeeper')
  File "/usr/local/lib/python2.7/dist-packages/django/db/models/fields/related.py", line 324, in __get__
    "%s has no %s." % (self.field.model.__name__, self.field.name))
DoesNotExist: Cage has no zookeeper.

Why is it raising an exception? Isn't getattr() supposed to return the provided default value if the attribute does not exist? I can prove that the attribute does not exist with:

print hasattr(instance, 'zookeeper')

...which prints False.

Answer

metatoaster picture metatoaster · Jul 17, 2014

Most likely that class has __getattribute__ defined. See this example:

>>> class O(object):
...     def __getattribute__(self, name):
...         raise Exception("can't get attribute")
...
>>> o = O()
>>> getattr(o, 'test', 'nothing')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __getattribute__
Exception: can't get attribute

Note how getattr essentially calls o.__getattribute__ internally, and if that raises a generic exception, it will just fail with that exception.

However, if it was properly define to raise AttributeError, getattr will catch that properly.

>>> class O(object):
...     def __getattribute__(self, name):
...         raise AttributeError("can't get attribute")
...
>>> o = O()
>>> getattr(o, 'test', 'nothing')
'nothing'

So this could be considered as a bug in the definition of the DoesNotExist exception where it does not correctly inherit from AttributeError as it should have.

A more complete example to demonstrate all of the above:

>>> class O(object):
...     def __getattribute__(self, name):
...         if name == 'test':
...             return 'good value'
...         elif name == 'bad':
...             raise Exception("don't raise this")
...         else:
...             raise DoesNotExist()
...
>>> class DoesNotExist(AttributeError):
...     pass
...
>>> o = O()
>>> getattr(o, 'test', 'nothing')
'good value'
>>> getattr(o, 'something', 'nothing')
'nothing'
>>> getattr(o, 'bad', 'nothing')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __getattribute__
Exception: don't raise this

Of course, all that above doesn't exactly help you in working around that bug. Rather than waiting for that bug to be resolved, just implement your getattr that traps that exception (or any other exceptions that you might be expecting). Something like this might work:

def safe_getattr(obj, name, default):
    try:
        return getattr(obj, name, default)
    except Exception:  # or your specific exceptions
        return default