When Things Go Wrong: Exceptions
Reading a traceback, catching only what you can handle, and why except Exception hides the bug you most needed to see. try, except, else and finally, and raising your own.
Reading a traceback, catching only what you can handle, and why except Exception hides the bug you most needed to see. try, except, else and finally, and raising your own.
Your photo script has worked perfectly for a week. This morning
someone put a file called notes.txt in the photos folder, and
the whole thing stopped on photo eleven of four hundred — with a
wall of red text and no indication of which photos had been
processed and which had not.
Every program meets things it did not expect. The question is not how to prevent that, because you cannot; it is what your program does when it happens. By the end of this lesson you will be able to read the red text, decide what to catch and what to let through, and — the part people get wrong — know why catching everything is worse than catching nothing.
When Python cannot do what you asked, it raises an exception: it stops what it was doing and looks for someone who has said they will handle this situation. If nobody has, the program stops and prints a traceback.
int("four")
# ValueError: invalid literal for int() with base 10: 'four'That is not a crash in the sense of something going wrong with Python. It is Python working correctly — refusing to guess, and telling you exactly what it could not do.
The ones you will meet first:
ValueError right type, impossible value int("four")
TypeError wrong type entirely "a" + 5
KeyError no such key in a dictionary d["missing"]
IndexError position beyond the end photos[999]
FileNotFoundError no file at that path open("nope.txt")
ZeroDivisionError dividing by zero 10 / 0
AttributeError no such method or attribute "text".appendx()
NameError no such name print(phtoo)Reading them is a skill worth ten minutes. AttributeError: 'NoneType' object has no attribute 'name' looks cryptic and
says something very specific: something you expected to be an
object is None, and the real bug is wherever that None came
from — usually a function that returned nothing, several lines
earlier.
try and except say "attempt this; if that particular thing
goes wrong, do this instead":
try:
count = int(input("How many photos? "))
except ValueError:
print("That was not a number. Using 100.")
count = 100Everything in the try block runs normally until something
raises. If what raises matches the except, the handler runs
and the program continues. If it does not match, the exception
carries on upward as though there were no try at all.
You can handle several kinds:
try:
photo = load(path)
except FileNotFoundError:
print(f"No file at {path}")
except PermissionError:
print(f"Cannot read {path} - check permissions")Or treat several the same way:
except (FileNotFoundError, PermissionError) as error:
print(f"Could not read {path}: {error}")as error gives you the exception object, which prints as its
message and carries details. Including it costs nothing and is
the difference between a log line that helps and one that says
"something went wrong".
Now the rule the whole lesson turns on.
Bad — catching everything and carrying on.
for path in paths:
try:
process(load(path))
except Exception:
continueGood — catching the expected failure, letting the rest through.
The first version does what it was written to do — one bad file
does not stop the run. It also swallows every other possible
problem: the typo in process that raises AttributeError, the
disk filling up, the bug that raises KeyError on every single
photo. The script reports success, the output folder is empty,
and there is no record anywhere of why. except Exception: continue is how a program lies to you.
The second catches the two failures it genuinely knows how to respond to and says so out loud. Anything else stops the program and prints a traceback — which is the correct outcome for a problem nobody anticipated, because a loud failure gets fixed and a silent one does not.
The principle: catch the exception you have a plan for. If
your handler is pass, or continue, or a comment saying "this
shouldn't happen", you did not have a plan.
try takes two more clauses, and both earn their place. All
four together read in order.
try — the one line that might fail
Keep it small. A fat try block catches failures from code
you were not worried about and blames them on the code you
were.
except — only if it raised
And only if the kind matches. Anything else carries on
upward as though the try were not there.
else — only if it did not raise
The follow-on work. Putting it here rather than inside the
try is how you keep the try block down to one line.
finally — either way, always
Success, exception, even a return from inside the try.
This is the cleanup guarantee.
else runs only if nothing was raised:
Putting process(photo) here rather than inside the try keeps
the try block down to the one line that might fail. That
matters: if process happened to raise FileNotFoundError for
its own reasons, a fat try block would catch it and blame the
load.
finally runs either way, raised or not:
That is the cleanup guarantee. Whatever happens — success, an
exception, even a return from inside the try — the finally
block runs. For files and connections there is a tidier tool
(with, in the next lesson), but finally is what it is built
on.
Your code can raise exceptions too, and should. When a function is given something it cannot work with, saying so immediately is kinder than continuing and producing nonsense:
Include the offending value in the message. "width must be positive" sends the reader looking; "got -50" often tells
them what is wrong on its own.
For situations specific to your program, define your own type:
That is a complete definition — you will meet class properly
in a later lesson, and this is one of the few places you need it
before then. The benefit is that callers can catch this
specifically:
With a plain ValueError they would have to catch every
ValueError in the call, including ones from unrelated code.
Two styles solve the same problem, and Python has a preference.
Python leans toward the second, and there is a concrete reason beyond taste.
Fine when the test is cheap and local.
if key in dictionary is clearer than catching a KeyError,
and nothing else can change that dictionary between the two
lines.
Against a file or a network, the same shape is a trap: the world can change in the gap, and a check that something else can invalidate is not a check.
No gap. The attempt is the check.
The file cannot be deleted between the test and the use, because there is no separate test.
This is why Python code reads the way it does around files, networks and anything else outside the program.
You can now read a traceback, handle the failures you expect, raise informative errors of your own, and guarantee cleanup runs. The judgement to keep is about scope: catch narrowly, say something when you do, and let the unexpected reach you loudly. A program that fails clearly is worth far more than one that appears to succeed.
Next is Reading and Writing Files, where most of these
exceptions actually come from. It introduces with, which is
finally in a form you will use every day, and the encoding
questions that turn text on disk into text in your program.
Before you move on, take a program you have written and feed it
something wrong on purpose — a missing file, letters where it
wanted digits. Read each traceback, then handle exactly that one
exception and leave the others alone. Then try except Exception: pass and watch a real bug disappear without trace.
The second experiment is the one that makes the rule stick.
THE SHAPE
try:
risky()
except SpecificError as error:
handle(error)
else:
only_if_nothing_raised()
finally:
always_runs()
COMMON ONES
ValueError right type, impossible value
TypeError wrong type
KeyError no such dictionary key
IndexError position past the end
FileNotFoundError no file there
ZeroDivisionError divided by zero
AttributeError no such method - often means "it was None"
NameError no such name - usually a typo
RAISING
raise ValueError(f"width must be positive, got {width}")
class MyError(Exception): """Why this exists."""
raise MyError(...) from original keeps the real cause
THE RULES
catch what you have a plan for
never `except Exception: pass` that is how a program lies
never bare `except:` it eats Ctrl+C too
always `as error`, always log it
keep the try block to the line that can fail
put the follow-on work in `else`
STYLE
check first -> for things you control (`k in d`)
try/except -> for things you do not (files, network)
a check another process can invalidate is not a checkfor path in paths:
try:
process(load(path))
except (FileNotFoundError, UnsupportedFormatError) as error:
print(f"Skipping {path}: {error}")try:
photo = load(path)
except FileNotFoundError:
print(f"No file at {path}")
else:
process(photo) # only when the load succeededconnection = open_database()
try:
save_all(photos, connection)
finally:
connection.close() # runs even if save_all raisesdef resize(photo, width):
if width <= 0:
raise ValueError(f"width must be positive, got {width}")
...class UnsupportedFormatError(Exception):
"""Raised when a file is not an image format we can process."""try:
process(path)
except UnsupportedFormatError:
move_to_review_folder(path)except OSError as error:
raise PhotoLoadError(f"could not read {path}") from error# check first
if os.path.exists(path):
photo = load(path)
# or try, and handle failure
try:
photo = load(path)
except FileNotFoundError:
photo = None