FChris
FChris

Reputation: 338

Find out variable name of object that caused an exception

Let's consider the following code example which I will use to raise an AttributeError as an example.

def test(first, second):
    print("My age is " + first.age + " and my neighbour is " + second.age)

Say I have the following class.

class Dummy(object):
    def __init__(self):
        pass

If I call the function with

d = Dummy()
d.__setattr__("age", "25")
test(d, Dummy())

I will get an AttributeError because the second Dummy has no Attribute age. This is caused by second.age.

My question now is if there is a way that I can find out what the name of the variable is that causes the error. Looking at the source code it is obvious that it is second, but how can I find this out in an try except block?

Upvotes: 3

Views: 2581

Answers (3)

FChris
FChris

Reputation: 338

Ok, so I found a solution which should also work in case a class is not under my control. This solution only targets the AttributeError but should be extendable in case other Errors need to be caught.

We still have the same test function and the same Dummy class

def test(first, second):
    print("My name is " + first.age + " and I am here with " + second.age)

class Dummy(object):
    def __init__(self):
        pass

We can use a Proxy object to wrap each value we pass to the test function. This proxy object records if it sees an AttributeError by setting the _had_exception flag.

class Proxy(object):
    def __init__(self, object_a):
        self._object_a = object_a
        self._had_exception: bool = False

    def __getattribute__(self, name):
        if name == "_had_exception":
            return object.__getattribute__(self, name)

        obj = object.__getattribute__(self, '_object_a')
        try:
            return getattr(obj, name)
        except AttributeError as e:
            # Flag this object as a cause for an exception
            self._had_exception = True 
            raise e

And the call to the function looks as follows

d = Dummy()
d.__setattr__("age", "25")
p1 = Proxy(d)
p2 = Proxy(Dummy())
try:
    test(p1, p2)
except AttributeError as e:
    # Get the local variables from when the Error happened
    locals = e.__traceback__.tb_next.tb_frame.f_locals
    offender_names = []

    # Check if one of the local items is the same 
    # as one of our inputs that caused an Error
    for key, val in locals.items():
        if p1._had_exception:
            if p1 is val:
                offender_names.append(key)

        if p2._had_exception:
            if p2 is val:
                offender_names.append(key)

    print(offender_names) # ['second']

The end result is a list with all local variable names -- used in the called function -- which correspond to our wrapped inputs, that caused an exception.

Upvotes: 0

Olivier Melançon
Olivier Melançon

Reputation: 22324

For debug purpose, note that the error message explains what happened.

obj = object()
print(obj.does_not_exist)

Error message

AttributeError: 'object' object has no attribute 'does_not_exist'

It is thus clear which attribute raised the exception. You can also recover that information though the sys.exc_info function if you think you might need that information at runtime.

Narrow down your try-except

If that does not satisfy you, be aware that the purpose of a try-except statement is to catch exceptions you expect to happen. Thus if two different exceptions might arise in the same block, you might as well split it into two try-except statements.

def test(first, second):
    try:
        first_age = first.age
    except AttributeError:
        # Do something if first doest not have attribute age

    try:
        second_age = second.age
    except AttributeError:
        # Do something if second does not have attribute age

    print("My age is " + first.age + " and my neighbour is " + second.age)

Use hasattr

Another option might be to use hasattr to check if the attribute exist.

def test(first, second):
    if not hasattr(first, 'age'):
        # Do something

    if not hasattr(second, 'age'):
        # Do something else

    print("My age is " + first.age + " and my neighbour is " + second.age)

Upvotes: 2

PMende
PMende

Reputation: 5470

You could change your test definition to split up the accessing of the attributes:

def test(first, second):
    f_age = first.age
    s_age = second.age
    print(f"My age is {f_age} and my neighbour is {s_age}")

Then when you invoke test, you'll be able to trace it to the particular line.

Upvotes: 0

Related Questions