Search code examples
pythonpython-3.xtry-exceptfinally

Python docs have misleading explanation of return in finally


I was going through the python docs to improve my core python and I was reading about errors and exceptions

In the doc it says

If a finally clause includes a return statement, the finally clause’s return statement will execute before, and instead of, the return statement in a try clause.

It also provides this example below:

def bool_return():
    try:
        return True
    finally:
        return False

bool_return()

Now looking at this example the above statement seems straight and fair enough, but if you modify that example a bit to make it look like this:

def bool_return():
    try:
        return print("foo")
    finally:
        return False

bool_return()

Now, if you run this you will see that foo will be printed and False will be returned. Now the doc says that finally clause's return will execute before, and instead of, try clause's return statement. If so, then why I can see the foo being printed?

I debugged this snippet with pycharm and it shows that the try clause's return statement is executed first and the string is printed and then it's output which is None is returned due to return statement, and the return statement in the finally clause will be executed later, which is the last return of the program so the function overrides previous return and False is returned.

My question is:

1) Why does doc say finally clause's return statement is executed before?

2) Why does doc say finally clause's return statement is executed instead of try clause's return statement?

I believe both the statements are the opposite of what happens in reality.

EDIT:

After reading @iBug's answer it is clear now that how the print("foo") is evaluated but None is not returned. Basically, the expression is evaluated first and then return happens. Later on return False in finally is executed. Which makes clear why we get the output that we did.

Still, I see that the return False in finally is executed after the return print("foo") of try.

Or as per @iBug's comment, 10 RETURN_VALUE is completely bypassed?

enter image description here

EDIT

This is now resolved in the documentation and it is correct now on what will be returned. However, if you wish to know "how" then read all the comments and answer carefully.


Solution

  • $ python3
    Python 3.7.5 (default, Nov 20 2019, 09:21:52)
    [GCC 9.2.1 20191008] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> def bool_return():
    ...     try:
    ...         return print("foo")
    ...     finally:
    ...         return False
    ...
    >>> import dis
    >>> dis.dis(bool_return)
      2           0 SETUP_FINALLY            8 (to 10)
    
      3           2 LOAD_GLOBAL              0 (print)
                  4 LOAD_CONST               1 ('foo')
                  6 CALL_FUNCTION            1
                  8 RETURN_VALUE
    
      5     >>   10 LOAD_CONST               2 (False)
                 12 RETURN_VALUE
    >>>
    

    As you can see above, return False does happen before the return statement in the try block, but after the to-be-returned value has been computed.

    I think the docs probably meant "the very action of returning" by return statement, or in other words, it didn't take into account the computation of the return value, which of course happens before it's returned.


    To observe whether 8 RETURN_VALUE is executed or not, you can compile CPython interpreter in debug mode and run it in GDB. A step-by-step guide would be too bloated for this answer, so I'll give an outline here (Linux).

    • Grab the CPython source code from an official source (python.org website or GitHub)
    • Configure the debug build ./configure --with-pydebug (you may want to give --prefix=/opt/python3-debug as well), make and make install
    • Start the debug Python in GDB: gdb /opt/python3-debug/bin/python3 and (gdb) r
    • Define the function bool_return as usual.
    • Locate the string RETURN_VALUE in Python/ceval.c, take down the line number (for 3.8.1, it's 1911).
    • Hang the Python interpreter by sending SIGTRAP, and set a breakpoint at the position from the previous step (b Python/ceval.c:1911), and then c.
    • Watch the breakpoint reached twice, with output like this:
    (gdb breakpoint info)
    False
    (gdb breakpoint info)
    
    • Observe how the breakpoint is reached once for every statement you enter in the REPL. This is to learn that the second breakpoint in the above step is caused by Python REPL, so only the first breakpoint comes from a return statement in the function.

    Now that it's clear that only one return has been executed in the function, it must be 12 RETURN_VALUE, so the Python instruction 8 RETURN_VALUE isn't executed at all.