Topic 7.1
Exceptions: try, except, else, finally
In one line
An exception is Python's way of saying that something went wrong at this exact line. try/except lets you catch a specific kind of failure and carry on, else runs only when nothing failed, and finally always runs for cleanup.
Think of it like this
A waiter carrying a tray. If a glass slips, the waiter does not stop the whole restaurant. They shout "dropped glass!", someone comes with a cloth, and service continues. An exception is that shout: a piece of code signals "something went wrong here", and the nearest code that knows how to deal with it takes over. If nobody knows how, the shout reaches the manager (Python itself) and the program stops with a report called a traceback.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Exception
- An object that Python creates when something goes wrong. It carries the kind of error and a message.
- Raise
- To signal an exception. Python raises one for you when code fails; you can raise your own.
- Catch (handle)
- To write code that runs when a certain exception happens, so the program does not crash.
- try / except
- The statement that runs code and jumps to a handler if a matching exception is raised.
- else (on try)
- A block that runs only when the try block raised no exception.
- finally
- A block that always runs at the end, whether or not there was an error. Used for cleanup.
- Traceback
- The report Python prints for an uncaught exception, listing the chain of calls that led to it.
- EAFP / LBYL
- Two styles: try first and handle failure (Python's usual style) versus check first, then act.
Step by step
01Without a handler the program stops
This program converts a piece of text to a number. int("4x2") cannot succeed, so Python raises a ValueError and the program stops at that line. The second print never runs.
text = "4x2"
number = int(text)
print("Parsed:", number)02Catch the one error you expect
Wrap the risky line in try: and name the error after except. If int(text) raises ValueError, Python skips the rest of the try block and runs the handler. After the handler the program carries on normally.
Notice we caught ValueError only. If a different error appeared (say a NameError from a typo), it would still show up loudly, which is what we want.
text = "4x2"
try:
number = int(text)
except ValueError:
number = 0
print("Not a whole number, using 0")
print("Number is", number)03The exception object: as e
except ValueError as e: gives the exception object a name. Printing it shows the message Python attached, and type(e).__name__ gives the class name. Use the message in logs or in an error shown to the user. The name e exists only inside the handler and is deleted when the handler ends.
try:
int("4x2")
except ValueError as e:
print("Class:", type(e).__name__)
print("Message:", e)04Several handlers, and the family tree
Recall from Topic 5.1: def makes a function and return hands a value back. The function below uses both.
You can stack except blocks for different errors, or list several classes in a tuple to share one handler. Python checks the handlers from top to bottom and runs the first one that matches. Put the most specific class first: if except Exception came first, it would catch everything and the specific handlers below it could never run.
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return float("inf")
except (TypeError, ValueError) as e:
print("Bad input:", e)
return None05else and finally
else is for code that should run only on success. Keeping it out of the try block means a bug in that code is not accidentally caught by your except. finally runs last, always. The order is: try, then (except if it failed, or else if it succeeded), then finally.
finally even runs when the try block uses return: Python finishes the finally first, then returns. Never put a return inside finally, because it silently replaces the real result and cancels any exception in flight.
06EAFP versus LBYL
Both functions below read a price from a dictionary. The LBYL version looks first. The EAFP version just tries and handles KeyError. EAFP does one lookup instead of two and stays correct even if the data changes between the check and the use, which matters with files and shared data.
Use LBYL when failure is common and the check is the natural logic (if not items:). Use EAFP when failure is the exception to the rule.
prices = {"tea": 20, "coffee": 35}
# LBYL: look before you leap
if "juice" in prices:
cost = prices["juice"]
else:
cost = 0
# EAFP: ask forgiveness
try:
cost = prices["juice"]
except KeyError:
cost = 0Try it yourself
- 1
Predict the order
Before running the first example, write down the order of the printed lines for "42" and for "4x2". Then run it. Which line is the only one that appears in both runs?
- 2
Swap the handlers
In the third example, move
except Exceptionaboveexcept ZeroDivisionError, and run it. Which value now gets the wrong message, and why? This is the "most specific first" rule in action. - 3
Catch a different error
In the fourth example, add the value
3.7to the list. Doesint("3.7")work? Add the valueNoneas well and see which exception appears. Then decide whether to catch it too.
Code & diagrams
Run it and read the order of the lines: for a good value the else block runs; for a bad value the except block runs. The finally line appears both times.
Expected output
Trying 42
'42' parsed fine
finished with '42'
result: 42
Trying 4x2
'4x2' is not a whole number
finished with '4x2'
result: NoneEvery class on the right of an arrow is a parent. Catching a parent catches the child.
Expected output
ZeroDivisionError -> ArithmeticError -> Exception -> BaseException -> object
KeyError -> LookupError -> Exception -> BaseException -> object
FileNotFoundError -> OSError -> Exception -> BaseException -> object
KeyboardInterrupt -> BaseException -> object
Caught as LookupError: KeyError 'missing'Python runs the first handler that matches. Try swapping the two except blocks to see the specific one never run.
Expected output
4 -> 25.0
0 -> cannot divide by zero
'x' -> needs a number
None -> needs a numberA tiny version of what a data pipeline does: one bad record is reported and skipped, and the rest are still processed.
Expected output
Total of good records: 70
Skipped: ['abc', '']Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Catch the wrong class
Handle a KeyError but let a ValueError happen.
try:
number = int("abc")
except KeyError:
number = 0
print(number)Break #2
A bare except hides a typo
Wrap a block in except: and misspell a name inside it.
total = 10
try:
print("Total is", totl)
except:
print("Something went wrong")Myth vs fact
Myth
Exceptions are for rare disasters, and good code avoids them.
Fact
In Python exceptions are an ordinary control tool. Iterating with a for loop even ends internally with a StopIteration exception. Catching KeyError or ValueError is everyday code.
Myth
except Exception: is safe because it catches everything.
Fact
Catching everything also catches your own bugs (typos, wrong types) and turns them into silent wrong behaviour. Catch what you can handle.
Myth
finally only runs when there is an error.
Fact
finally runs every time: on success, on a handled error, on an unhandled error, and even when the try block returns.
Myth
try makes code slower, so avoid it.
Fact
In current CPython a try block that raises nothing costs almost nothing. Raising and catching is slower than an if, so use LBYL when failure is the normal case, but never avoid try for speed in ordinary code.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Python 3.11 introduced "zero-cost" exception handling: entering a
tryblock adds no instructions on the normal path. The handler lookup uses a table consulted only when something is raised. That is why EAFP is cheap when failures are rare. - ▸
exceptclauses match withisinstanceagainst the raised exception, so you can catch a tuple of classes, and you can build custom hierarchies (Topic 7.2) so callers catch one parent class for a whole family of problems. - ▸
A caught exception keeps references to the traceback and, through it, to every local variable of the frames involved. That is why
eis deleted at the end of the handler: to break reference cycles that would keep large objects alive. If you need the exception later, store it under another name deliberately, or store only its message. - ▸
In
asynciocode, never swallowasyncio.CancelledError(aBaseExceptionsubclass since Python 3.8). Doing so breaks cancellation and timeouts. This is one more reason not to write bareexcept:blocks. - ▸
A
return,breakorcontinueinsidefinallydiscards any exception that is in flight and overrides the value being returned. It is legal, so for years it was a silent trap; Python 3.14 (PEP 765) now emits aSyntaxWarningfor it. An exception raised inside a handler or insidefinallyreplaces the one being handled (the old one stays reachable as__context__, Topic 7.2). To ignore one specific, expected error,contextlib.suppress(FileNotFoundError)says it more clearly than an emptyexceptblock.
Remember this
- 1
When Python hits a problem it cannot continue past, for example
int("abc")or10 / 0, it raises an exception. It stops running the current line. Then it searches upward, from the current function to the function that called it, and so on, for code that catches that kind of exception. If it finds none, the program ends and prints the traceback you met in Topic 0.7. Raising and catching is not shameful. It is the normal way Python code reports problems. - 2
The tool for catching is the
trystatement. Code that might fail goes in thetry:block. Eachexcept SomeError:block below it says: if this kind of exception is raised inside thetry, jump here instead of crashing. Only the matchingexceptruns, and after it the program continues with the next statement after the wholetry. - 3
Exceptions are classes arranged in a family tree. (A class is a blueprint for making objects. Phase 8 teaches classes, so for now just read "class" as "a kind of thing, with a name".)
BaseExceptionis the root. Below it,Exceptionis the parent of almost every error you will meet:ValueError,TypeError,KeyError(a kind ofLookupError),ZeroDivisionError(a kind ofArithmeticError),FileNotFoundError(a kind ofOSError). Catching a parent catches all of its children, soexcept LookupErrorcatches bothKeyErrorandIndexError.KeyboardInterruptandSystemExitsit directly underBaseException, outsideException, so a normalexcept Exceptiondoes not swallow Ctrl+C. - 4
Catch specific errors, never everything. A bare
except:(no class named) or anexcept Exception:that only hides the problem turns a loud, findable bug into a silent wrong answer. The rule of thumb: catch an exception only if you know what went wrong and have a sensible response (use a default, retry, show a friendly message, skip this item). Otherwise let it travel upward to code that can decide. - 5
The full shape has two more clauses.
else:runs only if thetryblock finished without an exception, which keeps the risky part small and the follow-up work outside it.finally:runs always: after success, after a handled error, even if thetryblock doesreturn. Usefinallyfor cleanup that must happen no matter what (closing a connection, removing a temporary file). Topic 7.4 showswith, which is the neater way to do most cleanup. - 6
Python programmers usually follow EAFP: "Easier to Ask Forgiveness than Permission". Just try the operation and handle the failure. The opposite style, LBYL ("Look Before You Leap"), checks conditions first (
if key in data:). EAFP is often shorter. It also avoids a race: a file you checked can vanish in the moment before you open it. LBYL is clearer when the check is cheap and is the natural rule ("if the list is empty, say so"). This matters for AI code: a pipeline that processes 10,000 documents should catch the failure for document 7,312, log it, and carry on with the rest.
Explain it without notes
What happens, step by step, when an exception is raised and no handler is found in the current function?
Why is a bare except: considered bad practice, and what should you do instead?
What do else and finally do in a try statement, and why is else not just 'code after the try'?
What are EAFP and LBYL? When would you pick each?
Practice
Write safe_int(text, default=0) that returns int(text), or default if the text is not a whole number. Test it with "12", "x" and "".
Write average(numbers) that returns the average of a list, or None for an empty list. Use try/except ZeroDivisionError.
Write a loop over ["3", "0", "x", "5"] that prints 10 / int(item) for each, printing a different friendly message for ZeroDivisionError and for ValueError.
Trade-offs
- ↔
Catching close to the source versus far away. Catching near the failure lets you recover precisely (use a default). Catching far away (one handler at the top) keeps the code clean but can only report and stop. Most programs do both.
- ↔
Exceptions versus return codes. Returning
Noneor-1for failure lets callers forget to check and carry on with bad data. An exception cannot be ignored by accident. The cost is that a caller must know which exceptions a function can raise, so document them.
Done when you can
Done when you can write try/except/else/finally and say in which order the blocks run.
Done when you can name the exception parent classes
Exception,LookupErrorandOSErrorand what they cover.Done when you can explain why a bare
except:is dangerous and what to write instead.Done when you can read
except A as eand usestr(e)andtype(e).__name__.Done when you can choose between EAFP and LBYL for a given problem.