Why care about exceptions when analysing control flow?

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.

Different flavours of handlers, and their semantics

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.

1. 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:

  1. No exceptions are raised in the try block.
    1. The try block runs to completion, and the except block is skipped.
    2. Statements X in the try block are fully executed; statements Y in the except block are not executed.
  2. An exception is raised in the try block that is caught by the exception handler
    1. The 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 handler
    2. The exception handler runs to completion; statements Y 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:

  1. Examples 1 and 2 use exhaustive handlers which will catch all exceptions, but examples 3 and 4 use fine-grained handlers which will only catch specific exceptions. This means that it’s possible that an exception could be raised in the 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.
  2. An exception could be raised by one of the statements inside the exception handler. However, again, this is uninteresting from the perspective of control flow in this situation, as it will lead to irrevocable and immediate termination.

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.

2. try statements with multiple excepts