HSHimanshu Sharma
Python Notes

Python exception handling: try, except, else, finally, raise and assert

Python exception handling: try, except, else, finally, raise and assert Advertisement

Programs fail. A file is missing, a user types letters where a number was expected, a network call times out. What matters is whether your program crashes with an unreadable message or handles the problem sensibly. Python reports problems as exceptions, and the try statement is how you catch them. This post explains the kinds of errors, the full try, except, else, finally structure and the order in which blocks run, how to raise and define exceptions, and how to log them and test assumptions with assert. Outputs were produced on Python 3.9.

Syntax errors and runtime errors

There are two broad kinds of error.

A syntax error means the code is not valid Python, so the program never starts. Python points at the problem:

try:
    compile("print('hello'", "demo", "exec")
except SyntaxError as e:
    print(type(e).__name__, "-", e.msg)

This printed SyntaxError - unexpected EOF while parsing on Python 3.9; newer versions word it better ('(' was never closed on 3.10 and later). You fix these by correcting the code; you cannot try your way around them in the file itself.

A runtime error happens while a valid program runs: dividing by zero, opening a missing file, converting "abc" to an integer. Python raises an exception, an object that describes the problem. If nothing handles it, the program stops and prints a traceback.

What the default behaviour looks like

def divide(a, b):
    return a / b

print(divide(10, 2))
print(divide(1, 0))
print("never reached")

The first call prints 5.0. The second stops the program with:

Traceback (most recent call last):
  File "demo.py", line 5, in <module>
    print(divide(1, 0))
  File "demo.py", line 2, in divide
    return a / b
ZeroDivisionError: division by zero

(Real tracebacks show the full path of the file; it is shortened here.) Read a traceback from the bottom: the last line gives the exception type and message; the lines above show the chain of calls, with the most recent call last. The last print never ran.

The exception hierarchy

Exceptions are classes, arranged in a tree. All of them inherit from BaseException; the ones you normally handle inherit from Exception:

print(ZeroDivisionError.__mro__[:4])
# (<class 'ZeroDivisionError'>, <class 'ArithmeticError'>, <class 'Exception'>, <class 'BaseException'>)
print(issubclass(KeyError, LookupError), issubclass(FileNotFoundError, OSError))    # True True

The commonly met ones:

Exception Typical cause
ValueError right type, wrong value: int("abc")
TypeError wrong type: "a" + 1
ZeroDivisionError 1 / 0, 5 % 0
IndexError list index out of range
KeyError missing dictionary key
AttributeError missing attribute or method
NameError using a name that does not exist
FileNotFoundError opening a missing file (a kind of OSError)
ImportError, ModuleNotFoundError a module cannot be imported
KeyboardInterrupt the user pressed Ctrl+C (inherits BaseException)

Catching a parent class catches all its children, so except LookupError handles both KeyError and IndexError. KeyboardInterrupt and SystemExit sit outside Exception on purpose, so that except Exception does not stop people from quitting a program.

try and except

Put the risky code in try; put the recovery in except.

try:
    n = int("abc")
except ValueError:
    n = 0
print(n)             # 0

If no exception happens, the except block is skipped. If an exception happens, the rest of the try block is skipped and Python looks for a matching except.

Getting information about the exception

Use as to name the exception object:

try:
    int("abc")
except ValueError as e:
    print(type(e).__name__, "|", e, "|", e.args)

This printed ValueError | invalid literal for int() with base 10: 'abc' | ("invalid literal for int() with base 10: 'abc'",). traceback.print_exc() prints the full traceback without stopping the program.

Handling several exceptions

Several except blocks handle different problems differently. Python uses the first one that matches, so put specific classes before general ones.

def safe_div(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return "cannot divide by zero"
    except TypeError:
        return "numbers only"

print(safe_div(6, 3), safe_div(1, 0), safe_div("a", 2))
# 2.0 cannot divide by zero numbers only

One block can catch several types by listing them in a tuple:

try:
    {}["x"]
except (KeyError, IndexError) as e:
    print("lookup failed:", repr(e))        # lookup failed: KeyError('x')

The default except

A bare except: catches everything, including KeyboardInterrupt. It hides bugs and makes a program hard to stop. If you need a catch-all, write except Exception as e: and at least log e. Better still, name the exceptions you expect.

else and finally

The full structure has two optional extras:

def run(text):
    try:
        n = int(text)
    except ValueError:
        print("except: not a number")
    else:
        print("else: got", n)
    finally:
        print("finally: always runs")

run("5")
run("x")
else: got 5
finally: always runs
except: not a number
finally: always runs
  • else runs only if the try block finished without an exception. Keep only the code that depends on the success here, so that errors in it are not accidentally caught by the except above.
  • finally runs always: after success, after a handled exception, and even when an exception is not handled, or the block uses return or break. Use it to release resources (close a file or a connection).

Because finally runs even on return, this works as you might not expect:

def f():
    try:
        return "from try"
    finally:
        print("cleanup")

print(f())       # cleanup, then: from try

Nowadays the with statement handles most clean-up cases (files, locks, connections) more neatly than finally; see the file handling post.

Control flow in nested try blocks

A try can sit inside another. If the inner try has no matching except, the exception travels outward to the enclosing one, and finally blocks run on the way:

try:
    try:
        1 / 0
    except KeyError:
        print("inner except")
    finally:
        print("inner finally")
except ZeroDivisionError:
    print("outer except")
finally:
    print("outer finally")
inner finally
outer except
outer finally

The inner except KeyError did not match, so the inner finally ran and the exception moved to the outer block. The video lessons Episode 51: Control Flow in Nested try-except-else-finally Blocks and Episode 52: 25 Possible Combinations of try-except-else-finally Blocks go through these combinations slowly.

Which combinations are legal

A try must have at least one except or a finally. else is allowed only if there is an except, and it must come after all of them. finally is last. So try/except, try/finally, try/except/else, try/except/finally and try/except/else/finally all work, while try alone, or try/else, is a SyntaxError.

Raising exceptions

You raise an exception with raise when your own code detects a problem:

def set_age(age):
    if not isinstance(age, int):
        raise TypeError("age must be an integer")
    if age < 0:
        raise ValueError(f"age cannot be negative: {age}")
    return age

for value in [30, -5, "x"]:
    try:
        print(set_age(value))
    except (TypeError, ValueError) as e:
        print(type(e).__name__ + ":", e)
30
ValueError: age cannot be negative: -5
TypeError: age must be an integer

Inside an except block, a bare raise re-raises the same exception after you have done something with it, such as logging. raise NewError(...) from original raises a new exception and records the original as its cause, which keeps the full story in the traceback:

def load(text):
    try:
        return int(text)
    except ValueError as e:
        raise RuntimeError("bad configuration value") from e

try:
    load("x")
except RuntimeError as e:
    print(e, "| caused by:", repr(e.__cause__))
# bad configuration value | caused by: ValueError("invalid literal for int() with base 10: 'x'")

Defining your own exceptions

Create a class that inherits from Exception. A custom type lets callers catch exactly your problem.

class InsufficientFunds(Exception):
    def __init__(self, balance, amount):
        super().__init__(f"balance {balance} is less than {amount}")
        self.balance = balance
        self.amount = amount

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFunds(balance, amount)
    return balance - amount

try:
    withdraw(100, 250)
except InsufficientFunds as e:
    print(e, "| short by", e.amount - e.balance)
# balance 100 is less than 250 | short by 150

Name custom exceptions after the problem and end the name with Error or leave it descriptive. For a large library, make one base class (ShopError) and derive the specific ones from it.

Logging exceptions

In real programs, print is not enough: you want errors written with the time and the full traceback. The logging module does this. logger.exception() inside an except block records the message plus the traceback:

import logging
logging.basicConfig(format="%(levelname)s:%(message)s")
try:
    1 / 0
except ZeroDivisionError:
    logging.exception("calculation failed")

On the screen this shows ERROR:calculation failed, followed by the traceback ending in ZeroDivisionError: division by zero. See the logging post for levels, files and handlers, and the video Episode 53: Python Logging.

Debugging with assert

assert condition, message raises AssertionError when the condition is false. It checks things that must be true if your code is correct, such as an internal assumption, and is meant for finding bugs, not for validating user input.

def average(values):
    assert len(values) > 0, "values must not be empty"
    return sum(values) / len(values)

print(average([2, 4]))
try:
    average([])
except AssertionError as e:
    print("AssertionError:", e)
# 3.0
# AssertionError: values must not be empty

Running Python with the -O option removes assertions entirely, so never depend on them for security or input checks; use raise ValueError for those.

Good practice

  • Catch the most specific exception you can, and wrap as little code as possible in try.
  • Do not use exceptions for ordinary decisions where an if is clearer (for example if key in d), although "ask forgiveness" style (try: d[key]) is common and accepted in Python when the failure is rare.
  • Never write except: pass. A silent failure is the hardest bug to find.
  • Clean up with with or finally.
  • Give error messages that help: say what was wrong and what was expected.
  • Let errors you cannot handle propagate to a place that can, instead of printing and carrying on.

Common mistakes

  • Putting a general except Exception before specific ones, so they never run.
  • Using a bare except: that also catches Ctrl+C.
  • Placing too much code in the try block, so a bug elsewhere is reported as the wrong error.
  • Using assert to validate input.
  • Forgetting that finally runs even after return.
  • Catching an exception just to print it and then continuing as if nothing happened.

Practice

Write a function read_number(prompt) that keeps asking until the user types a valid integer, using try, except and else. Write a read_config(path) function that raises a custom ConfigError with the original FileNotFoundError as its cause. Finally, write a decorator that logs any exception raised by the function it wraps and then re-raises it.

Continue

Errors are most useful with good input handling, functions and classes. The Python interview questions include exception questions to test yourself, and the Python video course covers this chapter in several lessons.

Advertisement