Unravelling `from` for `raise` statements
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 TypeError
.
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 type
(as inspect.issclass()
shows us); isinstance(obj, type)
. To tell if something is an exception instance, it needs to be an instance of BaseException
; isinstance(obj, BaseException)
. And to tell if a class is an exception class, we need to know if it subclasses BaseException
; 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
raise
clause, raiseTypeError
. - Rely on the fact that assigning anything other than an exception instance to
__cause__
raises the appropriateTypeError
for us. - Don't assign anything if the
from
clause evaluates toNone
.
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.