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
elseruns only if thetryblock finished without an exception. Keep only the code that depends on the success here, so that errors in it are not accidentally caught by theexceptabove.finallyruns always: after success, after a handled exception, and even when an exception is not handled, or the block usesreturnorbreak. 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
ifis clearer (for exampleif 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
withorfinally. - 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 Exceptionbefore specific ones, so they never run. - Using a bare
except:that also catches Ctrl+C. - Placing too much code in the
tryblock, so a bug elsewhere is reported as the wrong error. - Using
assertto validate input. - Forgetting that
finallyruns even afterreturn. - 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.