Python Exception Handling
An exception is Python’s way of saying “something went wrong here”: a file is missing, a number cannot be parsed, a key does not exist. Handling exceptions lets your program recover, retry or report the problem clearly instead of crashing.
- Read a traceback
- Catch specific exceptions with try/except
- Use else and finally correctly
- Raise your own and define custom exceptions
What an exception looks like
When an error is not handled, Python stops and prints a traceback. Read it from the bottom: the last line names the exception and the message, and the lines above show where it happened.
def average(values): return sum(values) / len(values) print(average([10, 20]))print(average([]))15.0
Traceback (most recent call last):
File "main.py", line 5, in <module>
print(average([]))
~~~~~~~^^^^
File "main.py", line 2, in average
return sum(values) / len(values)
~~~~~~~~~~~~^~~~~~~~~~~~~
ZeroDivisionError: division by zeroCommon built-in exceptions
| Exception | Typical cause |
|---|---|
ValueError | Right type, wrong value: int("abc") |
TypeError | Wrong type: "2" + 2 |
KeyError | Missing dict key |
IndexError | List index out of range |
ZeroDivisionError | Division by zero |
FileNotFoundError | Opening a file that does not exist |
AttributeError | Object has no such attribute or method |
NameError | Using a name that was never defined |
try and except
Put the risky code in try. If an exception happens, Python jumps to the matching except block and continues after it.
text = "12a"try: number = int(text) print("Parsed", number)except ValueError: print(f"{text!r} is not a number")print("program continues")'12a' is not a number program continues
Catch specific exceptions
Catch only the errors you expect and know how to handle. Use several except blocks, or a tuple, for different errors. as e gives you the exception object.
def safe_divide(a, b): try: return a / b except ZeroDivisionError: return "cannot divide by zero" except TypeError as e: return f"bad input: {e}" print(safe_divide(10, 4))print(safe_divide(10, 0))print(safe_divide(10, "2"))2.5 cannot divide by zero bad input: unsupported operand type(s) for /: 'int' and 'str'
data = {"items": [1, 2]}for key, index in [("items", 5), ("users", 0)]: try: print(data[key][index]) except (KeyError, IndexError) as e: print(type(e).__name__, e)IndexError list index out of range KeyError 'users'
Avoid a bare except: or except Exception: that swallows every error silently. It hides real bugs, including typos. If you must catch broadly, log the error.
else and finally
elseruns only if thetryblock raised no exception.finallyruns always, error or not. Use it for clean-up such as closing a connection.
def read_number(text): try: n = int(text) except ValueError: print(" invalid:", text) else: print(" valid:", n) finally: print(" done with", repr(text)) read_number("42")read_number("x")valid: 42 done with '42' invalid: x done with 'x'
For files and connections, a with statement does the clean-up for you, so you rarely need finally for that. See File Handling.
Raise an exception
Use raise to signal that something is wrong in your own code. Choose the built-in exception that fits best and write a helpful message.
def set_age(age): if not isinstance(age, int): raise TypeError(f"age must be an int, got {type(age).__name__}") if age < 0: raise ValueError("age cannot be negative") return age for value in [30, -1, "30"]: try: print("ok", set_age(value)) except (TypeError, ValueError) as e: print("rejected:", e)ok 30 rejected: age cannot be negative rejected: age must be an int, got str
A bare raise inside except re-raises the same exception after you have logged it:
try: {}["missing"]except KeyError: print("logging the error, then passing it on") raiselogging the error, then passing it on
Traceback (most recent call last):
File "main.py", line 2, in <module>
{}["missing"]
~~^^^^^^^^^^^
KeyError: 'missing'Custom exceptions
Create your own exception classes by inheriting from Exception. They make errors easy to recognise and catch.
class InsufficientFundsError(Exception): def __init__(self, balance, amount): super().__init__(f"need {amount}, only {balance} available") self.balance = balance self.amount = amount def withdraw(balance, amount): if amount > balance: raise InsufficientFundsError(balance, amount) return balance - amount try: withdraw(500, 800)except InsufficientFundsError as e: print("Declined:", e) print("Short by", e.amount - e.balance)Declined: need 800, only 500 available Short by 300
Exception chaining
raise ... from ... keeps the original error attached, so the traceback shows both:
def load_port(config): try: return int(config["port"]) except (KeyError, ValueError) as e: raise RuntimeError("invalid config: port") from e load_port({"port": "eighty"})Traceback (most recent call last):
File "main.py", line 3, in load_port
return int(config["port"])
ValueError: invalid literal for int() with base 10: 'eighty'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "main.py", line 7, in <module>
load_port({"port": "eighty"})
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
File "main.py", line 5, in load_port
raise RuntimeError("invalid config: port") from e
RuntimeError: invalid config: portRetry pattern
attempts = iter(["timeout", "timeout", "ok"]) def call_api(): result = next(attempts) if result == "timeout": raise TimeoutError("server did not answer") return "200 OK" for attempt in range(1, 4): try: print(call_api(), "on attempt", attempt) break except TimeoutError as e: print(f"attempt {attempt}: {e}")attempt 1: server did not answer attempt 2: server did not answer 200 OK on attempt 3
Check your understanding
Predict the output
def f(): try: return "try" finally: print("finally runs first") print(f())finally runs first try
finally runs even when try returns, just before the function actually hands back its value.
Quick quiz
1. When does the else block of a try statement run?
else is for code that should run only when the try block succeeded.
2. Which exception does int("abc") raise?
The type (str) is acceptable, but the value cannot be converted.
3. How do you create a custom exception?
class MyError(Exception): pass is enough.
4. Why is a bare except: discouraged?
Catch only the exceptions you can handle.
Exercise: safe average
Write safe_average(items) that converts every item to float, skips items that cannot be converted (printing a warning), and returns 0 for an empty result.
["10", "abc", "20.5", None, "30"]
skipping 'abc' skipping None 20.166666666666668 0
Hint 1
Catch both ValueError (bad text) and TypeError (None).
def safe_average(items): numbers = [] for item in items: try: numbers.append(float(item)) except (ValueError, TypeError): print("skipping", repr(item)) if not numbers: return 0 return sum(numbers) / len(numbers) print(safe_average(["10", "abc", "20.5", None, "30"]))print(safe_average([]))skipping 'abc' skipping None 20.166666666666668 0
float("abc") raises ValueError and float(None) raises TypeError, so both are caught. Checking if not numbers before dividing avoids a ZeroDivisionError.
Program Practice
20 Python Exception Handling Programs
Every program has a step-by-step explanation, colour-coded code and its real output.
Python exception handling FAQ
What is the difference between an error and an exception in Python?
In everyday use they mean the same thing. Technically, syntax errors stop the file from running at all, while exceptions happen while the program runs and can be caught.
Can I catch multiple exceptions in one except?
Yes. Use a tuple: except (ValueError, TypeError) as e:.
What is the purpose of finally in Python?
finally holds clean-up code that must run whether or not an exception happened, such as closing a connection.
How do I print the full traceback of a caught exception?
Use the traceback module: traceback.print_exc() inside the except block, or logging.exception("message").