Epitome:

  • return doesn’t really return, it pushes value to stack, then jumps to finally, which was set up by SETUP_FINALLY before.
  • after executing finally, END_FINALLY returns.

Let’s be forthright, what will the following code print?

def func():
    try:
        100/0
    except ZeroDivisionError as e:
        print 'Zero cannot be divisor'
        return
    finally:
        print 'finally'

if __name__ == "__main__":
    func()

The answer is:

Zero cannot be divisor
finally

It seems that finally executes after the return statement. Is it a truth? We’d better change the code a little to penetrate the result.

#!/usr/bin/env python2
# coding=utf-8
from dis import dis

def func():
    try:
        100/0
    except ZeroDivisionError as e:
        print 'Zero cannot be divisor'
        return
    finally:
        print 'finally'

if __name__ == "__main__":
    dis(func)

Next point, we can see,

  6           0 SETUP_FINALLY           44 (to 47)
              3 SETUP_EXCEPT            12 (to 18)

  7           6 LOAD_CONST               1 (100)
              9 LOAD_CONST               2 (0)
             12 BINARY_DIVIDE
             13 POP_TOP
             14 POP_BLOCK
             15 JUMP_FORWARD            25 (to 43)

  8     >>   18 DUP_TOP
             19 LOAD_GLOBAL              0 (ZeroDivisionError)
             22 COMPARE_OP              10 (exception match)
             25 POP_JUMP_IF_FALSE       42
             28 POP_TOP
             29 STORE_FAST               0 (e)
             32 POP_TOP

  9          33 LOAD_CONST               3 ('Zero cannot be divisor')
             36 PRINT_ITEM
             37 PRINT_NEWLINE

 10          38 LOAD_CONST               0 (None)
             41 RETURN_VALUE
        >>   42 END_FINALLY
        >>   43 POP_BLOCK
             44 LOAD_CONST               0 (None)

 12     >>   47 LOAD_CONST               4 ('finally')
             50 PRINT_ITEM
             51 PRINT_NEWLINE
             52 END_FINALLY
             53 LOAD_CONST               0 (None)
             56 RETURN_VALUE

For those who didn’t use dis module before:

The dis module supports the analysis of CPython bytecode by disassembling it. According to the above output, first column is line number, the second is offset of bytecode, followed by the name of bytecode. The forth column presents parameter and the one in parenthesis is the result after bytecode’s processing parameter.

As we can see, after RETURN_VALUE in line 10, the END_FINALLY would be executed. That doesn’t meet our expectation. What’s more weird, there is another RETURN_VALUE in line 12 after END_FINALLY. Since we get here, the source code is the only thing that we can ask for help.

// ceval.c
        TARGET_NOARG(RETURN_VALUE)
        {
            retval = POP();
            why = WHY_RETURN;
            goto fast_block_end;
        }

fast_block_end:
        while (why != WHY_NOT && f->f_iblock > 0) {
            /* Peek at the current block. */
            PyTryBlock *b = &f->f_blockstack[f->f_iblock - 1];

            assert(why != WHY_YIELD);
            if (b->b_type == SETUP_LOOP && why == WHY_CONTINUE) {
                why = WHY_NOT;
                JUMPTO(PyInt_AS_LONG(retval));
                Py_DECREF(retval);
                break;
            }

            /* Now we have to pop the block. */
            f->f_iblock--;

            while (STACK_LEVEL() > b->b_level) {
                v = POP();
                Py_XDECREF(v);
            }
            if (b->b_type == SETUP_LOOP && why == WHY_BREAK) {
                why = WHY_NOT;
                JUMPTO(b->b_handler);
                break;
            }
            if (b->b_type == SETUP_FINALLY ||
                (b->b_type == SETUP_EXCEPT &&
                 why == WHY_EXCEPTION) ||
                b->b_type == SETUP_WITH) {
                if (why == WHY_EXCEPTION) {
                    PyObject *exc, *val, *tb;
                    PyErr_Fetch(&exc, &val, &tb);
                    if (val == NULL) {
                        val = Py_None;
                        Py_INCREF(val);
                    }
                    /* Make the raw exception data
                       available to the handler,
                       so a program can emulate the
                       Python main loop.  Don't do
                       this for 'finally'. */
                    if (b->b_type == SETUP_EXCEPT ||
                        b->b_type == SETUP_WITH) {
                        PyErr_NormalizeException(
                            &exc, &val, &tb);
                        set_exc_info(tstate,
                                     exc, val, tb);
                    }
                    if (tb == NULL) {
                        Py_INCREF(Py_None);
                        PUSH(Py_None);
                    } else
                        PUSH(tb);
                    PUSH(val);
                    PUSH(exc);
                }
                else {
                    if (why & (WHY_RETURN | WHY_CONTINUE))
                        PUSH(retval);
                    v = PyInt_FromLong((long)why);
                    PUSH(v);
                }
                why = WHY_NOT;
                JUMPTO(b->b_handler);
                break;
            }
        } /* unwind stack */

        /* End the loop if we still have an error (or return) */

        if (why != WHY_NOT)
            break;
        READ_TIMESTAMP(loop1);

    } /* main loop */

    assert(why != WHY_YIELD);
    /* Pop remaining stack entries. */
    while (!EMPTY()) {
        v = POP();
        Py_XDECREF(v);
    }

    if (why != WHY_RETURN)
        retval = NULL;

RETURN_VALUE does not return really, it simply assigns popped value from stack to retval, set why to WHY_RETURN, then go to fast_block_end. This pathetic scapegoat would push retval from RETURN_VALUE into stack, convert why to Long and push it into stack too, then jump to b->b_handler. Well, what’s b_handler pointing to?


// ceval.c
        // SETUP_FINALLY
        TARGET_WITH_IMPL(SETUP_LOOP, _setup_finally)
        TARGET_WITH_IMPL(SETUP_EXCEPT, _setup_finally)
        TARGET(SETUP_FINALLY)
        _setup_finally:
        {
            /* NOTE: If you add any new block-setup opcodes that
               are not try/except/finally handlers, you may need
               to update the PyGen_NeedsFinalizing() function.
               */

            PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg,
                               STACK_LEVEL());
            DISPATCH();
        }

        // END_FINALLY
        PREDICTED(END_FINALLY);
        TARGET_NOARG(END_FINALLY)
        {
            v = POP();
            if (PyInt_Check(v)) {
                why = (enum why_code) PyInt_AS_LONG(v);
                assert(why != WHY_YIELD);
                if (why == WHY_RETURN ||
                    why == WHY_CONTINUE)
                    retval = POP();
            }
            else if (PyExceptionClass_Check(v) ||
                     PyString_Check(v)) {
                w = POP();
                u = POP();
                PyErr_Restore(v, w, u);
                why = WHY_RERAISE;
                break;
            }
            else if (v != Py_None) {
                PyErr_SetString(PyExc_SystemError,
                    "'finally' pops bad exception");
                why = WHY_EXCEPTION;
            }
            Py_DECREF(v);
            break;
        }

// frameobject.c

/* Block management */
void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
    PyTryBlock *b;
    if (f->f_iblock >= CO_MAXBLOCKS)
        Py_FatalError("XXX block stack overflow");
    b = &f->f_blockstack[f->f_iblock++];
    b->b_type = type;
    b->b_level = level;
    b->b_handler = handler;
}

SETUP_FINALLY calls PyFrame_BlockSetup to setup a block with type, level, handler. From the output of dis(notice the arrows before END_FINALLY), we can guess this handler is END_FINALLY. END_FINALLY gets retval and why, pushed by RETURN_VALUE, from stack, and returns.

Now we can articulate all the scratch:

  • return doesn’t really return, it pushs value to stack, then jumps to finally, which was set up by SETUP_FINALLY before.
  • after executing finally, END_FINALLY returns.

Reference