Search code examples
pythondjangodjango-modelsgetattr

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


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.


Solution

  • @metatoaster's explanation is really good and this is basically what is happening. See __get__ magic method defined here.

    As a solution, I would apply "Easier to ask for forgiveness than permission" principle. Try getting the attribute and catch the specific exception:

    from django.core.exceptions import ObjectDoesNotExist
    
    try:
        print instance.zookeeper
    except ObjectDoesNotExist:
        print "No zookeeper"