Python Try Except Finally
When something goes wrong while a Python program is running — a file is missing, a user types text instead of a number, a network call times out — Python raises an exception. Without any handling, that exception crashes the program and prints a traceback. The try, except, else, and finally statement is how you intercept that failure, decide what to do about it, and make sure cleanup code still runs no matter what happens. Mastering it is what separates a script that dies on the first bad input from a program that handles the real world gracefully.
Overview: How Exception Handling Works
Every error in Python is represented by an exception object — an instance of a class like ValueError, TypeError, or ZeroDivisionError, all of which ultimately inherit from the built-in BaseException (most of them from its subclass Exception). When code raises an exception, Python does not simply stop: it immediately abandons the rest of the current statement block and starts unwinding the call stack, looking outward through each enclosing try block for a matching except clause. If it finds one whose exception type matches (or is a superclass of) the raised exception, execution jumps into that handler. If no handler anywhere in the call stack matches, the exception propagates all the way to the top level, Python prints the traceback, and the program terminates.
A try statement can have four kinds of clauses, and understanding when each one runs is the key to using them correctly:
- try — the code you want to attempt. Python starts executing it and watches for exceptions.
- except — runs only if an exception matching its type was raised inside the
tryblock. You can have several of these for different exception types. - else — runs only if the
tryblock completed with no exception at all. It’s optional, and it lets you separate "code that might fail" from "code that should only run on success." - finally — runs unconditionally: whether the
trysucceeded, an exception was caught, an exception was not caught, or the block exited viareturn,break, orcontinue. It is Python’s guarantee for cleanup code (closing files, releasing locks, disconnecting from a database).
Internally, once an exception object is created, Python attaches a __traceback__ to it recording every frame the error passed through, and (since Python 3) an exception raised while handling another one gets its __context__ set automatically, which is why you sometimes see "During handling of the above exception, another exception occurred" in a traceback.
Common built-in exception types
| Exception | Raised when… |
|---|---|
ValueError |
A value has the right type but an inappropriate value (e.g. int("abc")) |
TypeError |
An operation is applied to an object of the wrong type |
ZeroDivisionError |
Dividing or taking the modulus by zero |
KeyError |
A dictionary key does not exist |
IndexError |
A sequence index is out of range |
FileNotFoundError |
Opening a file path that doesn’t exist |
AttributeError |
An object has no attribute with that name |
Syntax
The full general form looks like this:
try:
pass # code that might raise an exception
except SomeException:
pass # runs if SomeException is raised
except (AnotherException, YetAnotherException) as e:
pass # runs if either is raised; e holds the exception object
else:
pass # runs only if no exception occurred
finally:
pass # always runs, exception or not
except SomeException:catches only that exact class (or its subclasses).except (A, B) as e:catches several types in one clause;eis bound to the raised exception instance, so you can inspectstr(e),e.args, or custom attributes.- You only need the clauses you actually use —
except,else, andfinallyare each optional, but a baretrymust have at least oneexceptor afinally. - Clauses are checked top to bottom; put more specific exception types before more general ones, since Python uses the first match.
Examples
Example 1: try / except / else / finally together
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
return None
else:
print("Division succeeded.")
return result
finally:
print("Division attempt finished.")
print(safe_divide(10, 2))
print(safe_divide(10, 0))
Output:
Division succeeded.
Division attempt finished.
5.0
Error: Cannot divide by zero.
Division attempt finished.
None
For safe_divide(10, 2), the division succeeds, so the else block runs and prints "Division succeeded." before its return result executes. Crucially, finally still runs before the function actually hands control back to the caller, which is why "Division attempt finished." appears before 5.0 is printed by the outer print(). For safe_divide(10, 0), the ZeroDivisionError is caught, the else block is skipped entirely (it only runs on success), and finally still runs before returning None.
Example 2: multiple except clauses in a loop
def parse_config_value(raw_value):
try:
value = int(raw_value)
threshold = 100 / value
except ValueError:
print(f"'{raw_value}' is not a valid integer.")
except ZeroDivisionError:
print("Value cannot be zero.")
else:
print(f"Threshold computed: {threshold}")
finally:
print("Finished processing one config value.\n")
for raw in ["20", "0", "abc"]:
parse_config_value(raw)
Output:
Threshold computed: 5.0
Finished processing one config value.
Value cannot be zero.
Finished processing one config value.
'abc' is not a valid integer.
Finished processing one config value.
Each iteration hits a different clause: "20" converts fine and only fails when dividing (no — it doesn’t fail at all, 100/20 works, so else runs); "0" converts fine but then 100 / 0 raises ZeroDivisionError; "abc" fails at int("abc") with ValueError. Notice that finally fires every single time regardless of which branch was taken — that’s what makes it reliable for logging or cleanup that must always happen.
Example 3: custom exceptions with else and finally
class InsufficientFundsError(Exception):
"""Raised when a withdrawal exceeds the available balance."""
def __init__(self, balance, amount):
super().__init__(f"Cannot withdraw {amount}; balance is only {balance}.")
self.balance = balance
self.amount = amount
def withdraw(balance, amount):
try:
if amount <= 0:
raise ValueError("Withdrawal amount must be positive.")
if amount > balance:
raise InsufficientFundsError(balance, amount)
balance -= amount
except InsufficientFundsError as e:
print(f"Transaction blocked: {e}")
except ValueError as e:
print(f"Invalid input: {e}")
else:
print(f"Withdrew {amount}. New balance: {balance}")
finally:
print("Transaction attempt logged.")
return balance
balance = 100
balance = withdraw(balance, 30)
balance = withdraw(balance, 200)
balance = withdraw(balance, -5)
print(f"Final balance: {balance}")
Output:
Withdrew 30. New balance: 70
Transaction attempt logged.
Transaction blocked: Cannot withdraw 200; balance is only 70.
Transaction attempt logged.
Invalid input: Withdrawal amount must be positive.
Transaction attempt logged.
Final balance: 70
This example defines a custom exception, InsufficientFundsError, by subclassing Exception and building a descriptive message in __init__. Notice that when an exception is raised inside try, the assignment balance -= amount never runs, so the balance is left unchanged for the failed attempts — only the successful withdrawal of 30 actually reduces it. Custom exceptions like this are extremely common in real applications because they let calling code distinguish "this failed because of business rules" from "this failed because of a bug," using except InsufficientFundsError instead of a generic catch.
Under the Hood: What Python Actually Does
When Python enters a try block, it doesn’t do anything special upfront — there’s no cost to a try that never raises. It’s only when a raise statement executes (either explicitly, or implicitly inside a built-in operation like division or indexing) that Python creates the exception object, attaches the current call stack to it as a traceback, and begins searching outward for a handler. This search happens frame by frame: the current function, then whoever called it, and so on, checking each enclosing try‘s except clauses in order for a type match using isinstance-style checks against the exception class hierarchy.
The finally block is special because Python guarantees it executes no matter how control leaves the try/except/else structure — normal completion, an exception, or even a return, break, or continue statement inside the block. If the try block hits a return, Python computes the return value, then runs finally before actually handing control back to the caller — which is why a return or raise inside finally can silently override whatever the try or except was trying to do (see Common Mistakes below). If no handler catches the exception anywhere up the stack, it eventually reaches the top level, Python prints the traceback to stderr, and the interpreter exits with a non-zero status.
You can also deliberately preserve the connection between two errors using exception chaining: raise NewError("...") from original_error sets __cause__ on the new exception, so the traceback shows both errors and explains that the second was raised while handling the first — useful when you catch a low-level exception and re-raise a more meaningful, higher-level one.
Common Mistakes
Mistake 1: Catching everything with a bare except:
Writing except: with no exception type catches literally everything, including KeyboardInterrupt (Ctrl+C) and SystemExit, which means your program can become impossible to stop cleanly and real bugs get silently swallowed. The fix is to always name the exception type you expect, such as except ValueError:, or at worst except Exception:, which excludes those system-level signals while still catching ordinary errors.
Mistake 2: Putting return inside finally
A return (or unconditional raise) inside a finally block silently discards whatever the try or except block was about to return or raise. Here is the buggy version:
def risky_calculation(x):
try:
return 10 / x
except ZeroDivisionError:
print("Caught division by zero")
raise
finally:
return -1
print(risky_calculation(0))
print(risky_calculation(2))
Output:
Caught division by zero
-1
-1
Even though except re-raises the ZeroDivisionError with a bare raise, the finally block’s return -1 overrides it completely — the exception never reaches the caller, and even the successful case (risky_calculation(2), which should return 5.0) gets clobbered into -1 too. The corrected version keeps finally for cleanup only, with no return or raise in it:
def risky_calculation(x):
try:
return 10 / x
except ZeroDivisionError:
print("Caught division by zero")
raise
finally:
print("Cleanup complete")
print(risky_calculation(2))
try:
risky_calculation(0)
except ZeroDivisionError:
print("Exception propagated correctly")
Output:
Cleanup complete
5.0
Caught division by zero
Cleanup complete
Exception propagated correctly
Best Practices
- Catch the most specific exception type you can (
ValueError, not a bareexcept:) so unrelated bugs don’t get hidden. - Never leave an
exceptblock empty (except Exception: pass) — at minimum log the error, since silent failures are extremely hard to debug later. - Use
elseto keep code that should only run on success out of thetryblock, so you don’t accidentally catch exceptions raised by that follow-up code too. - Prefer context managers (
with open(...) as f:) over manualtry/finallyfor resource cleanup when the object supports it — it’s shorter and equally safe. - Avoid
return,break, orcontinueinside afinallyblock; they silently swallow any exception in flight. - Use
raise NewError(...) from originalwhen translating a low-level exception into a higher-level one, so the original traceback isn’t lost. - Don’t use exceptions for routine control flow where a simple
ifcheck is clearer and cheaper — reserve them for genuinely exceptional situations. - Define custom exception classes for your own domain errors instead of raising generic
Exception, so callers can catch precisely what they expect.
Practice Exercises
- Write a function
convert_to_float(value)that tries to convertvalueto afloatand return it. If aValueErroroccurs, print a message and return0.0instead. Usefinallyto print"Conversion attempt complete"every time, whether or not the conversion succeeded. - Write a function
read_first_item(sequence)that returnssequence[0]. CatchIndexErrorfor an empty sequence andTypeErrorfor a non-indexable argument (like anint), printing a distinct message for each. Useelseto print the retrieved item only when no exception occurred. - Create a custom exception class
NegativeValueErrorand a functionsquare_root(x)that raises it whenxis negative, and otherwise returnsx ** 0.5. Call it inside atry/exceptthat catchesNegativeValueErrorand prints a friendly message, with afinallyblock that always prints"Calculation attempt finished".
Summary
trywraps code that might raise an exception;excepthandles specific exception types when they occur.elseruns only when thetryblock completes with no exception, keeping success-path code separate from the risky code.finallyalways runs — on success, on a caught exception, on an uncaught exception, and even acrossreturn/break/continue— making it the right place for guaranteed cleanup.- A
returnorraiseplaced insidefinallysilently overrides whatever thetry/exceptwas doing — avoid it. - Catch specific exception types rather than using a bare
except:, which also catchesKeyboardInterruptandSystemExit. - Custom exception classes (subclassing
Exception) let you model domain-specific failure cases clearly for callers. - Use
raise ... from ...to chain exceptions and preserve the original cause when translating errors.
