Surprises with hasattr and the property decorator

Some Python fun: what does the following code print? And why?

import os

class A:
    @property
    def meth0(self):
        return None

    @property
    def meth1(self):
        return os.whoops

    @property
    def meth2(self):
        return whoops

    
a = A()
print(hasattr(a, 'meth0'))
print(hasattr(a, 'meth1'))
print(hasattr(a, 'meth2'))

The answer could be a little irritating:

True
False
Traceback (most recent call last):
  File "test.py", line 19, in <module>
    print(hasattr(a, 'meth2'))
  File "test.py", line 14, in meth2
    return whoops
NameError: name 'whoops' is not defined

It would be nice (or perhaps less surprising) if the output were instead:

True
True
True

So what’s happening here? A combination of things:

  • hasattr tests for the presence of an attribute by calling getattr on the attribute name, and catching AttributeError.
  • When getattr is called for meth1 it throws an AttributeError – not because meth1 doesn’t exist, but because os.whoops doesn’t – this propagates until it is caught by hasattr, which then concludes that the object has no method meth1.
  • When getattr is called for meth2, a NameError is thrown instead, which isn’t caught by hasattr, so we observe a different behaviour that can appear “inconsistent” with the behaviour for meth1.

Sometimes the behaviour exhibited with meth1 can mask an AttributeError due to a bug in the code for a property. For example:

import socket

class Data:
    def __init__(self, payload):
        self.payload = payload

class Packet(Data):
    @property
    def ip_address(self):
        hostname = socket.gethostname()
        # Type: should be `gethostbyname`
        return socket.getbyname(hostname)

class Frame(Data):
    @property
    def ethernet_address(self):
        return "00:11:22:33:44:55"

# Create a packet
p = Packet("HELLO WORLD")

# Handle p based on whether it is a Frame or a Packet

if hasattr(p, 'ip_address'):
    print("Packet with payload '%s'" % p.payload)
else:
    print("Frame with payload '%s'" % p.payload)

The output is, (maybe) surprisingly:

Frame with payload 'HELLO WORLD'

p is actually a Packet but it is mistreated as a Frame – in this straight-line, rather contrived example, it appears clear what’s happening. In the context of a larger program where the code is spread across multiple modules and functions along with other code, tracing the source of such a problem can be a little more time consuming and confusing – I encountered exactly this issue recently when debugging an issue in Numba, which left enough of an impression on me to motivate this post!

Leave a comment