[Python-ideas] the error that raises an AttributeError should be passed to __getattr__

Steven D'Aprano steve at pearwood.info
Mon Jun 19 20:18:19 EDT 2017


On Mon, Jun 19, 2017 at 04:06:56PM -0500, Jason Maldonis wrote:
> Hi everyone,
> 
> A while back I had a conversation with some folks over on python-list. I
> was having issues implementing error handling of `AttributeError`s using
> `__getattr__`.
[...]
> For example, we cannot tell the difference between `A.x` not existing
> (which would raise an AttributeError) and some attribute inside `A.x` not
> existing (which also raises an AttributeError).

I didn't understand what you were talking about here at first. If you 
write something like

A.x.y

where y doesn't exist, it's A.x.__getattr__ that is called, not 
A.__getattr__. But I went and looked at the thread in Python-Ideas and 
discovered that you're talking about the case where A.x is a descriptor, 
not an ordinary attribute, and the descriptor leaks AttributeError.

Apparently you heavily use properties, and __getattr__, and find that 
the two don't interact well together when the property getters and 
setters themselves raise AttributeError. I think that's relevant 
information that helps explain the problem you are hoping to fix.

So I *think* this demonstrates the problem:

class A(object):
    eggs = "text"
    def __getattr__(self, name):
        if name == 'cheese':
            return "cheddar"
        raise AttributeError('%s missing' % name)
    @property
    def spam(self):
        return self.eggs.uper() # Oops.


a = A()
a.spam



Which gives us 

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __getattr__
AttributeError: spam missing


But you go on to say that:

> This is evident from the
> stack trace that gets printed to screen, but `__getattr__` doesn't get that
> stack trace.

I can't reproduce that! As you can see from the above, the stack trace 
doesn't say anything about the actual missing attribute 'uper'.

So I must admit I don't actually understand the problem you are hoping 
to solve. It seems to be different from my understanding of it.


 
> I propose that the error that triggers an `AttributeError` should get
> passed to `__getattr__` (if `__getattr__` exists of course).  Then, when
> handling errors, users could dig into the problematic error if they so
> desire.

What precisely will be passed to __getattr__? The exception instance? 
The full traceback object? The name of the missing attribute? Something 
else? It is hard to really judge this proposal without more detail.

I think the most natural thing to pass would be the exception instance, 
but AttributeError instances don't record the missing attribute name 
directly (as far as I can tell). Given:

try:
    ''.foo
except AttributeError as e:
    print(e.???)

there's nothing in e we can inspect to get the name of the missing 
exception, 'foo'. (As far as I can see.) We must parse the error message 
itself, which we really shouldn't do, because the error message is not 
part of the exception API and could change at any time.

So... what precisely should be passed to __getattr__, and what exactly 
are you going to do with it?

Having said that, there's another problem: adding this feature (whatever 
it actually is) to __getattr__ will break every existing class that uses 
__getattr__. The problem is that everyone who writes a __getattr__ 
method writes it like this:

    def __getattr__(self, name):

not:

    def __getattr__(self, name, error):

so the class will break when the method receives two arguments 
(excluding self) but only has one parameter.

*If* we go down this track, it would probably require a __future__ 
import for at least one release, probably more:

- in 3.7, use `from __future__ import extra_getattr_argument`

- in 3.8, deprecate the single-argument form of __getattr__

- in 3.9 or 4.0 no longer require the __future__ import.


That's a fairly long and heavy process, and will be quite annoying to 
those writing cross-version code using __getattr__, but it can be done. 

But only if it actually helps solve the problem. I'm not convinced that 
it does. It comes down to the question of what this second argument is, 
and how do you expect to use it?



-- 
Steve


More information about the Python-ideas mailing list