Let us first consider how type checkers must treat exceptions outside the context of exception handlers.
In general, (nearly) any AST structure in Python represents a node that could possibly raise an exception. Only nodes that represent simple assignments to literals, and other nodes that are made up of similarly simple nodes, can be statically determined to never raise exceptions.
Python’s typing system does not permit any way to express that a function could raise no exceptions, could raise a specific exception, or could raise arbitrary exceptions. Type checkers must therefore assume that any scope could terminate at any point (which might or might not then lead to the termination of the program as a whole, depending on whether the exception is caught by an outer scope). The consequence of this is that there are many potential errors that could occur during a Python scope, and which might lead to the scope and/or program irrevocably terminating, but which a Python type checker cannot catch. However, this does not, in general, affect a type checker’s analysis of control flow: if an exception is raised that terminates the scope, there is no further control flow from that point onwards, so there are no “future events” in the code on that path that would care about the fact that intermediate events might not have taken place. (Note that all control-flow analysis is necessarily local to a given Python scope.) In other words: we do not care about events that lead to a scope’s irrevocable termination.
Given this, why do we need to apply any special handling to exception handlers? The answer is that in the context of an exception handler, a raised exception does not necessarily lead to a scope’s irrevocable termination. When exception handlers are brought into the mix, exceptions can be temporarily suspended or indeed wholly recovered from; but suspending or recovering from an exception using an exception handler can **lead to whole blocks of code being skipped. This will naturally have an impact on the symbols we consider defined from the perspective of “future events” in the scope.
The DSL used here for the examples is as follows: a capital letter represents a “suite”: one or more statements that might be executed (fully or partially) in a try
, except
, else
or finally
block.
try
statements with a single except
try:
X
except:
Y
Z
try:
X
except BaseException:
Y
Z
try:
X
except TypeError:
Y
Z
try:
X
except (TypeError, ValueError):
Y
Z
From the perspective of control-flow analysis, these are all the same. There are two possibilities relevant to the question of which statements could or could not have been executed in their entirety by the time we get to statements Z
:
try
block.
try
block runs to completion, and the except
block is skipped.X
in the try
block are fully executed; statements Y
in the except
block are not executed.try
block that is caught by the exception handler
try
block does not run to completion; at some point during the execution of statements X
, an exception is raised and code flow jumps to the exception handlerY
in the except
block are all executed.There are of course more possibilities here, but they are not relevant for us to consider as they both lead to irrevocable and immediate termination of the current scope:
try
block that would not be caught by the handler in example 3 or 4. However, this would lead to irrevocable and immediate termination of the scope, so this is uninteresting to us.Of course: talking about “termination of the current scope” is not quite accurate when discussing possibilities (3) and (4), since if the try
/ except
block is an inner statement inside an enclosing try
/ except
block, the exception could still be caught by an outer exception handler or finally
statement! This does not significantly affect our conclusions here, however. For simplicity’s sake, when we refer to “termination of the current scope” in this document, it should be generally assumed that we mean “termination of the current scope (unless caught by an outer exception handler in the same scope)”. The semantics of nested exception handlers will be discussed in more detail later on.
try
statements with multiple except
s