Exceptions B: else and finally — the Full Structure

Lesson goals
  • Explain when each of the four blocks runs — try, except, else, finally
  • Use else for code that runs only if there was no error
  • Use finally for cleanup actions that must always happen
  • Predict the order of the prints in a program with a full try

What will we learn?

In the previous lesson we used try and except. The structure has two more parts:

  • else — runs only if there was no error.
  • finally — runs always, with or without an error.

Why do we need this?

Imagine code that opens a connection to a Database (the backend and databases topics), performs an action and closes the connection. What happens if the action fails in the middle? If the closing is written after the action, it will not be executed, and the connection stays open. After a hundred such failures, the server will crash because it ran out of connections.

finally guarantees that the cleanup happens in every case. else keeps the try small, and makes it clear what runs only after success.

Explanation from the basics

The four blocks

BlockWhen it runsWhat it is for
tryYou always start with itThe code that may fail
exceptOnly if an error of the matching type was thrownHandling the error
elseOnly if no error at all was thrown in the tryContinuing that depends on success
finallyAlways, at the end, in every caseCleanup: closing files, connections, a finish message
Analogy: cooking

try = you try to bake a cake. except = if it burned, you order a cake from the bakery. else = if it came out well, you decorate it (you don’t decorate a burned cake!). finally = in any case, you turn off the oven and wash the dishes.

Why else and not simply continue in the try?

You can put the follow-up code inside the try. But then, if that code fails with the same error, the except will catch it by mistake, and handle the wrong problem. else makes it clear: only the risky line inside the try, and the rest outside.

finally runs even when…

  • There was an error and it was caught.
  • There was an error and it was not caught. finally runs, and then the program crashes.
  • There is a return in the middle of the try. finally runs before the function really returns.

Syntax

try:
    # risky code
except SomeError:
    # runs only if SomeError happened
else:
    # runs only if NO error happened in try
finally:
    # runs ALWAYS, last

Required order: try → except (one or more) → else (optional) → finally (optional). You can also have try + finally without except.

First code example

week-2/full_try.py:

def convert(text):
    print("--- converting:", text)
    try:
        number = int(text)
    except ValueError:
        print("except: not a number")
    else:
        print("else: success, doubled =", number * 2)
    finally:
        print("finally: done with", text)

convert("21")
convert("hi")

Output:

--- converting: 21
else: success, doubled = 42
finally: done with 21
--- converting: hi
except: not a number
finally: done with hi

Breaking down the code line by line

Blockconvert("21")convert("hi")
try: number = int(text)Succeeds: number = 21ValueError
except ValueError:SkippedRuns
else:Runs: prints 42Skipped
finally:RunsRuns

else and except never both run: either there was an error, or there was not. finally, on the other hand, runs in both cases.

Another example

A “connection” that must be closed. Here we simulate a connection to an external resource (like a Database), and see that finally closes it both when something fails in the middle and when there is a return. week-2/connection_demo.py:

def open_connection():
    print("[connection opened]")

def close_connection():
    print("[connection closed]")

def fetch_price(product_name, prices):
    open_connection()
    try:
        price = prices[product_name]
    except KeyError:
        print("No such product:", product_name)
        return None
    else:
        print("Found", product_name)
        return price
    finally:
        close_connection()

catalog = {"bread": 8, "milk": 6}

print("Result:", fetch_price("milk", catalog))
print()
print("Result:", fetch_price("cake", catalog))

Output:

[connection opened]
Found milk
[connection closed]
Result: 6

[connection opened]
No such product: cake
[connection closed]
Result: None

Breakdown:

  • catalog = {"bread": 8, "milk": 6} — this is a dict (dictionary): every key ("bread") is mapped to a value (8). prices["milk"] returns 6. We will learn it in depth in the data structures and functions topic. For now it is enough to know that accessing a key that does not exist throws a KeyError.
  • Notice: there is a return both in except and in else. And still [connection closed] is printed before the Result. That is, finally runs after the return was “decided”, but before the function really returns.
  • Without finally, we would have to write close_connection() before every return. Easy to forget, and a lot of duplication.

Common mistake

Mistake 1: putting cleanup after the try/except instead of in finally.

open_connection()
try:
    price = prices[name]
except KeyError:
    print("missing")
    return None
close_connection()   # never reached when except returns!

In the case of a KeyError, the return leaves the function, and close_connection() does not run. The connection stays open. finally solves exactly this.

Mistake 2: the follow-up code inside the try, and an except that catches the wrong error.

try:
    age = int(text)
    category = ["child", "adult"][age // 18]   # can also raise IndexError... or ValueError?
except ValueError:
    print("Age must be a number")

If the follow-up line fails, for example with an IndexError for age 40 (40 // 18 = 2, and there is no index 2), the message is completely unrelated. Put only int(text) inside the try, and the follow-up in else.

Mistake 3: thinking that finally “catches” the error. It does not. If there is no matching except, finally runs, and after that the program crashes with a Traceback.

Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

Exercise 1: Predict the output

Write what will be printed for each call — before running it:

def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)

half("8")
half("x")
Show solution
def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)


half("8")
half("x")
half is 4.0
checked 8
not a number: x
checked x
  • For "8" there is no error, so else runs, followed by finally.
  • For "x" a ValueError is thrown, so except runs — and else does not run. finally runs in both cases.

Exercise 2: finally runs even after return

def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)

print(check(5))
print(check(-2))

What will be printed, and in which order? Explain why.

Show solution
def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)


print(check(5))
print(check(-2))
done checking 5
positive
done checking -2
not positive

finally runs before the function really returns — even when there is a return inside the try. That is why “done checking” is printed before the value the function returned.

Exercise 3: The four blocks

Write a function parse_quantity(text) that uses all four blocks:

  • try — converts the text to a number;
  • except — prints invalid: <text>;
  • else — prints quantity: <number>;
  • finally — prints ---.
parse_quantity("3")
parse_quantity("three")

Expected output:

quantity: 3
---
invalid: three
---
Show solution
def parse_quantity(text):
    try:
        quantity = int(text)
    except ValueError:
        print("invalid:", text)
    else:
        print("quantity:", quantity)
    finally:
        print("---")


parse_quantity("3")
parse_quantity("three")
quantity: 3
---
invalid: three
---

The code that runs only when everything succeeded is in else and not inside the try — that way the try stays small, and catches only the line that can really fail.

Exercise 4: Closing what we opened

The functions open_register() and close_register() open and close a cash register. Write a function sell(prices, position) that opens the register, returns the price at the chosen position (the position is an index of the list — it starts from 0), and always closes the register — even when the position does not exist. A position that does not exist returns None.

def open_register():
    print("register opened")

def close_register():
    print("register closed")

print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))

Expected output:

register opened
register closed
20
register opened
register closed
None
Show solution
def open_register():
    print("register opened")


def close_register():
    print("register closed")


def sell(prices, position):
    open_register()
    try:
        return prices[position]
    except IndexError:
        return None
    finally:
        close_register()


print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))
register opened
register closed
20
register opened
register closed
None

That is exactly the job of finally: cleanup — closing a resource that was opened, no matter how the function ended.

Exercise 5: else or inside the try?

Two developers wrote the same code in two ways:

# A                              # B
try:                             try:
    number = int(text)               number = int(text)
    print(100 / number)          except ValueError:
except ValueError:                   print("not a number")
    print("not a number")        else:
                                     print(100 / number)

What is the difference, and which version is better? What will happen in each one when text is "0"?

Show solution
  • In A, the division is also inside the try. In B, only the conversion is inside the try, and the division moved to else.
  • When text is "0", both versions throw a ZeroDivisionError, which no except catches, and the program crashes. But in B it is completely clear that the except ValueError belongs only to the conversion.
  • B is better: a try as small as possible. That way except does not catch by mistake errors that come from other lines, and it is easy to see what each part does.

Summary

  • try — risky code. except — only if a matching error happened. else — only if there was no error. finally — always.
  • except and else never run together.
  • finally runs even after return, and also when the error was not caught (and then the program crashes after it).
  • else keeps the try small and focused. finally is the place for cleanup: closing files and connections.

Official documentation