Of course you're not using Python 2 anymore! After all, Python 3 came out in 2006, and the Python 2 end of life date was 2015 January 1st, 2020. Oh, and pip dropped support for Python 2 last month. But just in case you're maintaining a legacy application that hasn't yet had the chance to upgrade, here's a Python 2 gotcha that once threw me off.

hasattr() is a handy builtin function that accepts two arguments (an object and a string attribute) and returns True when the object contains the attribute. When the attribute is missing, it captures the AttributeError and gracefully returns False instead. This enables developers to reserve Exceptions for exceptional cases instead of using them for regular flow control.

The Gotcha

In Python 2, hasattr() incorrectly captures and hides all thrown Exceptions, not just those due to a missing attribute.

Example

(Note: These code snippets were last tested in a Python 2.7.17 environment.)

Imagine a class with two properties, where one is implemented correctly and the other has a bug:

class MyClass(object):
    @property
    def my_working_property(self):
        return "This property exists."

    @property
    def my_buggy_property(self):
        return 0 / 0

my_instance = MyClass()

When accessing the correctly implemented property, hasattr() returns True  as expected:

print("Trial 1: Attempt to access a property that exists:")

if hasattr(my_instance, "my_property"):
    print(my_instance.my_working_property)
else:
    print("Does not exist.")
Trial 1: Attempt to access a property that exists:
This property exists.  # Correct!

When attempting to access a property that doesn't exist, hasattr() returns False as expected:

print("Trial 2: Attempt to access a property that does not exist:")

if hasattr(my_instance, "property_that_does_not_exist"):
    print(my_instance.property_that_does_not_exist)
else:
    print("Does not exist.")
Trial 2: Attempt to access a property that does not exist:
Does not exist.  # Correct!

However, when accessing a property that exists but has bugs, hasattr() unexpectedly returns False:

print("Trial 3: Attempt to access a property that raises an Exception:")

if hasattr(my_instance, "my_buggy_property"):
    print(my_instance.my_buggy_property)
else:
    print("Does not exist.")
Trial 3: Attempt to access a property that raises an Exception:
Does not exist.  # Weird

Wat? That property definitely exists!

What Happened?

That last trial threw a ZeroDivisionError that was captured and hidden by hasattr(), which handled the Exception by assuming the property did not exist.

It may seem graceful or even desirable to swallow Exceptions like this, but it actually makes it harder to discover bugs, resulting in fragile code. This can be especially frustrating when working with buggy third-party libraries.

It would have been better if hasattr() had failed fast and visibly. The output could have looked like this instead:

Trial 3: Attempt to access a property that raises an Exception:
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "<stdin>", line 7, in my_buggy_property
ZeroDivisionError: integer division or modulo by zero

Luckily for you, this can easily be done!

The Fix

There are 3 good options available:

Solution 1: Migrate to Python 3

With no code changes, here's the output for the same three trials above (Python 3.8.1):

Trial 1: Attempt to access a property that exists:
This property exists.

Trial 2: Attempt to access a property that does not exist:
Does not exist.

Trial 3: Attempt to access a property that raises an Exception:
Traceback (most recent call last):
    File "2020_06_14_hasattr_example.py", line 31, in <module>
      if hasattr(my_instance, "my_buggy_property"):
    File "2020_06_14_hasattr_example.py", line 8, in my_buggy_property
      return 0 / 0

If your codebase is easy enough to migrate to Python 3, it'll get other fixes like this for free, so I highly recommend this solution. Not sure where to start? Try 2to3.

Solution 2: Use getattr()

Replace all instances of hasattr() with getattr():

property1 = getattr(my_instance, "property_that_does_not_exist", None)
property2 = getattr(my_instance, "my_buggy_property", None)

In line 2 of the above snippet, the desired Exception is raised:

Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "<stdin>", line 7, in my_incorrect_property
ZeroDivisionError: division by zero

Notice that I've used a default value of None for missing attributes, but this can be changed to whatever works best. And when the parameter is omitted, an AttributeError is raised by default.

Solution 3: Catch the Exceptions Yourself

Replace all instances of hasattr() with a try/except block. The code is explicit and well-scoped, but it's also the most verbose:

try:
    my_instance.property_that_does_not_exist
except AttributeError:
    print("'property_that_does_not_exist' does not exist.")

Graceful output:

'property_that_does_not_exist' does not exist.

And that's it! The last two solutions may make hasattr() seem unusable in its Python 2 form, but it can be very convenient when you're confident in your code. Tests and typing can both help in that department. More on that in a future post.