As part of my series on Python's syntax, I want to tackle the
from clause for
raise statements. In case you're unfamiliar,
raise A from B causes
B to be assigned to
A.__cause__ which lets chained tracebacks exist (as well as
__context__, but that's not relevant to today's topic). There is a restriction that only instances of exceptions can be assigned to
__cause__, and if you specify an exception class then it gets instantiated before assignment. Trying to assign anything else triggers a
So the first question is how much checking do we need to do for the
from clause's object to make sure it meets the requirement of being an (eventual) exception instance?
It appears that only assigning an instance of an exception to
__cause__ is allowed. This is convenient as we don't have to do the check for something not being an exception instance. But it's also inconvenient as we do have to instantiate any exception class ourselves. What this says to me is the following:
- Assigning an exception instance is fine.
- Assigning a non-exception-related object is "fine" because the exception object will raise the exception.
- Assigning an exception class is problematic, and so we will have to handle the instantiation.
So how do we detect if an object is an exception class but not an instance? To tell if an object is a class, it needs to be an instance of
inspect.issclass() shows us);
isinstance(obj, type). To tell if something is an exception instance, it needs to be an instance of
isinstance(obj, BaseException). And to tell if a class is an exception class, we need to know if it subclasses
issubclass(obj, BaseException). Now one thing to note is
issubclass() will raise a
TypeError if you pass anything that isn't a class as its first argument;
isinstance() does not have the inverse issue of passing in a class as its first argument.
Oh, and we also have to make sure the object we are raising is also appropriately instantiated before we attempt any assignment to
__cause__. That adds an extra wrinkle to this problem as it means we will have to raise the
TypeError when the to-be-raised exception isn't an exception-related object.
This can all be summarized by the following function:
When we unravel
raise A from B, we can inline the logic and simplify it a bit:
- If we have an exception class, instantiate it.
- If we have a non-exception object for the
- Rely on the fact that assigning anything other than an exception instance to
__cause__raises the appropriate
- Don't assign anything if the
fromclause evaluates to
This lets us unravel
raise A from B to:
In the general case of
raise A we don't have to do any of this as it's part of Python's semantics to handle the class-to-instance scenario.