Definition
A program constantly meets situations it cannot resolve: the expected file is not there, a calculation asks for a division by zero, a requested key is missing from the dictionary. Carrying on regardless produces wrong results, discovered hours later; stopping and saying what went wrong leaves a chance of understanding.
Python picked the second, and that choice has a name: the exception. It is the signal a program sends when it refuses to carry on. It stops the current statement, then travels back up the call stack looking for a place set up to handle it. With nobody to take it, it exits the program and prints a traceback.
That upward journey is valuable: the place where the problem happens and the place where it is caught need not be neighbours. A function buried deep down reports the incident, and the calling code handles it, being the only one that knows what to do.
Catching it, on the other hand, means marking out the area to watch and then stating what to do if it fails.
try:
# if share_count is 0, this line raises ZeroDivisionError
total = amount / share_count
except ZeroDivisionError:
# we only get here if the announced exception was actually raised
total = 0The word is misleading: an exception is not exceptional at all. It is the normal way of reporting a problem, and a solid program raises plenty of them, deliberately.
A family, not a list
Python defines several dozen exception types. Listing them one by one in every except block would be endless, and forgetting a single one would let the program stop anyway.
Hence the choice to arrange them as a tree, through inheritance: each type descends from a more general one, and catching a parent catches all its children along with it. That is how you set the width of the net.
A handful of types keeps coming back. Here is what each one reports, the question to ask when an unfamiliar name shows up in a traceback.
| Exception | What it reports |
|---|---|
| TypeError | An operation on a type that does not support it |
| ValueError | The right type, but an unacceptable value |
| KeyError | A key missing from a dictionary |
| IndexError | A position past the end of a sequence |
| AttributeError | An attribute the object does not have |
| LookupError | The parent of KeyError and IndexError |
At the top of the tree sits Exception, the common ancestor of nearly everything: except Exception therefore catches just about anything.
An except Exception thrown too wide also swallows your own typos: a misspelled variable name becomes indistinguishable from a network outage, and the message printed names the wrong cause.
Raising your own
Exceptions are not reserved for the language. A function you write also receives requests it cannot honour, withdrawing 500 euros from an account holding 30, and the way it reports that decides what follows.
raise raises an exception from your own code, exactly as Python does from its own.
def withdraw(balance, amount):
if amount > balance:
# refuse before computing: no wrong amount leaves this function
raise ValueError("Amount exceeds balance")
return balance - amountReturning a fallback value, None or -1, feels gentler, and that is the problem: the checking falls to the caller, who will forget it on a tired day. The wrong value then flows through the program and only blows up much later, with no visible link to its cause.
An exception cannot be ignored by accident: either the caller deals with it, or the program stops on the offending line, where the problem was born.
Asking forgiveness rather than permission
One question of method remains: is it better to check that everything is in order before acting, or to act and then catch the failure? Python commits to the second road.
Checking that a file exists before opening it looks tidier, but it leaves a window during which the file can be deleted or locked. Opening it straight inside a try closes that window: the attempt and its failure are handled in a single move.
The normal path then reads in one go, and a finally block guarantees the clean-up, whether an exception was raised or not.
This preference has a name in the Python community: EAFP, for "easier to ask forgiveness than permission". The opposite approach is called LBYL, "look before you leap".
Frequently asked questions
What is the difference between an exception and a syntax error?
The moment of detection separates them. A SyntaxError is spotted while Python reads the file: nothing starts at all, and no catching block can rescue it. An exception happens during execution, in valid code that meets a situation it cannot handle.
Should every exception be caught?
No, only the ones you know how to handle. An exception reaching the top at least reports a real problem. Swallowed by an empty except, it gives you a program carrying on with inconsistent data, which costs far more than a clean stop.
Does an exception slow the program down?
Its cost only shows when it is actually raised, never on the normal path: a try block that triggers nothing costs almost nothing, which makes this style well suited to loops. Knowing what to catch and what to let through is a matter of judgement, and our Python course works the question through on concrete cases.