assertRaises not detecting exceptions in __getattr__

A friend of mine asked for my help to find the cause of a bug he was seeing in a complex Python class he was writing.

The class was supposed to raise an exception when a certain method was called but the unit tests were not picking up that exception. I have to confess I was a little stumped at first but simplifying his code to the bare minimum made the problem more evident.

Here’s an even more simplified version of his code just to illustrate the problem:

    from unittest import TestCase


    class MyClass():

        def my_attr(self):
            raise AttributeError

        def __getattr__(self, name):
            raise AttributeError


    class MyTest(TestCase):

        def test_my_attr(self):
            c = MyClass()
            self.assertRaises(AttributeError, c.my_attr)

        def test_getattr(self):
            c = MyClass()
            self.assertRaises(AttributeError, c.foo)

The problem was that test_my_attr() was passing but test_getattr() was failing, which at first may seem odd because both methods in MyClass should raise an AttributeError but only one of them seemed to do so.

The explanation is actually quite simple: in test_my_attr(), c.my_attr is actually an attribute (a method, to be more specific) of MyClass but in test_getattr(), c.foo is not a method of MyClass, so c.__getattr__() should be executed instead, but in fact what happens is that the c.foo attribute access is evaluated before assertRaises() is executed, and thus the test never intercepts the exception.

So how do we deal with it? There are two ways to do it:

  1. With a context manager, so we can intercept the exception after assertRaises() is set up;
  2. Using a lambda function to delay the attribute access evaluation.

    # Context manager
    def test_getattr(self):
        c = MyClass()
        with self.assertRaises(AttributeError):
            c.foo()

    # Lambda function
    def test_getattr(self):
        c = MyClass()
        self.assertRaises(AttributeError, lambda: c.foo())

This is the type of thing that can trip up a novice Python developer and even more experienced ones, so here are a couple of solutions, for everyone’s enjoyment!

Do you know other ways of dealing with this scenario?