3

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?

FChris
  • 308
  • 2
  • 13
  • 1
    Why does your `except` block have to know which object caused the error? – Aran-Fey Oct 04 '18 at 21:46
  • 1
    Not exactly sure what you mean. Do you want to view the traceback mid-runtime? https://stackoverflow.com/q/3702675/4180176 – Joshua Nixon Oct 04 '18 at 21:47
  • @Aran-Fey, what I am trying to do later is some form of type reconstruction, where I try to collect constraints (e.g. needed type has to have age attribute) for inputs by testing a function with multiple (potentially wrong) inputs. If I know the name of the variable that caused it, I can find out to which I can add the constraint. – FChris Oct 04 '18 at 22:05
  • @JoshuaNixon, viewing the traceback seemed like a start but all I can get out of it is something like ```AttributeError("'Dummy' object has no attribute 'name'",)```. But here Dummy is the Type of the variable, not its name. – FChris Oct 04 '18 at 22:08
  • In the specific case of AttributeError, could you not parse the exception and check attribute & classname vs your arguments? Also, some exceptions include more information than just the message. – JL Peyret Oct 06 '18 at 18:43
  • Yes, I was thinking of that as well. But I opted for the solution in the accepted answer, because I want a place where I the information of multiple exception that might occur later as well and I figured a proxy object would be a good place to start. Nevertheless for the sake of this question I think parsing the exception message would have also worked. – FChris Oct 07 '18 at 07:59

3 Answers3

2

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)
Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
  • Thank you for the extensive answer. I think this will work in case the test function is under my control. If, however, it is part of a class I cannot change, I won't get the variable name. – FChris Oct 04 '18 at 22:31
  • What do you mean by the variable name? In you test? In the original code? – Olivier Melançon Oct 04 '18 at 22:34
  • In the test method I call ```second.age``` which causes the error. I want to find out that it was the variable with the name/identifier ```second``` which caused the error. Maybe this is just not possible, I don't know – FChris Oct 04 '18 at 22:37
  • Then use the second solution: put a try except block around this line specifically – Olivier Melançon Oct 04 '18 at 22:38
0

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.

PMende
  • 5,171
  • 2
  • 19
  • 26
  • Yes, this would work. However, I try to found a solution which works even if the function that causes the error is not under my control. – FChris Oct 04 '18 at 21:57
0

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.

FChris
  • 308
  • 2
  • 13