The exception family tree
Use the exception hierarchy to catch whole families of errors, order handlers correctly, and choose between look-first and ask-forgiveness code.
What you will learn
- Read the parent classes of an exception to predict which except clause catches it
- Order several except clauses so a specific handler is not hidden by a general one
- List several exception types in one except clause, with and without parentheses
- Choose between checking first (LBYL) and handling the error afterwards (EAFP)
On this page
In the try and except lesson you caught ValueError and KeyError by name, and you were told to be as specific as you can. This lesson explains how Python decides whether an except clause matches, because that rule is what makes “specific” and “general” mean something.
Exceptions have parents
Every exception type is built on another one. A handler for a type also catches everything built on that type. Python records the chain of parents on each type, in an attribute called __mro__:
for cls in KeyError.__mro__: print(cls.__name__)KeyErrorLookupErrorExceptionBaseExceptionobjectRead it from the top: a KeyError is a kind of LookupError, which is a kind of Exception, which is a kind of BaseException. (Every value in Python ends in object; you can ignore that line.) An except LookupError: clause therefore catches a KeyError, and so does except Exception:.
Here are the families you will meet most often. Each name in the right-hand column is built on the name on its left.
| Parent | Types built on it |
|---|---|
LookupError |
KeyError (missing dict key), IndexError (list index too big) |
ArithmeticError |
ZeroDivisionError, OverflowError |
OSError |
FileNotFoundError, FileExistsError, PermissionError, IsADirectoryError |
ValueError |
UnicodeError, which has UnicodeDecodeError and UnicodeEncodeError below it |
BaseException has a few children that are not under Exception, such as KeyboardInterrupt (the user pressed Ctrl+C) and SystemExit (raised by sys.exit()). That is the real reason a bare except: is dangerous: it also catches the user’s attempt to stop the program. except Exception: leaves those alone.
Catching a family
Use a parent type when you want to treat all its children alike. Looking something up by key or by position fails in two different ways, KeyError and IndexError, but a caller often does not care which:
def fetch(container, key): try: return container[key] except LookupError as error: return f"missing ({type(error).__name__})"
print(fetch({"a": 1}, "b"))print(fetch([10, 20], 5))print(fetch({"a": 1}, "a"))missing (KeyError)missing (IndexError)1type(error).__name__ gives the name of the exact type that was raised, so you can still report which child it was.
Order matters
Python checks the except clauses from top to bottom and runs the first one that matches. It does not look for the best match. If a parent comes first, it takes everything, and the child’s clause can never run:
try: {}["x"]except LookupError: print("general handler")except KeyError: print("specific handler")general handlerPython does not warn you about this. Put the most specific type first and the family after it:
try: {}["x"]except KeyError: print("specific handler")except LookupError: print("general handler")specific handlerSeveral types in one clause
When two unrelated types need the same response, list them in a tuple. This works in every version of Python:
try: number = int(None)except (ValueError, TypeError): number = 0print(number)0int(None) raises TypeError, while int("x") raises ValueError. One clause covers both.
Without the parentheses
Requires version 3.14 or later PEP 758 allows the parentheses to be left out when the clause has no as name:
for text in ["12", "x", None]: try: print(int(text)) except ValueError, TypeError: print("cannot convert", repr(text))12cannot convert 'x'cannot convert NoneTwo limits are worth remembering. If you want the exception object with as, the parentheses are required again: except (ValueError, TypeError) as error:. And on Python 3.13 and earlier the unparenthesised form is a SyntaxError, so code that has to run on older versions should keep the parentheses. Many teams do anyway, because the parenthesised form is harder to misread.
Look first, or handle it afterwards
There are two styles for dealing with something that might fail. LBYL (“look before you leap”) checks the condition first. EAFP (“easier to ask forgiveness than permission”) just does the thing and handles the failure. Counting words shows both:
words = ["tea", "milk", "tea"]
counts = {}for word in words: if word in counts: counts[word] += 1 else: counts[word] = 1print("look first:", counts)
counts = {}for word in words: try: counts[word] += 1 except KeyError: counts[word] = 1print("ask forgiveness:", counts)look first: {'tea': 2, 'milk': 1}ask forgiveness: {'tea': 2, 'milk': 1}Both are correct, and for a dictionary the first is the clearer one. EAFP earns its place in two situations. The first is when the world can change between the check and the action. Files are the usual case:
try: with open("no_such_settings.txt", encoding="utf-8") as file: settings = file.read()except FileNotFoundError: settings = "defaults"print(settings)defaultsChecking “does the file exist?” first would not make this safe: another program could delete the file in the moment between your check and your open, and you would still need the try. So you may as well use only the try.
The second situation is when the check is as hard as the work. There is no cheap test for “will int(text) succeed?” other than calling int(text). The fundamentals lesson on errors discusses this trade-off in general terms. Python’s own habit is to lean towards EAFP, but treat it as a tool, not a rule.
Common pitfalls
- Catching
OSErrorwhen you meant one case.FileNotFoundErrorandPermissionErrorare bothOSError. A handler for the parent treats “not there” and “not allowed” the same, which usually hides a real problem. Catch the child you can actually deal with. - EAFP around code that half-finishes. If the
trybody appends to a list and then fails, the list stays changed. Use EAFP when the risky step is the first or only step that changes anything. - Using exceptions when failure is the common case. Raising and catching is cheap when it is rare and noticeably slower when most iterations fail. If most of your keys are new,
if word in countsis the better fit. - Writing
except A, B as e:on 3.14. It is aSyntaxError(“multiple exception types must be parenthesized when using ‘as’”). Add the parentheses.
This loop looks before it leaps. Rewrite it with try and except KeyError, so that it prints exactly the same two lines:
settings = {"theme": "dark"}for key in ["theme", "font"]: if key in settings: print(key, "=", settings[key]) else: print(key, "is not set")Expected output:
theme = darkfont is not setShow solution
settings = {"theme": "dark"}for key in ["theme", "font"]: try: print(key, "=", settings[key]) except KeyError: print(key, "is not set")theme = darkfont is not setThe lookup settings[key] is the only thing that can raise KeyError, so it is safe to put the print in the same try. Note that print runs only after the lookup succeeded, because its argument is evaluated first.
What does this print? Write down all four lines before you run it. The calls are lookup([1], 3), lookup({}, "k"), lookup(None, 0) and lookup([1, 2], 1).
def lookup(data, key): try: return data[key] except IndexError: return "index" except LookupError: return "lookup" except Exception: return "other"
print(lookup([1], 3))print(lookup({}, "k"))print(lookup(None, 0))print(lookup([1, 2], 1))Show solution
indexlookupother2[1][3] raises IndexError, so the first clause matches. {}["k"] raises KeyError, which is not an IndexError but is a LookupError, so the second clause matches. None[0] raises TypeError (“‘NoneType’ object is not subscriptable”), which is neither, so except Exception catches it. The last call succeeds and returns 2, so no handler runs.
Write parse_number(value) that returns an int if the value reads as a whole number, a float if it reads as a decimal, and None otherwise (including when value is None). Try int first and float second. For the inputs "42", "3.5", " 7 ", "abc", None and "1e3", print repr(item), "->", parse_number(item). Expected output:
'42' -> 42'3.5' -> 3.5' 7 ' -> 7'abc' -> NoneNone -> None'1e3' -> 1000.0Show solution
def parse_number(value): for convert in (int, float): try: return convert(value) except (ValueError, TypeError): continue return None
for item in ["42", "3.5", " 7 ", "abc", None, "1e3"]: print(repr(item), "->", parse_number(item))'42' -> 42'3.5' -> 3.5' 7 ' -> 7'abc' -> NoneNone -> None'1e3' -> 1000.0The loop tries int and then float. A failed conversion raises ValueError (for text like "abc") or TypeError (for None), so the clause names both, and continue moves on to the next converter. int ignores the spaces around " 7 ". "1e3" is not a valid int, but float reads it as scientific notation. If neither converter works, the loop ends and the function returns None.
Summary
- Every exception has parents, and an
exceptclause catches its type and everything built on it.LookupErrorcoversKeyErrorandIndexError;OSErrorcovers the file errors. - Python runs the first matching clause from the top, so put specific types before general ones.
- List several types as
except (A, B):. Requires version 3.14 or laterexcept A, B:also works when there is noas. - Check first when the check is cheap and decisive; handle the error afterwards when the situation can change or the check is as hard as the work.