When defining a descriptor class, you can simply raise an AttributeError to signal that the attribute isn't available. For example:
from typing import Any, TypeVar, MutableMapping
V = TypeVar("V")
class Desc:
def __init__(self, key: str) -> None:
self.key = key
def __get__(self, instance: Any, owner: type) -> V:
try:
return instance._dict[self.key]
except KeyError:
raise AttributeError()
class C:
foo = Desc("d")
def __init__(self, d: MutableMapping[str, V]) -> None:
self._dict = d
Use like:
>>> d1 = dict(d=0, bar=0, baz=0)
>>> c = C(d1)
>>> c.foo
0
>>> d1.update(d=1)
>>> c.foo
1
>>> hasattr(C(dict()), "foo")
False
Note that the AttributeError causes the hasattr function to "fail silently". This is as expected and described by the descriptor protocol.
However, in some cases you may want the AttributeError to "bubble up" to the top. For example:
class NewDesc:
def __get__(self, instance, owner):
do_complex_task(instance) # <-- AttributeError occurs here
return instance._on_the_fly_private_attr
I have found that this is easily handled by wrapping the offending line of code in a try-except block that raises some other error different than AttributeError:
try:
do_complex_task(instance) # <-- AttributeError occurs here
except AttributeError as e:
raise MyError() from e
Is this the canonical way of handling this problem? What pitfalls am I leaving myself open to by doing it this way?